feat: e opens a repo's SOPS or .env file, in nvim, sops edit or your editor #1

Merged
keb merged 3 commits from feat/edit-files into main 2026-10-11 09:34:05 +02:00
3 changed files with 49 additions and 5 deletions
Showing only changes of commit 10c2362482 - Show all commits

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. without either, in the system's default app for the file.
Editors that take a line (vim, nvim, nano, emacs, micro, helix, VS Code, 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 ## Links
+27 -4
View File
@@ -1,6 +1,7 @@
package app package app
import ( import (
"cmp"
"errors" "errors"
"os" "os"
"os/exec" "os/exec"
@@ -71,6 +72,12 @@ func (m *Model) edit() tea.Cmd {
if f.Sops != "" { if f.Sops != "" {
cmd := exec.Command(f.Sops, "edit", f.Path) cmd := exec.Command(f.Sops, "edit", f.Path)
cmd.Dir = filepath.Dir(f.Path) // sops looks for .sops.yaml from here 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{} m.flash = flash{}
return tea.ExecProcess(cmd, finished) return tea.ExecProcess(cmd, finished)
} }
@@ -100,6 +107,22 @@ func (m *Model) edit() tea.Cmd {
return nil 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. // editorArgs opens f at its line, for editors known to take one.
func editorArgs(editor string, f provider.File) []string { func editorArgs(editor string, f provider.File) []string {
fields := strings.Fields(editor) fields := strings.Fields(editor)
@@ -107,12 +130,12 @@ func editorArgs(editor string, f provider.File) []string {
return []string{f.Path} return []string{f.Path}
} }
at := f.Path + ":" + strconv.Itoa(f.Line) at := f.Path + ":" + strconv.Itoa(f.Line)
switch filepath.Base(fields[0]) { switch base := filepath.Base(fields[0]); {
case "vi", "vim", "nvim", "nano", "emacs", "emacsclient", "micro", "kak", "mg", "ne", "joe", "vis": case plusLine[base]:
return []string{"+" + strconv.Itoa(f.Line), f.Path} 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} return []string{at}
case "code", "codium", "cursor", "windsurf": case base == "code" || base == "codium" || base == "cursor" || base == "windsurf":
return []string{"-g", at} return []string{"-g", at}
} }
return []string{f.Path} return []string{f.Path}
+19
View File
@@ -323,3 +323,22 @@ func TestEditorArgs(t *testing.T) {
t.Errorf("no line: %q", got) 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)
}
}
}