Find the data root via the XDG config directory
Running the tool from anywhere required either a --root flag or an exported MONEY_ROOT. Read the data root from $XDG_CONFIG_HOME/money/config.toml (~/.config/money/config.toml) instead, so it can be configured once. Resolution order is --root, then $MONEY_ROOT, then the config file, then ~/money. A leading tilde in the config file is expanded, since no shell has done it for us, and relative paths are made absolute. The new "money config" subcommand prints the resolved root and which rule chose it, which is the first thing to check when the tool reads the wrong directory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -177,3 +177,100 @@ func loadAccount(dir, slug, cfgPath string) (*Account, error) {
|
||||
func IndexPath(root string) string {
|
||||
return filepath.Join(root, StateDir, IndexFile)
|
||||
}
|
||||
|
||||
// UserConfig is the small file in the user's config directory that says where
|
||||
// the data root lives, so the tool can be run from anywhere without flags.
|
||||
type UserConfig struct {
|
||||
Root string `toml:"root"`
|
||||
}
|
||||
|
||||
// UserConfigPath returns the config file location, following the XDG base
|
||||
// directory spec: $XDG_CONFIG_HOME/money/config.toml, falling back to
|
||||
// ~/.config/money/config.toml.
|
||||
func UserConfigPath() (string, error) {
|
||||
if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" {
|
||||
return filepath.Join(dir, "money", "config.toml"), nil
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot locate the home directory: %w", err)
|
||||
}
|
||||
return filepath.Join(home, ".config", "money", "config.toml"), nil
|
||||
}
|
||||
|
||||
// LoadUserConfig reads the config file. A missing file is not an error: it
|
||||
// just means nothing overrides the default data root.
|
||||
func LoadUserConfig() (*UserConfig, string, error) {
|
||||
path, err := UserConfigPath()
|
||||
if err != nil {
|
||||
return &UserConfig{}, "", err
|
||||
}
|
||||
var c UserConfig
|
||||
if _, err := toml.DecodeFile(path, &c); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &UserConfig{}, path, nil
|
||||
}
|
||||
return nil, path, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
return &c, path, nil
|
||||
}
|
||||
|
||||
// RootSource records where a resolved data root came from, so the tool can
|
||||
// explain itself when the path is not what the user expected.
|
||||
type RootSource string
|
||||
|
||||
// The ways a data root can be chosen, in order of precedence.
|
||||
const (
|
||||
RootFromFlag RootSource = "--root flag"
|
||||
RootFromEnv RootSource = "MONEY_ROOT"
|
||||
RootFromConfig RootSource = "config file"
|
||||
RootFromDefault RootSource = "default"
|
||||
)
|
||||
|
||||
// ResolveRoot decides which data root to use. An explicit flag wins, then
|
||||
// $MONEY_ROOT, then the config file, then ~/money.
|
||||
func ResolveRoot(flagRoot string) (root string, source RootSource, err error) {
|
||||
switch {
|
||||
case flagRoot != "":
|
||||
root, source = flagRoot, RootFromFlag
|
||||
default:
|
||||
if env := os.Getenv("MONEY_ROOT"); env != "" {
|
||||
root, source = env, RootFromEnv
|
||||
} else {
|
||||
c, _, err := LoadUserConfig()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if c.Root != "" {
|
||||
root, source = c.Root, RootFromConfig
|
||||
} else {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("cannot locate the home directory: %w", err)
|
||||
}
|
||||
root, source = filepath.Join(home, "money"), RootFromDefault
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if root, err = expandHome(root); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if root, err = filepath.Abs(root); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return root, source, nil
|
||||
}
|
||||
|
||||
// expandHome resolves a leading ~, which a hand-written config file is likely
|
||||
// to contain and which the shell does not expand for us.
|
||||
func expandHome(path string) (string, error) {
|
||||
if path != "~" && !strings.HasPrefix(path, "~/") {
|
||||
return path, nil
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot expand %q: %w", path, err)
|
||||
}
|
||||
return filepath.Join(home, strings.TrimPrefix(strings.TrimPrefix(path, "~"), "/")), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user