pane_titles_test.go RawHistoryFolder

1package daemon
2
3import (
4	"errors"
5	"sync/atomic"
6	"testing"
7	"time"
8
9	"github.com/stretchr/testify/require"
10
11	"github.com/bnema/vev/internal/domain"
12	portsmocks "github.com/bnema/vev/internal/ports/mocks"
13)
14
15func TestFormatPaneTitle(t *testing.T) {
16	tests := []struct {
17		name, process, terminal, fallback, want string
18	}{
19		{name: "process prepends non-prefixed terminal title", process: "nvim", terminal: "project/main.go", fallback: "sh", want: "nvim: project/main.go"},
20		{name: "process does not duplicate its terminal-title prefix", process: "zot", terminal: "zot: session title", fallback: "sh", want: "zot: session title"},
21		{name: "process only", process: "nvim", fallback: "sh", want: "nvim"},
22		{name: "terminal only", terminal: "project/main.go", fallback: "sh", want: "project/main.go"},
23		{name: "fallback", fallback: "fish", want: "fish"},
24	}
25	for _, tt := range tests {
26		t.Run(tt.name, func(t *testing.T) {
27			require.Equal(t, tt.want, formatPaneTitle(tt.process, tt.terminal, tt.fallback))
28		})
29	}
30}
31
32func TestComposeTabTitle(t *testing.T) {
33	tests := []struct {
34		name, tabName, paneTitle, want string
35	}{
36		{name: "name and title", tabName: "api", paneTitle: "nvim: server.go — vev", want: "api (nvim: server.go — vev)"},
37		{name: "empty pane title", tabName: "api", paneTitle: "", want: "api"},
38		{name: "pane title equals name", tabName: "api", paneTitle: "api", want: "api"},
39		{name: "unnamed tab style", tabName: "1", paneTitle: "zsh", want: "1 (zsh)"},
40	}
41	for _, tt := range tests {
42		t.Run(tt.name, func(t *testing.T) {
43			require.Equal(t, tt.want, composeTabTitle(tt.tabName, tt.paneTitle))
44		})
45	}
46}
47
48func TestPaneTerminalTitleGenerationChangesOnlyWhenTitleChanges(t *testing.T) {
49	p := newPane("pane", nil, domain.Size{Cols: 20, Rows: 5})
50	p.mu.Lock()
51	p.screen.Write([]byte("\x1b]2;project/main.go\a"))
52	p.refreshTerminalTitleLocked()
53	first := p.title.generation
54	p.refreshTerminalTitleLocked()
55	second := p.title.generation
56	p.screen.Write([]byte("\x1b]0;other\a"))
57	p.refreshTerminalTitleLocked()
58	third := p.title.generation
59	p.mu.Unlock()
60
61	require.Equal(t, uint64(1), first)
62	require.Equal(t, first, second)
63	require.Equal(t, first+1, third)
64}
65
66func TestRefreshPaneTitleUsesForegroundProcessComm(t *testing.T) {
67	pty := portsmocks.NewMockPTY(t)
68	pty.EXPECT().ForegroundPgid().Return(1234, nil).Twice()
69	_, sess, _, _ := newManualSessionWithPTYs(t, pty)
70	clk := portsmocks.NewMockClock(t)
71	clk.EXPECT().Now().Return(time.Time{}).Maybe()
72	d := newTestDaemon(t, nil, clk)
73	d.shell = "/bin/zsh"
74	d.procComm = func(pid int) (string, error) {
75		require.Equal(t, 1234, pid)
76		return "vim\n", nil
77	}
78
79	title := d.refreshPaneTitle(sess, "pane-1")
80	require.Equal(t, "vim", title)
81
82	p := testAttachmentTab(sess).focusedPane()
83	p.mu.Lock()
84	p.screen.Write([]byte("\x1b]2;project/main.go\a"))
85	p.refreshTerminalTitleLocked()
86	p.mu.Unlock()
87	title = d.refreshPaneDisplayTitle(sess, p, true)
88
89	require.Equal(t, "vim: project/main.go", title)
90	require.Equal(t, "vim", p.title.processName)
91}
92
93func TestRefreshPaneTitleUsesProvidedOwningTab(t *testing.T) {
94	secondPTY := portsmocks.NewMockPTY(t)
95	secondPTY.EXPECT().ForegroundPgid().Return(222, nil).Once()
96	_, sess, _, _ := newManualSessionWithPTYs(t, nil)
97	second := newTabWithStableID("second-tab", "second-pane", secondPTY, domain.Size{Cols: 80, Rows: 23})
98	sess.mu.Lock()
99	sess.tabs = append(sess.tabs, second)
100	sess.mu.Unlock()
101	d := newTestDaemon(t, nil, stubClock{})
102	d.procComm = func(pid int) (string, error) {
103		require.Equal(t, 222, pid)
104		return "vim", nil
105	}
106
107	require.Equal(t, "vim", d.refreshPaneTitle(sess, "pane-1", second))
108	secondPane := second.panes["pane-1"]
109	secondPane.mu.Lock()
110	require.Equal(t, "vim", secondPane.title.processName)
111	secondPane.mu.Unlock()
112}
113
114func TestRefreshPaneTitleCachesByTTLAndRefreshesOnFocus(t *testing.T) {
115	pty := portsmocks.NewMockPTY(t)
116	pty.EXPECT().ForegroundPgid().Return(1234, nil).Twice()
117	_, sess, _, _ := newManualSessionWithPTYs(t, pty)
118	clk := portsmocks.NewMockClock(t)
119	now := time.Unix(10, 0)
120	clk.EXPECT().Now().RunAndReturn(func() time.Time { return now }).Maybe()
121	d := newTestDaemon(t, nil, clk)
122	var calls atomic.Int32
123	d.procComm = func(pid int) (string, error) {
124		require.Equal(t, 1234, pid)
125		calls.Add(1)
126		return "vim\n", nil
127	}
128
129	require.Equal(t, "vim", d.refreshPaneTitle(sess, "pane-1"))
130	now = now.Add(paneTitleCacheTTL / 2)
131	require.Equal(t, "vim", d.refreshPaneTitle(sess, "pane-1"))
132	require.Equal(t, int32(1), calls.Load(), "paint refresh inside TTL should use cached title")
133	require.Equal(t, "vim", d.refreshPaneTitleOnFocus(sess, "pane-1"))
134	require.Equal(t, int32(2), calls.Load(), "focus refresh should bypass TTL")
135}
136
137func TestRefreshFloatingPaneTitlePreservesConfiguredCommandFallback(t *testing.T) {
138	pty := portsmocks.NewMockPTY(t)
139	pty.EXPECT().ForegroundPgid().Return(0, errors.New("no foreground process")).Twice()
140	clk := portsmocks.NewMockClock(t)
141	clk.EXPECT().Now().Return(time.Time{}).Maybe()
142	d := newTestDaemon(t, nil, clk)
143	d.shell = "/usr/bin/fish"
144	d.procComm = func(int) (string, error) { return "", errors.New("unused") }
145	p := newPane("floating", pty, domain.Size{Cols: 20, Rows: 5})
146	p.title.displayFallback = floatingCommandFallback("btop --utf", d.shell)
147
148	require.Equal(t, "btop", d.refreshPaneDisplayTitle(nil, p, false))
149	require.Equal(t, "btop", d.refreshPaneDisplayTitle(nil, p, false), "cached refresh must retain the pane-owned fallback")
150	require.Equal(t, "btop", d.refreshPaneDisplayTitle(nil, p, true), "process refresh must retain the pane-owned fallback")
151	p.mu.Lock()
152	require.Equal(t, "btop", p.title.displayFallback)
153	p.mu.Unlock()
154}
155
156func TestRefreshFloatingPaneTitleUsesShellFallbackForEmptyCommand(t *testing.T) {
157	d := newTestDaemon(t, nil, stubClock{})
158	d.shell = "/usr/local/bin/zsh"
159	p := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})
160	p.title.displayFallback = floatingCommandFallback("", d.shell)
161
162	require.Equal(t, "zsh", d.refreshPaneDisplayTitle(nil, p, true))
163}
164
165func TestRefreshPaneTitleRetainsNormalPaneShellFallback(t *testing.T) {
166	d, sess, _, _ := newManualSessionWithPTYs(t, nil)
167	p := testAttachmentTab(sess).focusedPane()
168	p.setDisplayFallback("fish")
169
170	d.shell = "/bin/zsh"
171	require.Equal(t, "fish", d.refreshPaneTitle(sess, "pane-1"))
172	p.mu.Lock()
173	firstGeneration := p.title.generation
174	p.mu.Unlock()
175	require.Equal(t, "fish", d.refreshPaneTitle(sess, "pane-1"))
176	p.mu.Lock()
177	require.Equal(t, firstGeneration, p.title.generation, "a title refresh must preserve the pane-owned shell fallback")
178	p.mu.Unlock()
179}
180
181func TestRefreshPaneTitleLookupFailureKeepsProcessNameEmpty(t *testing.T) {
182	pty := portsmocks.NewMockPTY(t)
183	pty.EXPECT().ForegroundPgid().Return(0, errors.New("no foreground process")).Twice()
184	_, sess, _, _ := newManualSessionWithPTYs(t, pty)
185	clk := portsmocks.NewMockClock(t)
186	clk.EXPECT().Now().Return(time.Time{}).Maybe()
187	d := newTestDaemon(t, nil, clk)
188	d.shell = "/usr/bin/fish"
189	d.procComm = func(int) (string, error) { return "", errors.New("unused") }
190	p := testAttachmentTab(sess).focusedPane()
191
192	p.mu.Lock()
193	p.screen.Write([]byte("\x1b]2;terminal\a"))
194	p.refreshTerminalTitleLocked()
195	p.mu.Unlock()
196	require.Equal(t, "terminal", d.refreshPaneDisplayTitle(sess, p, true))
197	p.mu.Lock()
198	require.Empty(t, p.title.processName, "a lookup failure must not become process state")
199	p.mu.Unlock()
200
201	p.mu.Lock()
202	p.screen.Write([]byte("\x1b]2;\a"))
203	p.refreshTerminalTitleLocked()
204	p.mu.Unlock()
205	require.Equal(t, "sh", d.refreshPaneDisplayTitle(sess, p, true), "pointer refresh must retain its pane-owned fallback")
206}

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