detached f04184f

refactor(daemon): hold one ProcessInspector instead of four funcs

commit
f04184feb4855026ada30c48ee92c042f3e2b8ad
author
brice · · 2026-10-08 06:01 UTC
committer
brice · · 2026-10-08 08:12 UTC
parent
7d8f24d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
14 files · +140 -46
M internal/usecase/daemon/bar_refresh.go +3 -3
@@ -136,13 +136,13 @@ func (d *Daemon) collectBarScriptContext(sess *session, anchor string) (barScrip
136136 pid := 0
137137 if p := tb.focusedPane(); p != nil {
138138 ctx.Pane = p.stableID
139- if d != nil && d.procCwd != nil && p.pty != nil {
139+ if d != nil && d.proc != nil && p.pty != nil {
140140 pid = p.pty.Pid()
141141 }
142142 }
143143 tb.mu.Unlock()
144- if pid != 0 && d != nil && d.procCwd != nil {
145- if cwd, err := d.procCwd(pid); err == nil && cwd != "" {
144+ if pid != 0 && d != nil && d.proc != nil {
145+ if cwd, err := d.proc.Cwd(pid); err == nil && cwd != "" {
146146 ctx.PaneCWD = cwd
147147 }
148148 }
@@ -136,13 +136,13 @@ func (d *Daemon) collectBarScriptContext(sess *session, anchor string) (barScrip
136 pid := 0136 pid := 0
137 if p := tb.focusedPane(); p != nil {137 if p := tb.focusedPane(); p != nil {
138 ctx.Pane = p.stableID138 ctx.Pane = p.stableID
139- if d != nil && d.procCwd != nil && p.pty != nil {139+ if d != nil && d.proc != nil && p.pty != nil {
140 pid = p.pty.Pid()140 pid = p.pty.Pid()
141 }141 }
142 }142 }
143 tb.mu.Unlock()143 tb.mu.Unlock()
144- if pid != 0 && d != nil && d.procCwd != nil {144+ if pid != 0 && d != nil && d.proc != nil {
145- if cwd, err := d.procCwd(pid); err == nil && cwd != "" {145+ if cwd, err := d.proc.Cwd(pid); err == nil && cwd != "" {
146 ctx.PaneCWD = cwd146 ctx.PaneCWD = cwd
147 }147 }
148 }148 }
M internal/usecase/daemon/bar_refresh_test.go +2 -2
@@ -96,10 +96,10 @@ func TestBarScriptContextUsesFirstOrderedPaneAndClientCols(t *testing.T) {
9696 active := sess.tabs[0].focusedPane()
9797 active.stableID = "pane-default"
9898 active.pty = newScriptPTY(nil)
99- d.procCwd = func(pid int) (string, error) {
99+ WithCwdReader(func(pid int) (string, error) {
100100 require.Equal(t, 4242, pid)
101101 return "/pane-repo", nil
102- }
102+ })(d)
103103
104104 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true))
105105 waitBarRefreshIdle(t, d)
@@ -96,10 +96,10 @@ func TestBarScriptContextUsesFirstOrderedPaneAndClientCols(t *testing.T) {
96 active := sess.tabs[0].focusedPane()96 active := sess.tabs[0].focusedPane()
97 active.stableID = "pane-default"97 active.stableID = "pane-default"
98 active.pty = newScriptPTY(nil)98 active.pty = newScriptPTY(nil)
99- d.procCwd = func(pid int) (string, error) {99+ WithCwdReader(func(pid int) (string, error) {
100 require.Equal(t, 4242, pid)100 require.Equal(t, 4242, pid)
101 return "/pane-repo", nil101 return "/pane-repo", nil
102- }102+ })(d)
103 103
104 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true))104 require.True(t, d.refreshBarScriptsIfDue(sess, time.Unix(0, 0), true))
105 waitBarRefreshIdle(t, d)105 waitBarRefreshIdle(t, d)
M internal/usecase/daemon/control.go +2 -2
@@ -964,8 +964,8 @@ func (e controlExec) ListPanes(asJSON bool) (string, error) {
964964 }
965965 pane.mu.Unlock()
966966 cwd := fallbackCWD
967- if e.d.procCwd != nil && pid > 0 {
968- if live, err := e.d.procCwd(pid); err == nil && live != "" {
967+ if e.d.proc != nil && pid > 0 {
968+ if live, err := e.d.proc.Cwd(pid); err == nil && live != "" {
969969 cwd = live
970970 }
971971 }
@@ -964,8 +964,8 @@ func (e controlExec) ListPanes(asJSON bool) (string, error) {
964 }964 }
965 pane.mu.Unlock()965 pane.mu.Unlock()
966 cwd := fallbackCWD966 cwd := fallbackCWD
967- if e.d.procCwd != nil && pid > 0 {967+ if e.d.proc != nil && pid > 0 {
968- if live, err := e.d.procCwd(pid); err == nil && live != "" {968+ if live, err := e.d.proc.Cwd(pid); err == nil && live != "" {
969 cwd = live969 cwd = live
970 }970 }
971 }971 }
M internal/usecase/daemon/control_test.go +2 -2
@@ -868,10 +868,10 @@ func TestRemoteCatalogRefreshesFocusedTabTitle(t *testing.T) {
868868 pty := portsmocks.NewMockPTY(t)
869869 pty.EXPECT().ForegroundPgid().Return(1234, nil).Once()
870870 p.pty = pty
871- d.procComm = func(pid int) (string, error) {
871+ d.proc = &processInspectorFake{comm: func(pid int) (string, error) {
872872 require.Equal(t, 1234, pid)
873873 return "fish", nil
874- }
874+ }}
875875
876876 result := sendCommand(t, d, protocol.CommandRequest{Slug: "remote-catalog", JSON: true})
877877 require.True(t, result.Outcome == protocol.CommandSucceeded, result.Text)
@@ -868,10 +868,10 @@ func TestRemoteCatalogRefreshesFocusedTabTitle(t *testing.T) {
868 pty := portsmocks.NewMockPTY(t)868 pty := portsmocks.NewMockPTY(t)
869 pty.EXPECT().ForegroundPgid().Return(1234, nil).Once()869 pty.EXPECT().ForegroundPgid().Return(1234, nil).Once()
870 p.pty = pty870 p.pty = pty
871- d.procComm = func(pid int) (string, error) {871+ d.proc = &processInspectorFake{comm: func(pid int) (string, error) {
872 require.Equal(t, 1234, pid)872 require.Equal(t, 1234, pid)
873 return "fish", nil873 return "fish", nil
874- }874+ }}
875 875
876 result := sendCommand(t, d, protocol.CommandRequest{Slug: "remote-catalog", JSON: true})876 result := sendCommand(t, d, protocol.CommandRequest{Slug: "remote-catalog", JSON: true})
877 require.True(t, result.Outcome == protocol.CommandSucceeded, result.Text)877 require.True(t, result.Outcome == protocol.CommandSucceeded, result.Text)
M internal/usecase/daemon/daemon.go +8 -10
@@ -225,11 +225,10 @@ type Daemon struct {
225225 shutdownNoticedSessions map[string]struct{}
226226 restoreDone chan struct{}
227227 restoreOnce sync.Once
228- procCwd func(int) (string, error)
229- procComm func(int) (string, error)
230- procArgv func(int) ([]string, error)
231- procGroupArgv func(int, int) ([]string, error)
232- dirOrHome func(string) string
228+ // proc inspects pane processes (cwd, foreground command, argv). Nil means
229+ // no inspection is available; see WithProcessInspector and WithCwdReader.
230+ proc ports.ProcessInspector
231+ dirOrHome func(string) string
233232 // bindings is read by keys.Router through its own atomic pointer, so it is
234233 // published next to (not inside) the runtime config snapshot.
235234 bindings atomic.Pointer[keys.Bindings]
@@ -567,10 +566,12 @@ func WithSnapshotGarbageCollection() Option {
567566 }
568567
569568 // WithCwdReader overrides the process cwd reader used for persistence tests.
569+// Every other inspection keeps using the installed process inspector, or stays
570+// unavailable when none is installed.
570571 func WithCwdReader(fn func(int) (string, error)) Option {
571572 return func(d *Daemon) {
572573 if fn != nil {
573- d.procCwd = fn
574+ d.proc = cwdOverrideInspector{base: unwrapCwdOverride(d.proc), cwd: fn}
574575 }
575576 }
576577 }
@@ -581,10 +582,7 @@ func WithProcessInspector(ins ports.ProcessInspector) Option {
581582 if ins == nil {
582583 return
583584 }
584- d.procCwd = ins.Cwd
585- d.procComm = ins.Comm
586- d.procArgv = ins.Argv
587- d.procGroupArgv = ins.GroupArgv
585+ d.proc = ins
588586 }
589587 }
590588
@@ -225,11 +225,10 @@ type Daemon struct {
225 shutdownNoticedSessions map[string]struct{}225 shutdownNoticedSessions map[string]struct{}
226 restoreDone chan struct{}226 restoreDone chan struct{}
227 restoreOnce sync.Once227 restoreOnce sync.Once
228- procCwd func(int) (string, error)228+ // proc inspects pane processes (cwd, foreground command, argv). Nil means
229- procComm func(int) (string, error)229+ // no inspection is available; see WithProcessInspector and WithCwdReader.
230- procArgv func(int) ([]string, error)230+ proc ports.ProcessInspector
231- procGroupArgv func(int, int) ([]string, error)231+ dirOrHome func(string) string
232- dirOrHome func(string) string
233 // bindings is read by keys.Router through its own atomic pointer, so it is232 // bindings is read by keys.Router through its own atomic pointer, so it is
234 // published next to (not inside) the runtime config snapshot.233 // published next to (not inside) the runtime config snapshot.
235 bindings atomic.Pointer[keys.Bindings]234 bindings atomic.Pointer[keys.Bindings]
@@ -567,10 +566,12 @@ func WithSnapshotGarbageCollection() Option {
567 }566 }
568 567
569 // WithCwdReader overrides the process cwd reader used for persistence tests.568 // WithCwdReader overrides the process cwd reader used for persistence tests.
569+// Every other inspection keeps using the installed process inspector, or stays
570+// unavailable when none is installed.
570 func WithCwdReader(fn func(int) (string, error)) Option {571 func WithCwdReader(fn func(int) (string, error)) Option {
571 return func(d *Daemon) {572 return func(d *Daemon) {
572 if fn != nil {573 if fn != nil {
573- d.procCwd = fn574+ d.proc = cwdOverrideInspector{base: unwrapCwdOverride(d.proc), cwd: fn}
574 }575 }
575 }576 }
576 }577 }
@@ -581,10 +582,7 @@ func WithProcessInspector(ins ports.ProcessInspector) Option {
581 if ins == nil {582 if ins == nil {
582 return583 return
583 }584 }
584- d.procCwd = ins.Cwd585+ d.proc = ins
585- d.procComm = ins.Comm
586- d.procArgv = ins.Argv
587- d.procGroupArgv = ins.GroupArgv
588 }586 }
589 }587 }
590 588
M internal/usecase/daemon/floating.go +2 -2
@@ -361,8 +361,8 @@ func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.Floati
361361 focused := tb.focusedPane()
362362 tabStableID, tabCtx := tb.stableID, tb.ctx
363363 tb.mu.Unlock()
364- if focused != nil && d.procCwd != nil && focused.pty != nil {
365- if live, err := d.procCwd(focused.pty.Pid()); err == nil && live != "" {
364+ if focused != nil && d.proc != nil && focused.pty != nil {
365+ if live, err := d.proc.Cwd(focused.pty.Pid()); err == nil && live != "" {
366366 cwd = live
367367 }
368368 }
@@ -361,8 +361,8 @@ func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.Floati
361 focused := tb.focusedPane()361 focused := tb.focusedPane()
362 tabStableID, tabCtx := tb.stableID, tb.ctx362 tabStableID, tabCtx := tb.stableID, tb.ctx
363 tb.mu.Unlock()363 tb.mu.Unlock()
364- if focused != nil && d.procCwd != nil && focused.pty != nil {364+ if focused != nil && d.proc != nil && focused.pty != nil {
365- if live, err := d.procCwd(focused.pty.Pid()); err == nil && live != "" {365+ if live, err := d.proc.Cwd(focused.pty.Pid()); err == nil && live != "" {
366 cwd = live366 cwd = live
367 }367 }
368 }368 }
M internal/usecase/daemon/floating_spare_test.go +1 -1
@@ -124,7 +124,7 @@ func TestFloatingSpareIsSharedPerSession(t *testing.T) {
124124 d := newTestDaemon(t, factory, stubClock{})
125125 d.ApplyConfig(domain.Config{Floating: cfg})
126126 cwds := map[int]string{1: "/work", 2: tt.claimCwd}
127- d.procCwd = func(pid int) (string, error) { return cwds[pid], nil }
127+ WithCwdReader(func(pid int) (string, error) { return cwds[pid], nil })(d)
128128 d.dirOrHome = func(cwd string) string { return cwd }
129129
130130 first := newTab(&pidPTY{pid: 1}, domain.Size{Cols: 120, Rows: 40})
@@ -124,7 +124,7 @@ func TestFloatingSpareIsSharedPerSession(t *testing.T) {
124 d := newTestDaemon(t, factory, stubClock{})124 d := newTestDaemon(t, factory, stubClock{})
125 d.ApplyConfig(domain.Config{Floating: cfg})125 d.ApplyConfig(domain.Config{Floating: cfg})
126 cwds := map[int]string{1: "/work", 2: tt.claimCwd}126 cwds := map[int]string{1: "/work", 2: tt.claimCwd}
127- d.procCwd = func(pid int) (string, error) { return cwds[pid], nil }127+ WithCwdReader(func(pid int) (string, error) { return cwds[pid], nil })(d)
128 d.dirOrHome = func(cwd string) string { return cwd }128 d.dirOrHome = func(cwd string) string { return cwd }
129 129
130 first := newTab(&pidPTY{pid: 1}, domain.Size{Cols: 120, Rows: 40})130 first := newTab(&pidPTY{pid: 1}, domain.Size{Cols: 120, Rows: 40})
M internal/usecase/daemon/floating_test.go +1 -1
@@ -627,7 +627,7 @@ func TestFloatingLaunchUsesLiveOrValidatedSessionCwd(t *testing.T) {
627627 return cwd
628628 }
629629 if tt.liveCwd != "" {
630- d.procCwd = func(int) (string, error) { return tt.liveCwd, nil }
630+ WithCwdReader(func(int) (string, error) { return tt.liveCwd, nil })(d)
631631 }
632632 tb := newFloatingTestTab(t)
633633 sess := &session{sessionCore: sessionCore{name: "work"}, cwd: tt.sessionCwd, tabs: []*tab{tb}, ctx: t.Context()}
@@ -627,7 +627,7 @@ func TestFloatingLaunchUsesLiveOrValidatedSessionCwd(t *testing.T) {
627 return cwd627 return cwd
628 }628 }
629 if tt.liveCwd != "" {629 if tt.liveCwd != "" {
630- d.procCwd = func(int) (string, error) { return tt.liveCwd, nil }630+ WithCwdReader(func(int) (string, error) { return tt.liveCwd, nil })(d)
631 }631 }
632 tb := newFloatingTestTab(t)632 tb := newFloatingTestTab(t)
633 sess := &session{sessionCore: sessionCore{name: "work"}, cwd: tt.sessionCwd, tabs: []*tab{tb}, ctx: t.Context()}633 sess := &session{sessionCore: sessionCore{name: "work"}, cwd: tt.sessionCwd, tabs: []*tab{tb}, ctx: t.Context()}
M internal/usecase/daemon/input_test.go +6 -6
@@ -1262,7 +1262,7 @@ func TestMouseWheelOverUnfocusedPaneDoesNotFocusAndForwardsChildMouse(t *testing
12621262 p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe()
12631263 p2.EXPECT().Write([]byte("\x1b[<64;1;1M")).Return(len("\x1b[<64;1;1M"), nil).Once()
12641264 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
1265- d.procComm = nil
1265+ d.proc = nil
12661266 tb := testAttachmentTab(sess)
12671267 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})
12681268 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
@@ -1444,7 +1444,7 @@ func TestMouseDividerAndTitleBarDoNotForwardBogusCoordinates(t *testing.T) {
14441444 p2 := portsmocks.NewMockPTY(t)
14451445 p2.EXPECT().Resize(mock.Anything).Return(nil).Maybe()
14461446 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
1447- d.procComm = nil
1447+ d.proc = nil
14481448 tb := testAttachmentTab(sess)
14491449 tb.focusedPane().screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
14501450 tb.mu.Lock()
@@ -1467,7 +1467,7 @@ func TestCopyModeDragOutsideSplitPaneClampsToPaneContent(t *testing.T) {
14671467 p2 := portsmocks.NewMockPTY(t)
14681468 p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Maybe()
14691469 d, sess, ac, sends := newManualSessionWithPTYs(t, p1)
1470- d.procComm = nil
1470+ d.proc = nil
14711471 tb := testAttachmentTab(sess)
14721472 for i := range 10 {
14731473 writeTestRow(tb.focusedPane().screen, i, string(rune('a'+i)))
@@ -1501,7 +1501,7 @@ func TestMouseHitTestFocusesPaneAndTranslatesSGRColumns(t *testing.T) {
15011501 p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe()
15021502 p2.EXPECT().Write([]byte("\x1b[<0;1;1M")).Return(len("\x1b[<0;1;1M"), nil).Once()
15031503 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
1504- d.procComm = nil
1504+ d.proc = nil
15051505 tb := testAttachmentTab(sess)
15061506 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})
15071507 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
@@ -1528,7 +1528,7 @@ func TestMouseGatedWhileNoticesOverlayActive(t *testing.T) {
15281528 p2 := portsmocks.NewMockPTY(t)
15291529 p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe()
15301530 d, sess, ac, sends := newManualSessionWithPTYs(t, p1)
1531- d.procComm = nil
1531+ d.proc = nil
15321532 tb := testAttachmentTab(sess)
15331533 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})
15341534 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
@@ -1558,7 +1558,7 @@ func TestMouseCollapsedStackBarExpandsAndFocuses(t *testing.T) {
15581558 p2 := portsmocks.NewMockPTY(t)
15591559 p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 4}}).Return(nil).Maybe()
15601560 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
1561- d.procComm = nil
1561+ d.proc = nil
15621562 tb := testAttachmentTab(sess)
15631563 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 3})
15641564 tb.mu.Lock()
@@ -1262,7 +1262,7 @@ func TestMouseWheelOverUnfocusedPaneDoesNotFocusAndForwardsChildMouse(t *testing
1262 p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe()1262 p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe()
1263 p2.EXPECT().Write([]byte("\x1b[<64;1;1M")).Return(len("\x1b[<64;1;1M"), nil).Once()1263 p2.EXPECT().Write([]byte("\x1b[<64;1;1M")).Return(len("\x1b[<64;1;1M"), nil).Once()
1264 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)1264 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
1265- d.procComm = nil1265+ d.proc = nil
1266 tb := testAttachmentTab(sess)1266 tb := testAttachmentTab(sess)
1267 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})1267 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})
1268 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))1268 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
@@ -1444,7 +1444,7 @@ func TestMouseDividerAndTitleBarDoNotForwardBogusCoordinates(t *testing.T) {
1444 p2 := portsmocks.NewMockPTY(t)1444 p2 := portsmocks.NewMockPTY(t)
1445 p2.EXPECT().Resize(mock.Anything).Return(nil).Maybe()1445 p2.EXPECT().Resize(mock.Anything).Return(nil).Maybe()
1446 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)1446 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
1447- d.procComm = nil1447+ d.proc = nil
1448 tb := testAttachmentTab(sess)1448 tb := testAttachmentTab(sess)
1449 tb.focusedPane().screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))1449 tb.focusedPane().screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
1450 tb.mu.Lock()1450 tb.mu.Lock()
@@ -1467,7 +1467,7 @@ func TestCopyModeDragOutsideSplitPaneClampsToPaneContent(t *testing.T) {
1467 p2 := portsmocks.NewMockPTY(t)1467 p2 := portsmocks.NewMockPTY(t)
1468 p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Maybe()1468 p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 10}}).Return(nil).Maybe()
1469 d, sess, ac, sends := newManualSessionWithPTYs(t, p1)1469 d, sess, ac, sends := newManualSessionWithPTYs(t, p1)
1470- d.procComm = nil1470+ d.proc = nil
1471 tb := testAttachmentTab(sess)1471 tb := testAttachmentTab(sess)
1472 for i := range 10 {1472 for i := range 10 {
1473 writeTestRow(tb.focusedPane().screen, i, string(rune('a'+i)))1473 writeTestRow(tb.focusedPane().screen, i, string(rune('a'+i)))
@@ -1501,7 +1501,7 @@ func TestMouseHitTestFocusesPaneAndTranslatesSGRColumns(t *testing.T) {
1501 p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe()1501 p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe()
1502 p2.EXPECT().Write([]byte("\x1b[<0;1;1M")).Return(len("\x1b[<0;1;1M"), nil).Once()1502 p2.EXPECT().Write([]byte("\x1b[<0;1;1M")).Return(len("\x1b[<0;1;1M"), nil).Once()
1503 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)1503 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
1504- d.procComm = nil1504+ d.proc = nil
1505 tb := testAttachmentTab(sess)1505 tb := testAttachmentTab(sess)
1506 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})1506 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})
1507 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))1507 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
@@ -1528,7 +1528,7 @@ func TestMouseGatedWhileNoticesOverlayActive(t *testing.T) {
1528 p2 := portsmocks.NewMockPTY(t)1528 p2 := portsmocks.NewMockPTY(t)
1529 p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe()1529 p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 5}}).Return(nil).Maybe()
1530 d, sess, ac, sends := newManualSessionWithPTYs(t, p1)1530 d, sess, ac, sends := newManualSessionWithPTYs(t, p1)
1531- d.procComm = nil1531+ d.proc = nil
1532 tb := testAttachmentTab(sess)1532 tb := testAttachmentTab(sess)
1533 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})1533 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 5})
1534 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))1534 p2pane.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
@@ -1558,7 +1558,7 @@ func TestMouseCollapsedStackBarExpandsAndFocuses(t *testing.T) {
1558 p2 := portsmocks.NewMockPTY(t)1558 p2 := portsmocks.NewMockPTY(t)
1559 p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 4}}).Return(nil).Maybe()1559 p2.EXPECT().Resize(domain.Geometry{Size: domain.Size{Cols: 20, Rows: 4}}).Return(nil).Maybe()
1560 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)1560 d, sess, ac, _ := newManualSessionWithPTYs(t, p1)
1561- d.procComm = nil1561+ d.proc = nil
1562 tb := testAttachmentTab(sess)1562 tb := testAttachmentTab(sess)
1563 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 3})1563 p2pane := newPane("pane-2", p2, domain.Size{Cols: 20, Rows: 3})
1564 tb.mu.Lock()1564 tb.mu.Lock()
M internal/usecase/daemon/pane_titles.go +2 -2
@@ -187,9 +187,9 @@ func (d *Daemon) refreshPaneDisplayTitle(_ *session, p *pane, force bool) string
187187 // distinct from the display fallback: an OSC title must then stand alone,
188188 // and future floating panes can supply their command fallback at format time.
189189 processName := ""
190- if d.procComm != nil && p.pty != nil {
190+ if d.hasProcessDetails() && p.pty != nil {
191191 if pgid, err := p.pty.ForegroundPgid(); err == nil && pgid > 0 {
192- if comm, err := d.procComm(pgid); err == nil && strings.TrimSpace(comm) != "" {
192+ if comm, err := d.proc.Comm(pgid); err == nil && strings.TrimSpace(comm) != "" {
193193 processName = strings.TrimSpace(comm)
194194 }
195195 }
@@ -187,9 +187,9 @@ func (d *Daemon) refreshPaneDisplayTitle(_ *session, p *pane, force bool) string
187 // distinct from the display fallback: an OSC title must then stand alone,187 // distinct from the display fallback: an OSC title must then stand alone,
188 // and future floating panes can supply their command fallback at format time.188 // and future floating panes can supply their command fallback at format time.
189 processName := ""189 processName := ""
190- if d.procComm != nil && p.pty != nil {190+ if d.hasProcessDetails() && p.pty != nil {
191 if pgid, err := p.pty.ForegroundPgid(); err == nil && pgid > 0 {191 if pgid, err := p.pty.ForegroundPgid(); err == nil && pgid > 0 {
192- if comm, err := d.procComm(pgid); err == nil && strings.TrimSpace(comm) != "" {192+ if comm, err := d.proc.Comm(pgid); err == nil && strings.TrimSpace(comm) != "" {
193 processName = strings.TrimSpace(comm)193 processName = strings.TrimSpace(comm)
194 }194 }
195 }195 }
M internal/usecase/daemon/pane_titles_test.go +46 -8
@@ -71,10 +71,10 @@ func TestRefreshPaneTitleUsesForegroundProcessComm(t *testing.T) {
7171 clk.EXPECT().Now().Return(time.Time{}).Maybe()
7272 d := newTestDaemon(t, nil, clk)
7373 d.shell = "/bin/zsh"
74- d.procComm = func(pid int) (string, error) {
74+ d.proc = &processInspectorFake{comm: func(pid int) (string, error) {
7575 require.Equal(t, 1234, pid)
7676 return "vim\n", nil
77- }
77+ }}
7878
7979 title := d.refreshPaneTitle(sess, "pane-1")
8080 require.Equal(t, "vim", title)
@@ -99,10 +99,10 @@ func TestRefreshPaneTitleUsesProvidedOwningTab(t *testing.T) {
9999 sess.tabs = append(sess.tabs, second)
100100 sess.mu.Unlock()
101101 d := newTestDaemon(t, nil, stubClock{})
102- d.procComm = func(pid int) (string, error) {
102+ d.proc = &processInspectorFake{comm: func(pid int) (string, error) {
103103 require.Equal(t, 222, pid)
104104 return "vim", nil
105- }
105+ }}
106106
107107 require.Equal(t, "vim", d.refreshPaneTitle(sess, "pane-1", second))
108108 secondPane := second.panes["pane-1"]
@@ -120,11 +120,11 @@ func TestRefreshPaneTitleCachesByTTLAndRefreshesOnFocus(t *testing.T) {
120120 clk.EXPECT().Now().RunAndReturn(func() time.Time { return now }).Maybe()
121121 d := newTestDaemon(t, nil, clk)
122122 var calls atomic.Int32
123- d.procComm = func(pid int) (string, error) {
123+ d.proc = &processInspectorFake{comm: func(pid int) (string, error) {
124124 require.Equal(t, 1234, pid)
125125 calls.Add(1)
126126 return "vim\n", nil
127- }
127+ }}
128128
129129 require.Equal(t, "vim", d.refreshPaneTitle(sess, "pane-1"))
130130 now = now.Add(paneTitleCacheTTL / 2)
@@ -141,7 +141,7 @@ func TestRefreshFloatingPaneTitlePreservesConfiguredCommandFallback(t *testing.T
141141 clk.EXPECT().Now().Return(time.Time{}).Maybe()
142142 d := newTestDaemon(t, nil, clk)
143143 d.shell = "/usr/bin/fish"
144- d.procComm = func(int) (string, error) { return "", errors.New("unused") }
144+ d.proc = &processInspectorFake{comm: func(int) (string, error) { return "", errors.New("unused") }}
145145 p := newPane("floating", pty, domain.Size{Cols: 20, Rows: 5})
146146 p.title.displayFallback = floatingCommandFallback("btop --utf", d.shell)
147147
@@ -186,7 +186,7 @@ func TestRefreshPaneTitleLookupFailureKeepsProcessNameEmpty(t *testing.T) {
186186 clk.EXPECT().Now().Return(time.Time{}).Maybe()
187187 d := newTestDaemon(t, nil, clk)
188188 d.shell = "/usr/bin/fish"
189- d.procComm = func(int) (string, error) { return "", errors.New("unused") }
189+ d.proc = &processInspectorFake{comm: func(int) (string, error) { return "", errors.New("unused") }}
190190 p := testAttachmentTab(sess).focusedPane()
191191
192192 p.mu.Lock()
@@ -204,3 +204,41 @@ func TestRefreshPaneTitleLookupFailureKeepsProcessNameEmpty(t *testing.T) {
204204 p.mu.Unlock()
205205 require.Equal(t, "sh", d.refreshPaneDisplayTitle(sess, p, true), "pointer refresh must retain its pane-owned fallback")
206206 }
207+
208+// processInspectorFake is a hand-written ports.ProcessInspector whose
209+// behavior is set per test through func fields; an unset func reports an
210+// error, the same as an inspector that cannot read the process.
211+type processInspectorFake struct {
212+ cwd func(int) (string, error)
213+ comm func(int) (string, error)
214+ argv func(int) ([]string, error)
215+ groupArgv func(int, int) ([]string, error)
216+}
217+
218+func (f *processInspectorFake) Cwd(pid int) (string, error) {
219+ if f.cwd == nil {
220+ return "", errProcessInspectionUnavailable
221+ }
222+ return f.cwd(pid)
223+}
224+
225+func (f *processInspectorFake) Comm(pid int) (string, error) {
226+ if f.comm == nil {
227+ return "", errProcessInspectionUnavailable
228+ }
229+ return f.comm(pid)
230+}
231+
232+func (f *processInspectorFake) Argv(pid int) ([]string, error) {
233+ if f.argv == nil {
234+ return nil, errProcessInspectionUnavailable
235+ }
236+ return f.argv(pid)
237+}
238+
239+func (f *processInspectorFake) GroupArgv(pgid, shellPid int) ([]string, error) {
240+ if f.groupArgv == nil {
241+ return nil, errProcessInspectionUnavailable
242+ }
243+ return f.groupArgv(pgid, shellPid)
244+}
@@ -71,10 +71,10 @@ func TestRefreshPaneTitleUsesForegroundProcessComm(t *testing.T) {
71 clk.EXPECT().Now().Return(time.Time{}).Maybe()71 clk.EXPECT().Now().Return(time.Time{}).Maybe()
72 d := newTestDaemon(t, nil, clk)72 d := newTestDaemon(t, nil, clk)
73 d.shell = "/bin/zsh"73 d.shell = "/bin/zsh"
74- d.procComm = func(pid int) (string, error) {74+ d.proc = &processInspectorFake{comm: func(pid int) (string, error) {
75 require.Equal(t, 1234, pid)75 require.Equal(t, 1234, pid)
76 return "vim\n", nil76 return "vim\n", nil
77- }77+ }}
78 78
79 title := d.refreshPaneTitle(sess, "pane-1")79 title := d.refreshPaneTitle(sess, "pane-1")
80 require.Equal(t, "vim", title)80 require.Equal(t, "vim", title)
@@ -99,10 +99,10 @@ func TestRefreshPaneTitleUsesProvidedOwningTab(t *testing.T) {
99 sess.tabs = append(sess.tabs, second)99 sess.tabs = append(sess.tabs, second)
100 sess.mu.Unlock()100 sess.mu.Unlock()
101 d := newTestDaemon(t, nil, stubClock{})101 d := newTestDaemon(t, nil, stubClock{})
102- d.procComm = func(pid int) (string, error) {102+ d.proc = &processInspectorFake{comm: func(pid int) (string, error) {
103 require.Equal(t, 222, pid)103 require.Equal(t, 222, pid)
104 return "vim", nil104 return "vim", nil
105- }105+ }}
106 106
107 require.Equal(t, "vim", d.refreshPaneTitle(sess, "pane-1", second))107 require.Equal(t, "vim", d.refreshPaneTitle(sess, "pane-1", second))
108 secondPane := second.panes["pane-1"]108 secondPane := second.panes["pane-1"]
@@ -120,11 +120,11 @@ func TestRefreshPaneTitleCachesByTTLAndRefreshesOnFocus(t *testing.T) {
120 clk.EXPECT().Now().RunAndReturn(func() time.Time { return now }).Maybe()120 clk.EXPECT().Now().RunAndReturn(func() time.Time { return now }).Maybe()
121 d := newTestDaemon(t, nil, clk)121 d := newTestDaemon(t, nil, clk)
122 var calls atomic.Int32122 var calls atomic.Int32
123- d.procComm = func(pid int) (string, error) {123+ d.proc = &processInspectorFake{comm: func(pid int) (string, error) {
124 require.Equal(t, 1234, pid)124 require.Equal(t, 1234, pid)
125 calls.Add(1)125 calls.Add(1)
126 return "vim\n", nil126 return "vim\n", nil
127- }127+ }}
128 128
129 require.Equal(t, "vim", d.refreshPaneTitle(sess, "pane-1"))129 require.Equal(t, "vim", d.refreshPaneTitle(sess, "pane-1"))
130 now = now.Add(paneTitleCacheTTL / 2)130 now = now.Add(paneTitleCacheTTL / 2)
@@ -141,7 +141,7 @@ func TestRefreshFloatingPaneTitlePreservesConfiguredCommandFallback(t *testing.T
141 clk.EXPECT().Now().Return(time.Time{}).Maybe()141 clk.EXPECT().Now().Return(time.Time{}).Maybe()
142 d := newTestDaemon(t, nil, clk)142 d := newTestDaemon(t, nil, clk)
143 d.shell = "/usr/bin/fish"143 d.shell = "/usr/bin/fish"
144- d.procComm = func(int) (string, error) { return "", errors.New("unused") }144+ d.proc = &processInspectorFake{comm: func(int) (string, error) { return "", errors.New("unused") }}
145 p := newPane("floating", pty, domain.Size{Cols: 20, Rows: 5})145 p := newPane("floating", pty, domain.Size{Cols: 20, Rows: 5})
146 p.title.displayFallback = floatingCommandFallback("btop --utf", d.shell)146 p.title.displayFallback = floatingCommandFallback("btop --utf", d.shell)
147 147
@@ -186,7 +186,7 @@ func TestRefreshPaneTitleLookupFailureKeepsProcessNameEmpty(t *testing.T) {
186 clk.EXPECT().Now().Return(time.Time{}).Maybe()186 clk.EXPECT().Now().Return(time.Time{}).Maybe()
187 d := newTestDaemon(t, nil, clk)187 d := newTestDaemon(t, nil, clk)
188 d.shell = "/usr/bin/fish"188 d.shell = "/usr/bin/fish"
189- d.procComm = func(int) (string, error) { return "", errors.New("unused") }189+ d.proc = &processInspectorFake{comm: func(int) (string, error) { return "", errors.New("unused") }}
190 p := testAttachmentTab(sess).focusedPane()190 p := testAttachmentTab(sess).focusedPane()
191 191
192 p.mu.Lock()192 p.mu.Lock()
@@ -204,3 +204,41 @@ func TestRefreshPaneTitleLookupFailureKeepsProcessNameEmpty(t *testing.T) {
204 p.mu.Unlock()204 p.mu.Unlock()
205 require.Equal(t, "sh", d.refreshPaneDisplayTitle(sess, p, true), "pointer refresh must retain its pane-owned fallback")205 require.Equal(t, "sh", d.refreshPaneDisplayTitle(sess, p, true), "pointer refresh must retain its pane-owned fallback")
206 }206 }
207+
208+// processInspectorFake is a hand-written ports.ProcessInspector whose
209+// behavior is set per test through func fields; an unset func reports an
210+// error, the same as an inspector that cannot read the process.
211+type processInspectorFake struct {
212+ cwd func(int) (string, error)
213+ comm func(int) (string, error)
214+ argv func(int) ([]string, error)
215+ groupArgv func(int, int) ([]string, error)
216+}
217+
218+func (f *processInspectorFake) Cwd(pid int) (string, error) {
219+ if f.cwd == nil {
220+ return "", errProcessInspectionUnavailable
221+ }
222+ return f.cwd(pid)
223+}
224+
225+func (f *processInspectorFake) Comm(pid int) (string, error) {
226+ if f.comm == nil {
227+ return "", errProcessInspectionUnavailable
228+ }
229+ return f.comm(pid)
230+}
231+
232+func (f *processInspectorFake) Argv(pid int) ([]string, error) {
233+ if f.argv == nil {
234+ return nil, errProcessInspectionUnavailable
235+ }
236+ return f.argv(pid)
237+}
238+
239+func (f *processInspectorFake) GroupArgv(pgid, shellPid int) ([]string, error) {
240+ if f.groupArgv == nil {
241+ return nil, errProcessInspectionUnavailable
242+ }
243+ return f.groupArgv(pgid, shellPid)
244+}
A internal/usecase/daemon/process_inspector.go +58 -0
@@ -0,0 +1,58 @@
1+package daemon
2+
3+import (
4+ "errors"
5+
6+ "github.com/bnema/vev/internal/ports"
7+)
8+
9+var errProcessInspectionUnavailable = errors.New("process inspection unavailable")
10+
11+// cwdOverrideInspector replaces only Cwd on top of an optional base inspector.
12+// With a nil base, Comm, Argv, and GroupArgv report the inspection as
13+// unavailable, matching a daemon that was given only a cwd reader.
14+type cwdOverrideInspector struct {
15+ base ports.ProcessInspector
16+ cwd func(int) (string, error)
17+}
18+
19+func (i cwdOverrideInspector) Cwd(pid int) (string, error) { return i.cwd(pid) }
20+
21+func (i cwdOverrideInspector) Comm(pid int) (string, error) {
22+ if i.base == nil {
23+ return "", errProcessInspectionUnavailable
24+ }
25+ return i.base.Comm(pid)
26+}
27+
28+func (i cwdOverrideInspector) Argv(pid int) ([]string, error) {
29+ if i.base == nil {
30+ return nil, errProcessInspectionUnavailable
31+ }
32+ return i.base.Argv(pid)
33+}
34+
35+func (i cwdOverrideInspector) GroupArgv(pgid, shellPid int) ([]string, error) {
36+ if i.base == nil {
37+ return nil, errProcessInspectionUnavailable
38+ }
39+ return i.base.GroupArgv(pgid, shellPid)
40+}
41+
42+// unwrapCwdOverride returns the inspector beneath a previous cwd override so
43+// repeated overrides do not stack.
44+func unwrapCwdOverride(ins ports.ProcessInspector) ports.ProcessInspector {
45+ if o, ok := ins.(cwdOverrideInspector); ok {
46+ return o.base
47+ }
48+ return ins
49+}
50+
51+// hasProcessDetails reports whether Comm, Argv, and GroupArgv are backed by a
52+// real inspector rather than a cwd-only override.
53+func (d *Daemon) hasProcessDetails() bool {
54+ if o, ok := d.proc.(cwdOverrideInspector); ok {
55+ return o.base != nil
56+ }
57+ return d.proc != nil
58+}
@@ -0,0 +1,58 @@
1+package daemon
2+
3+import (
4+ "errors"
5+
6+ "github.com/bnema/vev/internal/ports"
7+)
8+
9+var errProcessInspectionUnavailable = errors.New("process inspection unavailable")
10+
11+// cwdOverrideInspector replaces only Cwd on top of an optional base inspector.
12+// With a nil base, Comm, Argv, and GroupArgv report the inspection as
13+// unavailable, matching a daemon that was given only a cwd reader.
14+type cwdOverrideInspector struct {
15+ base ports.ProcessInspector
16+ cwd func(int) (string, error)
17+}
18+
19+func (i cwdOverrideInspector) Cwd(pid int) (string, error) { return i.cwd(pid) }
20+
21+func (i cwdOverrideInspector) Comm(pid int) (string, error) {
22+ if i.base == nil {
23+ return "", errProcessInspectionUnavailable
24+ }
25+ return i.base.Comm(pid)
26+}
27+
28+func (i cwdOverrideInspector) Argv(pid int) ([]string, error) {
29+ if i.base == nil {
30+ return nil, errProcessInspectionUnavailable
31+ }
32+ return i.base.Argv(pid)
33+}
34+
35+func (i cwdOverrideInspector) GroupArgv(pgid, shellPid int) ([]string, error) {
36+ if i.base == nil {
37+ return nil, errProcessInspectionUnavailable
38+ }
39+ return i.base.GroupArgv(pgid, shellPid)
40+}
41+
42+// unwrapCwdOverride returns the inspector beneath a previous cwd override so
43+// repeated overrides do not stack.
44+func unwrapCwdOverride(ins ports.ProcessInspector) ports.ProcessInspector {
45+ if o, ok := ins.(cwdOverrideInspector); ok {
46+ return o.base
47+ }
48+ return ins
49+}
50+
51+// hasProcessDetails reports whether Comm, Argv, and GroupArgv are backed by a
52+// real inspector rather than a cwd-only override.
53+func (d *Daemon) hasProcessDetails() bool {
54+ if o, ok := d.proc.(cwdOverrideInspector); ok {
55+ return o.base != nil
56+ }
57+ return d.proc != nil
58+}
M internal/usecase/daemon/session.go +3 -3
@@ -2111,7 +2111,7 @@ func (d *Daemon) cwdSampler(ctx context.Context) {
21112111 return
21122112 case <-t.C():
21132113 }
2114- if d.procCwd != nil {
2114+ if d.proc != nil {
21152115 d.refreshNamedSessionCwds()
21162116 }
21172117 if err := d.flushCatalogue(); err != nil {
@@ -2136,7 +2136,7 @@ func (d *Daemon) refreshNamedSessionCwds() {
21362136 }
21372137
21382138 func (d *Daemon) refreshSessionCwd(sess *session) {
2139- if d.procCwd == nil {
2139+ if d.proc == nil {
21402140 return
21412141 }
21422142 tb := sess.firstTab()
@@ -2149,7 +2149,7 @@ func (d *Daemon) refreshSessionCwd(sess *session) {
21492149 if p == nil {
21502150 return
21512151 }
2152- cwd, err := d.procCwd(p.pty.Pid())
2152+ cwd, err := d.proc.Cwd(p.pty.Pid())
21532153 if err != nil || cwd == "" {
21542154 return
21552155 }
@@ -2111,7 +2111,7 @@ func (d *Daemon) cwdSampler(ctx context.Context) {
2111 return2111 return
2112 case <-t.C():2112 case <-t.C():
2113 }2113 }
2114- if d.procCwd != nil {2114+ if d.proc != nil {
2115 d.refreshNamedSessionCwds()2115 d.refreshNamedSessionCwds()
2116 }2116 }
2117 if err := d.flushCatalogue(); err != nil {2117 if err := d.flushCatalogue(); err != nil {
@@ -2136,7 +2136,7 @@ func (d *Daemon) refreshNamedSessionCwds() {
2136 }2136 }
2137 2137
2138 func (d *Daemon) refreshSessionCwd(sess *session) {2138 func (d *Daemon) refreshSessionCwd(sess *session) {
2139- if d.procCwd == nil {2139+ if d.proc == nil {
2140 return2140 return
2141 }2141 }
2142 tb := sess.firstTab()2142 tb := sess.firstTab()
@@ -2149,7 +2149,7 @@ func (d *Daemon) refreshSessionCwd(sess *session) {
2149 if p == nil {2149 if p == nil {
2150 return2150 return
2151 }2151 }
2152- cwd, err := d.procCwd(p.pty.Pid())2152+ cwd, err := d.proc.Cwd(p.pty.Pid())
2153 if err != nil || cwd == "" {2153 if err != nil || cwd == "" {
2154 return2154 return
2155 }2155 }
M internal/usecase/daemon/snapshot_capture.go +4 -4
@@ -61,8 +61,8 @@ func (d *Daemon) captureSnapshotState(sess *session, generation uint64) (*snapsh
6161 paneCapture.tail = paneCapture.sealed.Tail()
6262 p.mu.Unlock()
6363 paneCapture.cwd = fallbackCwd
64- if d.procCwd != nil && pid > 0 {
65- if cwd, err := d.procCwd(pid); err == nil && cwd != "" {
64+ if d.proc != nil && pid > 0 {
65+ if cwd, err := d.proc.Cwd(pid); err == nil && cwd != "" {
6666 paneCapture.cwd = cwd
6767 }
6868 }
@@ -82,14 +82,14 @@ func (d *Daemon) captureSession(sess *session) bool {
8282 }
8383
8484 func (d *Daemon) capturePaneProcess(pty interface{ ForegroundPgid() (int, error) }, shellPid int) *snapcodec.Process {
85- if d == nil || pty == nil || shellPid <= 0 || d.procGroupArgv == nil {
85+ if d == nil || pty == nil || shellPid <= 0 || !d.hasProcessDetails() {
8686 return nil
8787 }
8888 pgid, err := pty.ForegroundPgid()
8989 if err != nil || pgid <= 0 || pgid == shellPid {
9090 return nil
9191 }
92- argv, err := d.procGroupArgv(pgid, shellPid)
92+ argv, err := d.proc.GroupArgv(pgid, shellPid)
9393 if err != nil || len(argv) == 0 || argv[0] == "" {
9494 return nil
9595 }
@@ -61,8 +61,8 @@ func (d *Daemon) captureSnapshotState(sess *session, generation uint64) (*snapsh
61 paneCapture.tail = paneCapture.sealed.Tail()61 paneCapture.tail = paneCapture.sealed.Tail()
62 p.mu.Unlock()62 p.mu.Unlock()
63 paneCapture.cwd = fallbackCwd63 paneCapture.cwd = fallbackCwd
64- if d.procCwd != nil && pid > 0 {64+ if d.proc != nil && pid > 0 {
65- if cwd, err := d.procCwd(pid); err == nil && cwd != "" {65+ if cwd, err := d.proc.Cwd(pid); err == nil && cwd != "" {
66 paneCapture.cwd = cwd66 paneCapture.cwd = cwd
67 }67 }
68 }68 }
@@ -82,14 +82,14 @@ func (d *Daemon) captureSession(sess *session) bool {
82 }82 }
83 83
84 func (d *Daemon) capturePaneProcess(pty interface{ ForegroundPgid() (int, error) }, shellPid int) *snapcodec.Process {84 func (d *Daemon) capturePaneProcess(pty interface{ ForegroundPgid() (int, error) }, shellPid int) *snapcodec.Process {
85- if d == nil || pty == nil || shellPid <= 0 || d.procGroupArgv == nil {85+ if d == nil || pty == nil || shellPid <= 0 || !d.hasProcessDetails() {
86 return nil86 return nil
87 }87 }
88 pgid, err := pty.ForegroundPgid()88 pgid, err := pty.ForegroundPgid()
89 if err != nil || pgid <= 0 || pgid == shellPid {89 if err != nil || pgid <= 0 || pgid == shellPid {
90 return nil90 return nil
91 }91 }
92- argv, err := d.procGroupArgv(pgid, shellPid)92+ argv, err := d.proc.GroupArgv(pgid, shellPid)
93 if err != nil || len(argv) == 0 || argv[0] == "" {93 if err != nil || len(argv) == 0 || argv[0] == "" {
94 return nil94 return nil
95 }95 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close