diff --git a/README.md b/README.md index 06942e1..4740259 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # andcli -[![Go Report Card](https://goreportcard.com/badge/github.com/tjblackheart/andcli)](https://goreportcard.com/report/github.com/tjblackheart/andcli) ![Build](https://github.com/tjblackheart/andcli/actions/workflows/build.yaml/badge.svg) [![AI free project](https://img.shields.io/badge/AI-is_a_lie-cc0000?logo=githubcopilot&logoColor=white)](https://deplet.ing/the-copilot-delusion) - andcli lets you work with 2FA tokens directly in your shell, using encrypted backups exported out of your favourite 2FA apps. All the data is held in memory only and will never leave your machine. andcli can handle input from the following providers (if they provide more than 2FA support, only the OTP related entries will be consumed): @@ -49,7 +47,8 @@ By default andcli will choose the first system clipboard tool found. For Linux, ## Config file -The configuration will get persisted in the default user home config directory. For Linux, this is `$HOME/.config/andcli`. For MacOS, it's `$HOME/Library/Application Support/andcli` and for Windows it should be in `C:\Users\$USER\AppData\Roaming\andcli`. +The configuration is persisted in the default user home config directory. For Linux, this is `$XDG_CONFIG_HOME/andcli`. For MacOS, it's `$HOME/Library/Application Support/andcli`. For Windows it's `C:\Users\$USER\AppData\Roaming\andcli`. +If `$XDG_CONFIG_HOME` is set it will be used as the preferred config home dir on all OSes. ## Theming diff --git a/internal/config/config.go b/internal/config/config.go index bf85abe..eadabc7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,6 +14,8 @@ import ( "github.com/tjblackheart/andcli/v2/internal/vaults" ) +const cfgFileName = "config.yaml" + type ( Config struct { File string `yaml:"file"` @@ -44,7 +46,7 @@ func Create() (*Config, error) { if err != nil { return nil, fmt.Errorf("unable to read user directory: %s", err) } - return create(dir) + return create(resolve(dir)) } func create(dir string) (*Config, error) { @@ -54,7 +56,7 @@ func create(dir string) (*Config, error) { } cfg := &Config{ - path: filepath.Join(path, "config.yaml"), + path: filepath.Join(path, cfgFileName), Options: &Opts{ ShowUsernames: true, ShowTokens: false, @@ -213,3 +215,13 @@ func (cfg *Config) validate() error { return nil } + +func resolve(dir string) string { + path := filepath.Join(dir, buildinfo.AppName, cfgFileName) + if _, err := os.Stat(path); os.IsNotExist(err) { + if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" { + return xdg + } + } + return dir +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index d723207..d22389c 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -310,7 +310,7 @@ func Test_create(t *testing.T) { ShowTokens: false, }, Theme: &DefaultTheme, - path: filepath.Join(cfgDir, buildinfo.AppName, "config.yaml"), + path: filepath.Join(cfgDir, buildinfo.AppName, cfgFileName), dirty: true, timeout: 5, } @@ -320,6 +320,50 @@ func Test_create(t *testing.T) { } } +func Test_resolve(t *testing.T) { + writeCfg := func(dir string) { + t.Helper() + path := filepath.Join(dir, buildinfo.AppName, cfgFileName) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, nil, 0o600); err != nil { + t.Fatal(err) + } + } + + tests := []struct { + name string + xdg string + hasCfg bool + }{ + {"xdg unset uses default dir", "", false}, + {"xdg unset keeps default dir with existing config", "", true}, + {"prefers xdg when no config exists", "/tmp/xdg-test", false}, + {"keeps existing config over xdg", "/tmp/xdg-test", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", tt.xdg) + + dir := t.TempDir() + if tt.hasCfg { + writeCfg(dir) + } + + want := dir + if !tt.hasCfg && tt.xdg != "" { + want = tt.xdg + } + + if got := resolve(dir); got != want { + t.Errorf("resolve() = %q, want %q", got, want) + } + }) + } +} + func TestConfig_Flags(t *testing.T) { args := os.Args defer func() { os.Args = args }()