From ba9363128236952a552fb7c8986a76f99657863a Mon Sep 17 00:00:00 2001 From: Bharath-code Date: Sat, 26 Sep 2026 12:49:40 +0530 Subject: [PATCH] fix(tui): launch lazygit and other cwd-based editors correctly (#23) Bare lazygit/gitui/tig now run in the repo dir without a positional path (lazygit parsed it as a subcommand). Editors support a {path} token, and every editor starts with cwd set to the repo. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- internal/tui/editor.go | 39 +++++++++++++++++++++++++++++++++++++ internal/tui/editor_test.go | 36 ++++++++++++++++++++++++++++++++++ internal/tui/update.go | 4 +--- 4 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 internal/tui/editor.go create mode 100644 internal/tui/editor_test.go diff --git a/README.md b/README.md index 64ff1a7..235d88a 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ ignore: - .venv - dist -editor: code # options: code,nvim,lazygit,vim,cursor +editor: code # options: code,nvim,lazygit,vim,cursor — use {path} to place the repo path, e.g. "lazygit --path {path}" ``` ----- diff --git a/internal/tui/editor.go b/internal/tui/editor.go new file mode 100644 index 0000000..2b8aba4 --- /dev/null +++ b/internal/tui/editor.go @@ -0,0 +1,39 @@ +package tui + +import ( + "os/exec" + "strings" +) + +// cwdOnlyEditors reject a positional repo path (lazygit parses it as a +// subcommand, #23) but open whatever repo they are started in. +var cwdOnlyEditors = map[string]bool{"lazygit": true, "gitui": true, "tig": true} + +// editorArgs builds argv for opening repoPath. A {path} token in the config +// is substituted; otherwise the path is appended unless the tool is cwd-only. +func editorArgs(fields []string, repoPath string) []string { + args := make([]string, 0, len(fields)+1) + substituted := false + for _, f := range fields { + if strings.Contains(f, "{path}") { + f = strings.ReplaceAll(f, "{path}", repoPath) + substituted = true + } + args = append(args, f) + } + name := strings.TrimSuffix(strings.ToLower(fields[0][strings.LastIndexAny(fields[0], `/\`)+1:]), ".exe") + // ponytail: cwd-only applies to the bare command, so existing + // "lazygit --path" configs keep getting the path appended. + cwdOnly := len(fields) == 1 && cwdOnlyEditors[name] + if !substituted && !cwdOnly { + args = append(args, repoPath) + } + return args +} + +func editorCmd(fields []string, repoPath string) *exec.Cmd { + args := editorArgs(fields, repoPath) + c := exec.Command(args[0], args[1:]...) + c.Dir = repoPath + return c +} diff --git a/internal/tui/editor_test.go b/internal/tui/editor_test.go new file mode 100644 index 0000000..b334018 --- /dev/null +++ b/internal/tui/editor_test.go @@ -0,0 +1,36 @@ +package tui + +import ( + "reflect" + "testing" +) + +func TestEditorArgs(t *testing.T) { + const p = "/repos/app" + tests := []struct { + name string + fields []string + want []string + }{ + {"plain editor appends path", []string{"code"}, []string{"code", p}}, + {"flags kept before path", []string{"nvim", "-R"}, []string{"nvim", "-R", p}}, + {"lazygit uses cwd only", []string{"lazygit"}, []string{"lazygit"}}, + {"lazygit.exe on windows", []string{`C:\bin\LazyGit.exe`}, []string{`C:\bin\LazyGit.exe`}}, + {"path token substituted", []string{"lazygit", "--path", "{path}"}, []string{"lazygit", "--path", p}}, + {"path token inside arg", []string{"tool", "--dir={path}"}, []string{"tool", "--dir=" + p}}, + {"legacy trailing flag still works", []string{"lazygit", "--path"}, []string{"lazygit", "--path", p}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := editorArgs(tt.fields, p); !reflect.DeepEqual(got, tt.want) { + t.Errorf("editorArgs(%q) = %q, want %q", tt.fields, got, tt.want) + } + }) + } +} + +func TestEditorCmdSetsDir(t *testing.T) { + if c := editorCmd([]string{"vim"}, "/repos/app"); c.Dir != "/repos/app" { + t.Errorf("Dir = %q, want repo path", c.Dir) + } +} diff --git a/internal/tui/update.go b/internal/tui/update.go index 22a0c09..907f4ea 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -95,9 +95,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - args := append(fields[1:], msg.path) - c := exec.Command(fields[0], args...) - return m, tea.ExecProcess(c, func(err error) tea.Msg { + return m, tea.ExecProcess(editorCmd(fields, msg.path), func(err error) tea.Msg { if err != nil { return editorClosedMsg{err: err} }