feat: project scaffold + config package
57daa7f26d079219680447ba9544c992f6199cf9
humdrum-tiv <45084903+humdrum-tiv@users.noreply.github.com> · 2026-06-27 21:07
parent e6baa683
4 files changed
go.mod +5 −0
@@ -0,0 +1,5 @@
+module glint
+
+go 1.26.4
+
+require github.com/BurntSushi/toml v1.6.0
go.sum +2 −0
@@ -0,0 +1,2 @@
+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
internal/config/config.go +75 −0
@@ -0,0 +1,75 @@
+// 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"`
+}
+
+// Default returns the built-in configuration used when no file is present.
+func Default() Config {
+ home, _ := os.UserHomeDir()
+ return Config{
+ VaultDir: filepath.Join(home, "Humdrum"),
+ DailySubdir: "Daily",
+ DailyFormat: "2006-01-02",
+ GlamourStyle: "dark",
+ }
+}
+
+// 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
+ }
+ return cfg, nil
+}
+
+// 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")
+}
internal/config/config_test.go +83 −0
@@ -0,0 +1,83 @@
+package config
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+func TestDefaultHasSaneValues(t *testing.T) {
+ d := Default()
+ if d.DailySubdir != "Daily" {
+ t.Errorf("DailySubdir = %q, want Daily", d.DailySubdir)
+ }
+ if d.DailyFormat != "2006-01-02" {
+ t.Errorf("DailyFormat = %q, want 2006-01-02", d.DailyFormat)
+ }
+ if d.GlamourStyle != "dark" {
+ t.Errorf("GlamourStyle = %q, want dark", d.GlamourStyle)
+ }
+ if d.VaultDir == "" {
+ t.Error("VaultDir should not be empty")
+ }
+}
+
+func TestLoadFromFileMissingReturnsDefaults(t *testing.T) {
+ cfg, err := loadFromFile(filepath.Join(t.TempDir(), "nope.toml"))
+ if err != nil {
+ t.Fatalf("missing file should not error, got %v", err)
+ }
+ if cfg.DailyFormat != "2006-01-02" {
+ t.Errorf("DailyFormat = %q, want default", cfg.DailyFormat)
+ }
+}
+
+func TestLoadFromFileOverlays(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "config.toml")
+ body := `vault_dir = "/tmp/vault"
+daily_subdir = "journal"
+daily_format = "20060102"
+`
+ if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ cfg, err := loadFromFile(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if cfg.VaultDir != "/tmp/vault" {
+ t.Errorf("VaultDir = %q", cfg.VaultDir)
+ }
+ if cfg.DailySubdir != "journal" {
+ t.Errorf("DailySubdir = %q", cfg.DailySubdir)
+ }
+ if cfg.DailyFormat != "20060102" {
+ t.Errorf("DailyFormat = %q", cfg.DailyFormat)
+ }
+ // unset key keeps default
+ if cfg.GlamourStyle != "dark" {
+ t.Errorf("GlamourStyle = %q, want default dark", cfg.GlamourStyle)
+ }
+}
+
+func TestLoadFromFileMalformedErrors(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "bad.toml")
+ if err := os.WriteFile(path, []byte("vault_dir = = ="), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := loadFromFile(path); err == nil {
+ t.Error("malformed config should return an error")
+ }
+}
+
+func TestDailyPath(t *testing.T) {
+ c := Config{VaultDir: "/v", DailySubdir: "Daily", DailyFormat: "2006-01-02"}
+ got := c.DailyPath(time.Date(2026, 6, 27, 0, 0, 0, 0, time.UTC))
+ want := filepath.Join("/v", "Daily", "2026-06-27.md")
+ if got != want {
+ t.Errorf("DailyPath = %q, want %q", got, want)
+ }
+}