From 765f9f6aeab2d7d36b8002370df4894312874ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Einar=20Bergstr=C3=B8m?= Date: Tue, 13 Oct 2026 12:55:03 +0200 Subject: [PATCH] feat: E edits the config file E opens your config like e opens a file: in the nvim lazysecrets runs in, else in $VISUAL or $EDITOR, else in the system's default app. A missing config is made first, with a link to its docs. Closing the editor says to restart lazysecrets to use the changes. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/config.md | 4 +++- docs/keys.md | 1 + internal/ui/app/app.go | 2 ++ internal/ui/app/edit.go | 40 +++++++++++++++++++++++++++++++++--- internal/ui/app/repo_test.go | 40 ++++++++++++++++++++++++++++++++++++ internal/ui/keys/keys.go | 5 +++-- 6 files changed, 86 insertions(+), 6 deletions(-) diff --git a/docs/config.md b/docs/config.md index f7a04e6..97caa96 100644 --- a/docs/config.md +++ b/docs/config.md @@ -2,7 +2,9 @@ Optional, at `~/.config/lazysecrets/config.yaml` (or `$XDG_CONFIG_HOME/lazysecrets/config.yaml`, or `--config`). Flags override -it. Unknown keys are an error, so typos don't go unnoticed. +it. Unknown keys are an error, so typos don't go unnoticed. `E` in +lazysecrets opens it in your editor (making it if it isn't there yet); +changes apply when lazysecrets starts again. ```yaml icons: true # Nerd Font icons (v3+); also --icons=false or LAZYSECRETS_ICONS=0 diff --git a/docs/keys.md b/docs/keys.md index b4c1527..b69d1b0 100644 --- a/docs/keys.md +++ b/docs/keys.md @@ -59,6 +59,7 @@ match case exactly. | `x` | [custom commands](custom-commands.md) | | `L` | sign in again (shown when a login has expired) | | `U` | a newer lazysecrets: its changelog, and update in place (restart to use it); or look for one | +| `E` | edit your [config](config.md), like `e` opens a file (made if it isn't there yet; restart to use it) | A revealed value is shown as it reads best: JSON indented, and PEM certificates as their subject, issuer, dates, names and thumbprint, with diff --git a/internal/ui/app/app.go b/internal/ui/app/app.go index 9deebef..d176a53 100644 --- a/internal/ui/app/app.go +++ b/internal/ui/app/app.go @@ -965,6 +965,8 @@ func (m *Model) handleKey(msg tea.KeyPressMsg) (cmd tea.Cmd, quit bool) { return m.refresh(), false case key.Matches(msg, k.Update): return m.update(), false + case key.Matches(msg, k.EditConfig): + return m.editConfig(), false } return nil, false } diff --git a/internal/ui/app/edit.go b/internal/ui/app/edit.go index 420b38c..dc95941 100644 --- a/internal/ui/app/edit.go +++ b/internal/ui/app/edit.go @@ -3,6 +3,7 @@ package app import ( "cmp" "errors" + "io/fs" "os" "os/exec" "path/filepath" @@ -13,6 +14,7 @@ import ( "git.kebcloud.no/keb/lazysecrets/internal/browser" "git.kebcloud.no/keb/lazysecrets/internal/provider" + "git.kebcloud.no/keb/lazysecrets/internal/update" ) // Opening a repo's file (e): SOPS files with sops edit, which decrypts @@ -22,9 +24,10 @@ import ( // secret, editors that can open at a line start on its key. type editDoneMsg struct { - c provider.Container - name string - err error + c provider.Container + name string + config bool // the config file: read at startup + err error } // sopsUnchanged is sops edit's exit code when the file wasn't changed. @@ -93,6 +96,33 @@ func (m *Model) edit() tea.Cmd { return m.openInEditor(f, finished) } +// configHeader starts a config file made to be edited. +const configHeader = "# lazysecrets config: " + update.DefaultRepo + "/src/branch/main/docs/config.md\n" + +// editConfig opens the user's config file (the first of them), as e +// opens a file, making it if it isn't there yet. +func (m *Model) editConfig() tea.Cmd { + if len(m.configFiles) == 0 { + m.flash = warning("no config file") + return nil + } + path := m.configFiles[0] + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { + err := os.MkdirAll(filepath.Dir(path), 0o755) + if err == nil { + err = os.WriteFile(path, []byte(configHeader), 0o644) + } + if err != nil { + m.flash = flash{text: "edit config: " + err.Error(), isErr: true} + return nil + } + } + name := filepath.Base(path) + return m.openInEditor(provider.File{Path: path}, func(err error) tea.Msg { + return editDoneMsg{name: name, config: true, err: err} + }) +} + // plusLine are editors that open at a line with +N before the file. var plusLine = map[string]bool{ "vi": true, "vim": true, "nvim": true, "nano": true, "emacs": true, "emacsclient": true, @@ -142,6 +172,10 @@ func (m *Model) handleEditDone(msg editDoneMsg) tea.Cmd { m.flash = flash{text: "open " + msg.name + ": " + msg.err.Error(), isErr: true} return nil } + if msg.config { + m.flash = done("closed " + msg.name + ": restart lazysecrets to use it") + return nil + } m.flash = done("closed " + msg.name) if c, ok := m.containers.Selected(); ok && containerKey(c) == containerKey(msg.c) { m.hideValue() diff --git a/internal/ui/app/repo_test.go b/internal/ui/app/repo_test.go index 8222d9a..4ecc742 100644 --- a/internal/ui/app/repo_test.go +++ b/internal/ui/app/repo_test.go @@ -342,3 +342,43 @@ func TestEditorAtLine(t *testing.T) { } } } + +func TestEditConfig(t *testing.T) { + t.Parallel() + path := filepath.Join(t.TempDir(), "lazysecrets", "config.yaml") + var opened []string + d := newDriverOpts(t, Options{ConfigFiles: []string{path}, OpenURL: func(u string) error { opened = append(opened, u); return nil }}) + d.m.getenv = func(string) string { return "" } + + // No editor: the system's default app, on a config made for it. + d.keys("E") + if !slices.Equal(opened, []string{path}) { + t.Errorf("opened %q", opened) + } + if b, err := os.ReadFile(path); err != nil || !strings.HasPrefix(string(b), "# lazysecrets config") { + t.Errorf("config made: %q, %v", b, err) + } + + // Inside nvim: in that nvim; an existing config is left as it is. + os.WriteFile(path, []byte("icons: false\n"), 0o644) + d.m.getenv = func(k string) string { return map[string]string{"NVIM": "/tmp/nvim.sock"}[k] } + var nvim []string + d.m.nvimOpen = func(server, path string, line int) error { + nvim = append(nvim, server+" "+path) + return nil + } + if msg := d.m.editConfig()(); msg != (tea.QuitMsg{}) { + t.Errorf("didn't quit for nvim: %v", msg) + } + if want := []string{"/tmp/nvim.sock " + path}; !slices.Equal(nvim, want) { + t.Errorf("nvim got %q", nvim) + } + if b, _ := os.ReadFile(path); string(b) != "icons: false\n" { + t.Errorf("config changed: %q", b) + } + + d.send(editDoneMsg{name: "config.yaml", config: true}) + if !strings.Contains(d.m.flash.text, "restart") { + t.Errorf("flash %q", d.m.flash.text) + } +} diff --git a/internal/ui/keys/keys.go b/internal/ui/keys/keys.go index c9c3a0d..21a392d 100644 --- a/internal/ui/keys/keys.go +++ b/internal/ui/keys/keys.go @@ -48,7 +48,7 @@ type Map struct { SplitLeft, SplitRight, SplitReset key.Binding Taller, Shorter, ZoomIn, ZoomOut key.Binding Login, Refresh, Help, Quit key.Binding - Update key.Binding + Update, EditConfig key.Binding // filter AcceptFilter, ClearFilter key.Binding FilterUp, FilterDown key.Binding @@ -131,6 +131,7 @@ func (m *Map) Actions() []Action { a(Lists, "refresh", &m.Refresh, "refresh", "other", "Reload the focused pane, bypassing the cache."), a(Lists, "login", &m.Login, "sign in", "other", "Sign in again (az login or databricks auth login) when a login has expired."), a(Lists, "update", &m.Update, "update", "other", "Check for a newer lazysecrets: its changelog, and update to it."), + a(Lists, "editConfig", &m.EditConfig, "edit config", "other", "Open your config file like e opens a file: in the nvim lazysecrets runs in, else in $VISUAL or $EDITOR, else in the system's default app. Made if it doesn't exist yet. Changes apply when lazysecrets starts again."), a(Lists, "help", &m.Help, "help", "other", "Show this list. Also works in Go to while browsing, and in menus, showing their keys first."), a(Lists, "quit", &m.Quit, "quit", "other", "Quit lazysecrets. ctrl+c always quits too."), @@ -194,7 +195,7 @@ var defaults = map[string][]string{ "lists.dividerLeft": {"ctrl+left", "<"}, "lists.dividerRight": {"ctrl+right", ">"}, "lists.taller": {"ctrl+down", "}"}, "lists.shorter": {"ctrl+up", "{"}, "lists.zoomIn": {"+"}, "lists.zoomOut": {"_"}, "lists.resetLayout": {"="}, - "lists.refresh": {"r"}, "lists.login": {"L"}, "lists.update": {"U"}, "lists.help": {"?"}, "lists.quit": {"q"}, + "lists.refresh": {"r"}, "lists.login": {"L"}, "lists.update": {"U"}, "lists.editConfig": {"E"}, "lists.help": {"?"}, "lists.quit": {"q"}, "filter.down": {"ctrl+j", "ctrl+n", "down"}, "filter.up": {"ctrl+k", "ctrl+p", "up"}, "filter.copyValue": {"ctrl+y"}, "filter.reveal": {"ctrl+r"}, "filter.star": {"ctrl+f"},