feat: glint config — interactive setup walkthrough
90e7280d09ee129804f59364209b54d23968deed
humdrum <me@humdrum.me> · 2026-06-28 21:17
parent e6085a1f
feat: glint config — interactive setup walkthrough Add a huh-based 'glint config' subcommand that loads the current config, walks through every setting (theme, vault_dir, inbox_dir, daily subfolder, daily filename format via friendly presets, preview style), and writes ~/.config/glint/config.toml — no hand-editing TOML and no raw Go time layouts. Adds config.Path()/config.Save() (round-trip tested) and the huh dependency.
7 files changed
README.md +4 −1
@@ -30,6 +30,7 @@ glint vault # fuzzy picker over your configured vault, from anywhere
glint --daily # open (and create) today's daily note
glint new # start a blank scratch document
glint new ideas # create and open <inbox>/ideas.md
+glint config # interactive setup walkthrough (writes the config file)
glint --version # print version
```
@@ -51,7 +52,9 @@ | `Esc` | back to the editor |
## Configuration
-glint reads `~/.config/glint/config.toml` (all keys optional):
+Run `glint config` for an interactive walkthrough that writes the file for you
+(no hand-editing, daily-format presets instead of raw Go layouts). The settings
+live in `~/.config/glint/config.toml` (all keys optional):
```toml
vault_dir = "" # the vault `glint vault` opens from anywhere (e.g. "~/Notes"); unset = current dir
go.mod +5 −0
@@ -7,6 +7,7 @@ github.com/BurntSushi/toml v1.6.0
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/glamour v1.0.0
+ github.com/charmbracelet/huh v1.0.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
)
@@ -15,15 +16,18 @@ github.com/alecthomas/chroma/v2 v2.20.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
+ github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
+ github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
@@ -31,6 +35,7 @@ github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
+ github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
go.sum +22 −0
@@ -1,5 +1,7 @@
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
+github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
@@ -14,6 +16,8 @@ github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
+github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
@@ -22,26 +26,42 @@ github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08=
github.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo=
+github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw=
+github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
+github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
+github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
+github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
+github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
+github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
+github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
+github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
+github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
+github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
+github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
+github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
+github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
@@ -59,6 +79,8 @@ github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
+github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
+github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
internal/config/config.go +25 −4
@@ -53,13 +53,34 @@ }
return c.WorkingDir()
}
-// Load reads ~/.config/glint/config.toml and overlays it onto the defaults.
+// Path is the config file location: ~/.config/glint/config.toml.
+func Path() string {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return filepath.Join(".config", "glint", "config.toml")
+ }
+ return filepath.Join(home, ".config", "glint", "config.toml")
+}
+
+// Load reads the config file and overlays it onto the defaults.
func Load() (Config, error) {
- home, err := os.UserHomeDir()
+ return loadFromFile(Path())
+}
+
+// Save writes cfg to path as TOML, creating the parent directory.
+func Save(cfg Config, path string) error {
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ return err
+ }
+ f, err := os.Create(path)
if err != nil {
- return Default(), nil
+ return err
}
- return loadFromFile(filepath.Join(home, ".config", "glint", "config.toml"))
+ if err := toml.NewEncoder(f).Encode(cfg); err != nil {
+ _ = f.Close()
+ return err
+ }
+ return f.Close()
}
// loadFromFile is the testable core of Load. A missing file yields the
internal/config/config_test.go +27 −0
@@ -159,3 +159,30 @@ if got := (Config{}).Vault(); got != "/work" {
t.Errorf("without vault_dir: Vault = %q, want working dir /work", got)
}
}
+
+func TestPathEndsAtGlintConfig(t *testing.T) {
+ p := Path()
+ if filepath.Base(p) != "config.toml" || filepath.Base(filepath.Dir(p)) != "glint" {
+ t.Errorf("Path() = %q, want .../glint/config.toml", p)
+ }
+}
+
+func TestSaveRoundTrips(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "sub", "config.toml") // parent created by Save
+ cfg := Default()
+ cfg.VaultDir = "/my/vault"
+ cfg.InboxDir = "Inbox"
+ cfg.DailyFormat = "20060102"
+ cfg.Theme = "charm"
+ if err := Save(cfg, path); err != nil {
+ t.Fatal(err)
+ }
+ got, err := loadFromFile(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got.VaultDir != "/my/vault" || got.InboxDir != "Inbox" || got.DailyFormat != "20060102" || got.Theme != "charm" {
+ t.Errorf("round-trip mismatch: %+v", got)
+ }
+}
internal/configui/configui.go +128 −0
@@ -0,0 +1,128 @@
+// Package configui is the interactive `glint config` walkthrough: a huh form
+// that loads the current config, walks the user through every setting, and
+// writes the result to the config file.
+package configui
+
+import (
+ "fmt"
+
+ "glint/internal/config"
+
+ "github.com/charmbracelet/huh"
+)
+
+// dailyPresets are friendly daily-note filename formats (label → Go layout), so
+// users never have to write a raw Go time layout by hand.
+var dailyPresets = []struct{ label, layout string }{
+ {"2026-06-28", "2006-01-02"},
+ {"2026-06-28 Sun", "2006-01-02 Mon"},
+ {"20260628", "20060102"},
+ {"2026.06.28", "2006.01.02"},
+ {"28-06-2026", "02-01-2006"},
+}
+
+const customLayout = "__custom__"
+
+// Run walks the user through the settings and saves them to the config file.
+func Run() error {
+ cfg, _ := config.Load() // defaults on error
+
+ theme := orDefault(cfg.Theme, "auto")
+ vaultDir := cfg.VaultDir
+ inboxDir := cfg.InboxDir
+ dailySubdir := orDefault(cfg.DailySubdir, "Daily")
+ glamour := cfg.GlamourStyle
+
+ // Match the current layout to a preset; otherwise offer it as custom.
+ fmtChoice := customLayout
+ for _, p := range dailyPresets {
+ if p.layout == cfg.DailyFormat {
+ fmtChoice = p.layout
+ }
+ }
+ customFmt := cfg.DailyFormat
+
+ fmtOpts := make([]huh.Option[string], 0, len(dailyPresets)+1)
+ for _, p := range dailyPresets {
+ fmtOpts = append(fmtOpts, huh.NewOption(p.label+" ("+p.layout+")", p.layout))
+ }
+ fmtOpts = append(fmtOpts, huh.NewOption("Custom…", customLayout))
+
+ form := huh.NewForm(
+ huh.NewGroup(
+ huh.NewSelect[string]().
+ Title("Theme").
+ Description("Colors; auto follows your macOS appearance.").
+ Options(
+ huh.NewOption("auto", "auto"),
+ huh.NewOption("flexoki-light", "flexoki-light"),
+ huh.NewOption("flexoki-dark", "flexoki-dark"),
+ huh.NewOption("charm", "charm"),
+ ).Value(&theme),
+ huh.NewInput().
+ Title("Vault directory").
+ Description("Where `glint vault` opens, from anywhere. Blank = none.").
+ Placeholder("/Users/you/Notes").
+ Value(&vaultDir),
+ huh.NewInput().
+ Title("Inbox directory").
+ Description("Where `glint new` and save-as land. Blank = the directory you run glint in.").
+ Value(&inboxDir),
+ huh.NewInput().
+ Title("Daily notes subfolder").
+ Description("Daily notes live in <dir>/<this>/.").
+ Value(&dailySubdir),
+ huh.NewSelect[string]().
+ Title("Daily filename format").
+ Options(fmtOpts...).
+ Value(&fmtChoice),
+ huh.NewSelect[string]().
+ Title("Preview style").
+ Description("Glamour style for the read preview. Blank follows the theme.").
+ Options(
+ huh.NewOption("follow theme", ""),
+ huh.NewOption("dark", "dark"),
+ huh.NewOption("light", "light"),
+ huh.NewOption("dracula", "dracula"),
+ huh.NewOption("tokyo-night", "tokyo-night"),
+ ).Value(&glamour),
+ ),
+ huh.NewGroup(
+ huh.NewInput().
+ Title("Custom daily format (Go layout)").
+ Description("Reference date is Mon Jan 2 2006 — e.g. 2006-01-02 or 20060102.").
+ Value(&customFmt),
+ ).WithHideFunc(func() bool { return fmtChoice != customLayout }),
+ )
+
+ if err := form.Run(); err != nil {
+ return err
+ }
+
+ dailyFmt := fmtChoice
+ if fmtChoice == customLayout {
+ dailyFmt = customFmt
+ }
+
+ out := config.Config{
+ Theme: theme,
+ VaultDir: vaultDir,
+ InboxDir: inboxDir,
+ DailySubdir: dailySubdir,
+ DailyFormat: dailyFmt,
+ GlamourStyle: glamour,
+ }
+ path := config.Path()
+ if err := config.Save(out, path); err != nil {
+ return err
+ }
+ fmt.Println("Saved", path)
+ return nil
+}
+
+func orDefault(v, def string) string {
+ if v == "" {
+ return def
+ }
+ return v
+}
main.go +7 −0
@@ -8,6 +8,7 @@ "os"
"glint/internal/app"
"glint/internal/config"
+ "glint/internal/configui"
tea "github.com/charmbracelet/bubbletea"
)
@@ -48,6 +49,12 @@ fmt.Fprintln(os.Stderr, "glint:", err)
os.Exit(1)
}
run(a)
+ return
+ case "config": // `glint config` — interactive setup walkthrough
+ if err := configui.Run(); err != nil {
+ fmt.Fprintln(os.Stderr, "glint:", err)
+ os.Exit(1)
+ }
return
}
}