Files
wand/cmd/config.go
2026-04-01 11:43:54 +03:00

163 lines
3.5 KiB
Go

package cmd
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/samber/lo"
"github.com/spf13/viper"
"go.yaml.in/yaml/v3"
)
type Flag struct {
Alias string `yaml:"alias"`
Description string `yaml:"description"`
Default interface{} `yaml:"default"`
Type string `yaml:"type"`
}
type Command struct {
Description string `yaml:"description"`
Cmd string `yaml:"cmd"`
Children map[string]Command `yaml:"children"`
Flags map[string]Flag `yaml:"flags"`
Env map[string]string `yaml:"env"`
WorkingDir string `yaml:"working_dir"`
Aliases []string `yaml:"aliases"`
Confirm interface{} `yaml:"confirm"`
ConfirmDefault string `yaml:"confirm_default"`
}
// GetConfirmMessage returns the confirm prompt message and whether confirmation is enabled.
func (c Command) GetConfirmMessage() (string, bool) {
switch v := c.Confirm.(type) {
case bool:
if v {
return "Are you sure?", true
}
return "", false
case string:
return v, true
}
return "", false
}
// GetConfirmDefault returns true if the default answer is yes.
func (c Command) GetConfirmDefault() bool {
return strings.EqualFold(c.ConfirmDefault, "yes") || strings.EqualFold(c.ConfirmDefault, "y")
}
type Config struct {
Shell interface{} `yaml:"shell"`
Env map[string]string `yaml:"env"`
}
func (c *Config) GetShell() string {
switch v := c.Shell.(type) {
case string:
return v
case map[string]interface{}:
key := runtimeOS()
if shell, ok := v[key]; ok {
if s, ok := shell.(string); ok {
return s
}
}
}
// fall back to $SHELL or sh
return lo.CoalesceOrEmpty(os.Getenv("SHELL"), "sh")
}
func runtimeOS() string {
switch runtime.GOOS {
case "darwin":
return "macos"
default:
return runtime.GOOS
}
}
type rawConfig struct {
DotConfig *Config `yaml:".config"`
Commands map[string]Command `yaml:",inline"`
}
// expandPath expands a leading ~ to the user's home directory.
func expandPath(path string) string {
if path == "~" || strings.HasPrefix(path, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return path
}
return filepath.Join(home, path[1:])
}
return path
}
func loadConfig(explicitPath string) (*Config, map[string]Command, error) {
var configPath string
var err error
if explicitPath != "" {
configPath = expandPath(explicitPath)
} else {
configPath, err = findConfigFile()
if err != nil {
return nil, nil, err
}
}
data, err := os.ReadFile(configPath)
if err != nil {
return nil, nil, fmt.Errorf("failed to read config: %w", err)
}
var raw rawConfig
if err := yaml.Unmarshal(data, &raw); err != nil {
return nil, nil, fmt.Errorf("failed to parse config: %w", err)
}
cfg := lo.FromPtrOr(raw.DotConfig, Config{})
commands := lo.OmitByKeys(raw.Commands, []string{".config"})
return &cfg, commands, nil
}
func findConfigFile() (string, error) {
viper.SetConfigName("wand")
viper.SetConfigType("yaml")
// ./wand.yml
viper.AddConfigPath(".")
// search up from cwd
dir, err := os.Getwd()
if err == nil {
for {
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
viper.AddConfigPath(dir)
}
}
// ~/.wand.yml and ~/.config/wand.yml
home, err := os.UserHomeDir()
if err == nil {
viper.AddConfigPath(home)
viper.AddConfigPath(filepath.Join(home, ".config"))
}
if err := viper.ReadInConfig(); err != nil {
return "", fmt.Errorf("config file not found: %w", err)
}
return viper.ConfigFileUsed(), nil
}