Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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

Expand Down
16 changes: 14 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"github.com/tjblackheart/andcli/v2/internal/vaults"
)

const cfgFileName = "config.yaml"

type (
Config struct {
File string `yaml:"file"`
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
46 changes: 45 additions & 1 deletion internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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 }()
Expand Down
Loading