From 10c236248270682a38c47b4e9b2d8a5499aff20d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Einar=20Bergstr=C3=B8m?= Date: Sun, 11 Oct 2026 00:25:01 +0200 Subject: [PATCH] feat: start sops edit on the key's line Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/repos.md | 4 +++- internal/ui/app/edit.go | 31 +++++++++++++++++++++++++++---- internal/ui/app/repo_test.go | 19 +++++++++++++++++++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/docs/repos.md b/docs/repos.md index bc2ccdf..47c837b 100644 --- a/docs/repos.md +++ b/docs/repos.md @@ -77,7 +77,9 @@ KMS, PGP). When sops can't decrypt, its whole error message is shown. without either, in the system's default app for the file. Editors that take a line (vim, nvim, nano, emacs, micro, helix, VS Code, -Zed, Sublime and others) start on the key. +Zed, Sublime and others) start on the key. With `sops edit` that works for +editors taking `+N` (vim, nvim, nano, emacs, micro and others), and needs +sops 3.8 or later. ## Links diff --git a/internal/ui/app/edit.go b/internal/ui/app/edit.go index a89772e..add1444 100644 --- a/internal/ui/app/edit.go +++ b/internal/ui/app/edit.go @@ -1,6 +1,7 @@ package app import ( + "cmp" "errors" "os" "os/exec" @@ -71,6 +72,12 @@ func (m *Model) edit() tea.Cmd { if f.Sops != "" { cmd := exec.Command(f.Sops, "edit", f.Path) cmd.Dir = filepath.Dir(f.Path) // sops looks for .sops.yaml from here + // sops starts SOPS_EDITOR (else EDITOR, else vim) on the decrypted + // copy, which keeps the file's lines: start it on the key. + editor := cmp.Or(m.getenv("SOPS_EDITOR"), m.getenv("EDITOR"), "vim") + if at, ok := editorAtLine(editor, f.Line); ok { + cmd.Env = append(os.Environ(), "SOPS_EDITOR="+at) + } m.flash = flash{} return tea.ExecProcess(cmd, finished) } @@ -100,6 +107,22 @@ func (m *Model) edit() tea.Cmd { return nil } +// 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, + "micro": true, "kak": true, "mg": true, "ne": true, "joe": true, "vis": true, +} + +// editorAtLine is editor starting at line, for editors that take +N: +// sops adds the file after it. +func editorAtLine(editor string, line int) (string, bool) { + fields := strings.Fields(editor) + if line == 0 || len(fields) == 0 || !plusLine[filepath.Base(fields[0])] { + return "", false + } + return editor + " +" + strconv.Itoa(line), true +} + // editorArgs opens f at its line, for editors known to take one. func editorArgs(editor string, f provider.File) []string { fields := strings.Fields(editor) @@ -107,12 +130,12 @@ func editorArgs(editor string, f provider.File) []string { return []string{f.Path} } at := f.Path + ":" + strconv.Itoa(f.Line) - switch filepath.Base(fields[0]) { - case "vi", "vim", "nvim", "nano", "emacs", "emacsclient", "micro", "kak", "mg", "ne", "joe", "vis": + switch base := filepath.Base(fields[0]); { + case plusLine[base]: return []string{"+" + strconv.Itoa(f.Line), f.Path} - case "hx", "helix", "subl", "zed": + case base == "hx" || base == "helix" || base == "subl" || base == "zed": return []string{at} - case "code", "codium", "cursor", "windsurf": + case base == "code" || base == "codium" || base == "cursor" || base == "windsurf": return []string{"-g", at} } return []string{f.Path} diff --git a/internal/ui/app/repo_test.go b/internal/ui/app/repo_test.go index d7ca1a7..115b81b 100644 --- a/internal/ui/app/repo_test.go +++ b/internal/ui/app/repo_test.go @@ -323,3 +323,22 @@ func TestEditorArgs(t *testing.T) { t.Errorf("no line: %q", got) } } + +func TestEditorAtLine(t *testing.T) { + t.Parallel() + for _, c := range []struct { + editor string + line int + want string + ok bool + }{ + {"nvim", 4, "nvim +4", true}, + {"/usr/bin/vim -p", 4, "/usr/bin/vim -p +4", true}, + {"code --wait", 4, "", false}, // sops adds the file last: no -g file:N + {"nvim", 0, "", false}, + } { + if got, ok := editorAtLine(c.editor, c.line); got != c.want || ok != c.ok { + t.Errorf("%q at %d: %q %v", c.editor, c.line, got, ok) + } + } +}