▍ humdrum codex / glint v1.0.2

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)
+	}
+}