feat: start sops edit on the key's line
CI / Checks (pull_request) Successful in 25s
PR title / Conventional title (pull_request) Successful in 1s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kebandClaude Opus 5.5 committed 2026-10-11 00:25:01 +02:00
1 parent c29319b821
commit 10c2362482
3 files changed
+49 -5

No files matched your search

+3 -1
View File
@@ -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
+27 -4
View File
@@ -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}
+19
View File
@@ -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)
}
}
}