// Package config loads glint's TOML configuration, falling back to defaults. package config import ( "errors" "fmt" "os" "path/filepath" "time" "github.com/BurntSushi/toml" ) // Config holds the user-tunable settings glint reads at startup. type Config struct { VaultDir string `toml:"vault_dir"` DailySubdir string `toml:"daily_subdir"` DailyFormat string `toml:"daily_format"` GlamourStyle string `toml:"glamour_style"` Theme string `toml:"theme"` InboxDir string `toml:"inbox_dir"` } // Default returns the built-in configuration used when no file is present. The // vault defaults to $GLINT_VAULT, or the current working directory when that is // unset — a config-file vault_dir (applied in loadFromFile) overrides both. func Default() Config { vault := os.Getenv("GLINT_VAULT") if vault == "" { if wd, err := os.Getwd(); err == nil { vault = wd } } return Config{ VaultDir: vault, DailySubdir: "Daily", DailyFormat: "2006-01-02", Theme: "auto", } } // Load reads ~/.config/glint/config.toml and overlays it onto the defaults. func Load() (Config, error) { home, err := os.UserHomeDir() if err != nil { return Default(), nil } return loadFromFile(filepath.Join(home, ".config", "glint", "config.toml")) } // loadFromFile is the testable core of Load. A missing file yields the // defaults with no error; a malformed file yields the defaults plus an error. func loadFromFile(path string) (Config, error) { cfg := Default() data, err := os.ReadFile(path) if err != nil { if errors.Is(err, os.ErrNotExist) { return cfg, nil // absent config is fine } return cfg, fmt.Errorf("read %s: %w", path, err) } var fileCfg Config if _, err := toml.Decode(string(data), &fileCfg); err != nil { return cfg, fmt.Errorf("parse %s: %w", path, err) } if fileCfg.VaultDir != "" { cfg.VaultDir = fileCfg.VaultDir } if fileCfg.DailySubdir != "" { cfg.DailySubdir = fileCfg.DailySubdir } if fileCfg.DailyFormat != "" { cfg.DailyFormat = fileCfg.DailyFormat } if fileCfg.GlamourStyle != "" { cfg.GlamourStyle = fileCfg.GlamourStyle } if fileCfg.Theme != "" { cfg.Theme = fileCfg.Theme } if fileCfg.InboxDir != "" { cfg.InboxDir = fileCfg.InboxDir } return cfg, nil } // InboxRoot is the directory new notes default into: the vault root when // InboxDir is empty, an absolute InboxDir as-is, or InboxDir resolved under the // vault when relative. func (c Config) InboxRoot() string { if c.InboxDir == "" { return c.VaultDir } if filepath.IsAbs(c.InboxDir) { return c.InboxDir } return filepath.Join(c.VaultDir, c.InboxDir) } // DailyPath builds the absolute path to the daily note for time t. func (c Config) DailyPath(t time.Time) string { return filepath.Join(c.VaultDir, c.DailySubdir, t.Format(c.DailyFormat)+".md") }