feat: glint vault + working-dir/vault split
b029710a57d5c4429e726e6182e11831ea365914
humdrum <me@humdrum.me> · 2026-06-28 17:20
parent 035eb328
feat: glint vault + working-dir/vault split Decouple the working directory from the vault so glint serves both 'work in the repo I'm in' and 'open my vault from anywhere': - bare glint / glint new / --daily operate on the working dir ($GLINT_VAULT, else cwd); inbox and daily resolve there. - vault_dir (config) is now the vault that the new `glint vault` subcommand opens from anywhere; it no longer roots bare glint. - picker Ctrl+N (new note from query) uses the inbox root, matching save-as and `glint new`. Adds Config.WorkingDir()/Vault(), App.openVault()/StartVault(), the vault subcommand, tests, and README docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7 files changed
README.md +10 −6
@@ -24,8 +24,9 @@
## Usage
```bash
-glint # open the fuzzy picker over your vault
+glint # fuzzy picker over the current directory
glint notes.md # open a file
+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
@@ -51,16 +52,19 @@
glint reads `~/.config/glint/config.toml` (all keys optional):
```toml
-vault_dir = "" # notes root; default: $GLINT_VAULT, else the current directory
-inbox_dir = "" # where `glint new` / save-as land; "" = vault root, relative = under vault
-daily_subdir = "Daily" # daily notes live in <vault>/<daily_subdir>/
+vault_dir = "" # the vault `glint vault` opens from anywhere (e.g. "~/Notes"); unset = current dir
+inbox_dir = "" # where `glint new` / save-as land; "" = working dir, relative = under it
+daily_subdir = "Daily" # daily notes live in <working dir>/<daily_subdir>/
daily_format = "2006-01-02" # Go time layout for daily-note filenames
theme = "auto" # auto | flexoki-light | flexoki-dark | charm (auto detects macOS appearance)
glamour_style = "" # override the preview style; "" follows the theme
```
-The vault resolves as `vault_dir` (config) → `$GLINT_VAULT` → the current working
-directory, so `glint` works in whatever folder you launch it from.
+**Two roots.** Bare `glint`, `glint new`, and `--daily` operate on the **working
+directory** — the folder you launched from (or `$GLINT_VAULT` if set), so glint
+works in whatever repo you're in. **`glint vault`** opens the picker on your
+**configured `vault_dir`** from anywhere — set it to your notes vault to reach it
+without `cd`-ing there.
## Themes
Testing.md +3 −0
@@ -0,0 +1,3 @@
+# Testing to see where this goes
+
+- Does it work as expected?
\ No newline at end of file
internal/app/app.go +20 −3
@@ -381,9 +381,19 @@ a.picker.SetSize(w, h-1) // picker keeps its full-width split
}
}
-// openPicker builds a fresh picker over the vault and switches to it.
+// openPicker builds a fresh picker over the working directory and switches to it.
func (a *App) openPicker() (tea.Model, tea.Cmd) {
- p, err := picker.New(a.cfg.VaultDir, a.theme, a.cfg.DailyPath(time.Now()), a.glamourStyle())
+ return a.openPickerAt(a.cfg.WorkingDir())
+}
+
+// openVault opens the picker over the configured vault, from anywhere.
+func (a *App) openVault() (tea.Model, tea.Cmd) {
+ return a.openPickerAt(a.cfg.Vault())
+}
+
+// openPickerAt opens the picker rooted at dir.
+func (a *App) openPickerAt(dir string) (tea.Model, tea.Cmd) {
+ p, err := picker.New(dir, a.theme, a.cfg.DailyPath(time.Now()), a.glamourStyle())
if err != nil {
a.status = "Picker failed: " + err.Error()
return a, nil
@@ -392,6 +402,13 @@ p.SetSize(a.width, a.height-1)
a.picker = p
a.mode = ModePicker
return a, nil
+}
+
+// StartVault is the entry point for `glint vault`: open the picker over the
+// configured vault directory.
+func (a *App) StartVault() error {
+ _, _ = a.openVault()
+ return nil
}
// openDaily opens today's daily note, creating the file and directory if needed.
@@ -415,7 +432,7 @@ }
// newNote creates a note named after the picker query and opens it.
func (a *App) newNote() (tea.Model, tea.Cmd) {
- p := picker.NewNotePath(a.cfg.VaultDir, a.picker.Query())
+ p := picker.NewNotePath(a.cfg.InboxRoot(), a.picker.Query())
if p == "" {
a.status = "Type a name first"
return a, nil
internal/app/app_test.go +30 −11
@@ -165,7 +165,7 @@ func TestCtrlFOpensPicker(t *testing.T) {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "note.md"), []byte("x"), 0o644)
cfg := config.Default()
- cfg.VaultDir = dir
+ t.Setenv("GLINT_VAULT", dir)
a := New(cfg)
a.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
a.Update(tea.KeyMsg{Type: tea.KeyCtrlF})
@@ -177,7 +177,7 @@
func TestCtrlDCreatesAndOpensDaily(t *testing.T) {
dir := t.TempDir()
cfg := config.Default()
- cfg.VaultDir = dir
+ t.Setenv("GLINT_VAULT", dir)
cfg.DailySubdir = "Daily"
cfg.DailyFormat = "2006-01-02"
a := New(cfg)
@@ -198,7 +198,7 @@ func TestStartInPickerWhenNoPath(t *testing.T) {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "n.md"), []byte("x"), 0o644)
cfg := config.Default()
- cfg.VaultDir = dir
+ t.Setenv("GLINT_VAULT", dir)
a := New(cfg)
if err := a.Start("", false); err != nil {
t.Fatal(err)
@@ -224,7 +224,7 @@
func TestStartDailyCreatesFile(t *testing.T) {
dir := t.TempDir()
cfg := config.Default()
- cfg.VaultDir = dir
+ t.Setenv("GLINT_VAULT", dir)
a := New(cfg)
if err := a.Start("", true); err != nil {
t.Fatal(err)
@@ -241,7 +241,7 @@ func TestCtrlFDirtyNeedsConfirm(t *testing.T) {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "note.md"), []byte("x"), 0o644)
cfg := config.Default()
- cfg.VaultDir = dir
+ t.Setenv("GLINT_VAULT", dir)
a := New(cfg)
a.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
// Make dirty
@@ -265,7 +265,7 @@ func TestCtrlFDirtyDisarmedByOtherKey(t *testing.T) {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "note.md"), []byte("x"), 0o644)
cfg := config.Default()
- cfg.VaultDir = dir
+ t.Setenv("GLINT_VAULT", dir)
a := New(cfg)
a.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
// Make dirty
@@ -284,7 +284,7 @@
func TestCtrlDDirtyNeedsConfirm(t *testing.T) {
dir := t.TempDir()
cfg := config.Default()
- cfg.VaultDir = dir
+ t.Setenv("GLINT_VAULT", dir)
cfg.DailySubdir = "Daily"
cfg.DailyFormat = "2006-01-02"
a := New(cfg)
@@ -333,7 +333,7 @@
func TestCtrlNCreatesNoteFromQuery(t *testing.T) {
dir := t.TempDir()
cfg := config.Default()
- cfg.VaultDir = dir
+ t.Setenv("GLINT_VAULT", dir)
a := New(cfg)
a.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
a.Update(tea.KeyMsg{Type: tea.KeyCtrlF}) // open picker (Ctrl+F)
@@ -417,7 +417,7 @@
func TestStartNewWithNameLandsInInbox(t *testing.T) {
dir := t.TempDir()
cfg := config.Default()
- cfg.VaultDir = dir
+ t.Setenv("GLINT_VAULT", dir)
cfg.InboxDir = "Inbox"
a := New(cfg)
if err := a.StartNew("foo"); err != nil {
@@ -435,7 +435,7 @@
func TestSaveAsPromptOnPathlessBuffer(t *testing.T) {
dir := t.TempDir()
cfg := config.Default()
- cfg.VaultDir = dir
+ t.Setenv("GLINT_VAULT", dir)
cfg.InboxDir = "" // vault root
a := New(cfg)
a.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
@@ -473,7 +473,7 @@ dir := t.TempDir()
src := filepath.Join(dir, "a.md")
os.WriteFile(src, []byte("body"), 0o644)
cfg := config.Default()
- cfg.VaultDir = dir
+ t.Setenv("GLINT_VAULT", dir)
a := New(cfg)
a.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
a.Load(src)
@@ -511,3 +511,22 @@ if got := a.preview.Style(); got != "dark" {
t.Errorf("preview style after cycle = %q, want dark (re-rendered)", got)
}
}
+
+func TestStartVaultOpensVaultPicker(t *testing.T) {
+ vault := t.TempDir()
+ os.WriteFile(filepath.Join(vault, "vaultnote.md"), []byte("x"), 0o644)
+ t.Setenv("GLINT_VAULT", t.TempDir()) // working dir is somewhere else
+ cfg := config.Default()
+ cfg.VaultDir = vault
+ a := New(cfg)
+ a.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
+ if err := a.StartVault(); err != nil {
+ t.Fatal(err)
+ }
+ if a.mode != ModePicker {
+ t.Fatalf("mode = %d, want ModePicker", a.mode)
+ }
+ if sel := a.picker.Selected(); !strings.HasPrefix(sel, vault) {
+ t.Errorf("vault picker selected %q, want a file under the vault %q", sel, vault)
+ }
+}
internal/config/config.go +31 −17
@@ -13,7 +13,7 @@ )
// Config holds the user-tunable settings glint reads at startup.
type Config struct {
- VaultDir string `toml:"vault_dir"`
+ VaultDir string `toml:"vault_dir"` // the vault `glint vault` opens from anywhere
DailySubdir string `toml:"daily_subdir"`
DailyFormat string `toml:"daily_format"`
GlamourStyle string `toml:"glamour_style"`
@@ -21,24 +21,38 @@ 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.
+// Default returns the built-in configuration used when no file is present.
+// VaultDir is empty by default: bare glint works in the current directory; set
+// vault_dir (config) to give `glint vault` a fixed vault to open from anywhere.
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",
}
}
+// WorkingDir is where bare `glint`, `glint new`, and `--daily` operate: the
+// $GLINT_VAULT pin if set, otherwise the current working directory.
+func (c Config) WorkingDir() string {
+ if v := os.Getenv("GLINT_VAULT"); v != "" {
+ return v
+ }
+ if wd, err := os.Getwd(); err == nil {
+ return wd
+ }
+ return "."
+}
+
+// Vault is the directory `glint vault` opens from anywhere: the configured
+// vault_dir, or the working directory when none is set.
+func (c Config) Vault() string {
+ if c.VaultDir != "" {
+ return c.VaultDir
+ }
+ return c.WorkingDir()
+}
+
// Load reads ~/.config/glint/config.toml and overlays it onto the defaults.
func Load() (Config, error) {
home, err := os.UserHomeDir()
@@ -84,20 +98,20 @@ }
return cfg, nil
}
-// InboxRoot is the directory new notes default into: the vault root when
+// InboxRoot is the directory new notes default into: the working directory when
// InboxDir is empty, an absolute InboxDir as-is, or InboxDir resolved under the
-// vault when relative.
+// working directory when relative.
func (c Config) InboxRoot() string {
if c.InboxDir == "" {
- return c.VaultDir
+ return c.WorkingDir()
}
if filepath.IsAbs(c.InboxDir) {
return c.InboxDir
}
- return filepath.Join(c.VaultDir, c.InboxDir)
+ return filepath.Join(c.WorkingDir(), c.InboxDir)
}
-// DailyPath builds the absolute path to the daily note for time t.
+// DailyPath builds the path to the daily note for time t, under the working dir.
func (c Config) DailyPath(t time.Time) string {
- return filepath.Join(c.VaultDir, c.DailySubdir, t.Format(c.DailyFormat)+".md")
+ return filepath.Join(c.WorkingDir(), c.DailySubdir, t.Format(c.DailyFormat)+".md")
}
internal/config/config_test.go +26 −11
@@ -21,8 +21,11 @@ }
if d.Theme != "auto" {
t.Errorf("Theme = %q, want auto", d.Theme)
}
- if d.VaultDir == "" {
- t.Error("VaultDir should not be empty")
+ if d.VaultDir != "" {
+ t.Errorf("VaultDir default = %q, want empty (set vault_dir for `glint vault`)", d.VaultDir)
+ }
+ if d.WorkingDir() == "" {
+ t.Error("WorkingDir should not be empty")
}
}
@@ -92,7 +95,8 @@ }
}
func TestDailyPath(t *testing.T) {
- c := Config{VaultDir: "/v", DailySubdir: "Daily", DailyFormat: "2006-01-02"}
+ t.Setenv("GLINT_VAULT", "/v") // pins the working dir
+ c := Config{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 {
@@ -119,9 +123,10 @@ }
}
func TestInboxRootResolves(t *testing.T) {
- c := Config{VaultDir: "/v"}
+ t.Setenv("GLINT_VAULT", "/v") // pins the working dir
+ c := Config{}
if got := c.InboxRoot(); got != "/v" {
- t.Errorf("empty InboxDir → %q, want /v (vault root)", got)
+ t.Errorf("empty InboxDir → %q, want /v (working dir)", got)
}
c.InboxDir = "Inbox"
if got := c.InboxRoot(); got != filepath.Join("/v", "Inbox") {
@@ -133,14 +138,24 @@ t.Errorf("absolute InboxDir → %q, want /abs/inbox", got)
}
}
-func TestDefaultVaultUsesEnvThenCwd(t *testing.T) {
- t.Setenv("GLINT_VAULT", "/explicit/vault")
- if d := Default(); d.VaultDir != "/explicit/vault" {
- t.Errorf("with GLINT_VAULT set: VaultDir = %q, want /explicit/vault", d.VaultDir)
+func TestWorkingDirUsesEnvThenCwd(t *testing.T) {
+ t.Setenv("GLINT_VAULT", "/explicit/dir")
+ if got := (Config{}).WorkingDir(); got != "/explicit/dir" {
+ t.Errorf("with GLINT_VAULT set: WorkingDir = %q, want /explicit/dir", got)
}
t.Setenv("GLINT_VAULT", "")
wd, _ := os.Getwd()
- if d := Default(); d.VaultDir != wd {
- t.Errorf("without GLINT_VAULT: VaultDir = %q, want cwd %q", d.VaultDir, wd)
+ if got := (Config{}).WorkingDir(); got != wd {
+ t.Errorf("without GLINT_VAULT: WorkingDir = %q, want cwd %q", got, wd)
+ }
+}
+
+func TestVaultPrefersConfigThenWorkingDir(t *testing.T) {
+ t.Setenv("GLINT_VAULT", "/work")
+ if got := (Config{VaultDir: "/the/vault"}).Vault(); got != "/the/vault" {
+ t.Errorf("with vault_dir set: Vault = %q, want /the/vault", got)
+ }
+ if got := (Config{}).Vault(); got != "/work" {
+ t.Errorf("without vault_dir: Vault = %q, want working dir /work", got)
}
}
main.go +21 −12
@@ -28,19 +28,28 @@ fmt.Fprintln(os.Stderr, "glint: config:", err)
}
a := app.New(cfg)
- // `glint new [name]` — start a fresh document: a blank unnamed buffer, or a
- // new note created under the inbox when a name is given.
- if args := os.Args[1:]; len(args) > 0 && args[0] == "new" {
- name := ""
- if len(args) > 1 {
- name = args[1]
- }
- if err := a.StartNew(name); err != nil {
- fmt.Fprintln(os.Stderr, "glint:", err)
- os.Exit(1)
+ // Subcommands run before flag parsing.
+ if args := os.Args[1:]; len(args) > 0 {
+ switch args[0] {
+ case "new": // `glint new [name]` — blank buffer, or a named note under the inbox
+ name := ""
+ if len(args) > 1 {
+ name = args[1]
+ }
+ if err := a.StartNew(name); err != nil {
+ fmt.Fprintln(os.Stderr, "glint:", err)
+ os.Exit(1)
+ }
+ run(a)
+ return
+ case "vault": // `glint vault` — open the picker over the configured vault, from anywhere
+ if err := a.StartVault(); err != nil {
+ fmt.Fprintln(os.Stderr, "glint:", err)
+ os.Exit(1)
+ }
+ run(a)
+ return
}
- run(a)
- return
}
daily := flag.Bool("daily", false, "open today's daily note")