detached 1169241

test(daemon): cover floating activation paths

commit
116924109462c4cecc13292b163c376e5f3649ec
author
brice · · 2026-07-10 10:58 UTC
parent
bc8142c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +126 -0
M internal/usecase/daemon/attention_test.go +5 -0
@@ -440,6 +440,10 @@ func TestCloseRingingTabRefreshesOtherSessionBottomBar(t *testing.T) {
440440
441441 func TestAltAJumpAttentionSelectsOldestLocalTab(t *testing.T) {
442442 d, sess, ac, sends, releases := newManualTabSession(t, 3)
443+ sess.mu.Lock()
444+ sess.client = ac
445+ sess.mu.Unlock()
446+ d.ptys = newBlockingOpenFactory(t, d)
443447 defer releases[0]()
444448 defer releases[1]()
445449 defer releases[2]()
@@ -454,6 +458,7 @@ func TestAltAJumpAttentionSelectsOldestLocalTab(t *testing.T) {
454458 d.handleInput(sess, ac, []byte("\x1ba"))
455459
456460 require.Equal(t, 2, activeTabIndex(sess))
461+ requireFloatingInitialized(t, sess.activeTab())
457462 _ = mustOutputData(t, sends)
458463 }
459464
@@ -440,6 +440,10 @@ func TestCloseRingingTabRefreshesOtherSessionBottomBar(t *testing.T) {
440 440
441 func TestAltAJumpAttentionSelectsOldestLocalTab(t *testing.T) {441 func TestAltAJumpAttentionSelectsOldestLocalTab(t *testing.T) {
442 d, sess, ac, sends, releases := newManualTabSession(t, 3)442 d, sess, ac, sends, releases := newManualTabSession(t, 3)
443+ sess.mu.Lock()
444+ sess.client = ac
445+ sess.mu.Unlock()
446+ d.ptys = newBlockingOpenFactory(t, d)
443 defer releases[0]()447 defer releases[0]()
444 defer releases[1]()448 defer releases[1]()
445 defer releases[2]()449 defer releases[2]()
@@ -454,6 +458,7 @@ func TestAltAJumpAttentionSelectsOldestLocalTab(t *testing.T) {
454 d.handleInput(sess, ac, []byte("\x1ba"))458 d.handleInput(sess, ac, []byte("\x1ba"))
455 459
456 require.Equal(t, 2, activeTabIndex(sess))460 require.Equal(t, 2, activeTabIndex(sess))
461+ requireFloatingInitialized(t, sess.activeTab())
457 _ = mustOutputData(t, sends)462 _ = mustOutputData(t, sends)
458 }463 }
459 464
M internal/usecase/daemon/daemon_test.go +22 -0
@@ -125,6 +125,28 @@ func (*quietPTY) Resize(domain.Size) error { return nil }
125125 func (*quietPTY) Pid() int { return 0 }
126126 func (*quietPTY) ForegroundPgid() (int, error) { return 0, nil }
127127
128+// blockingOpenFactory keeps activation tests deterministically in Warming
129+// without starting pane reader/scheduler goroutines.
130+type blockingOpenFactory struct {
131+ release chan struct{}
132+ once sync.Once
133+}
134+
135+func newBlockingOpenFactory(t *testing.T, d *Daemon) *blockingOpenFactory {
136+ t.Helper()
137+ f := &blockingOpenFactory{release: make(chan struct{})}
138+ t.Cleanup(func() {
139+ f.once.Do(func() { close(f.release) })
140+ d.sessWg.Wait()
141+ })
142+ return f
143+}
144+
145+func (f *blockingOpenFactory) Open(string, []string, []string, string, domain.Size) (ports.PTY, error) {
146+ <-f.release
147+ return nil, io.ErrClosedPipe
148+}
149+
128150 // newFactory keeps the supplied fixture for normal panes. Floating prewarms
129151 // have a distinct (smaller) geometry and receive independent quiet PTYs, so
130152 // they cannot consume test output or add post-test mock calls.
@@ -125,6 +125,28 @@ func (*quietPTY) Resize(domain.Size) error { return nil }
125 func (*quietPTY) Pid() int { return 0 }125 func (*quietPTY) Pid() int { return 0 }
126 func (*quietPTY) ForegroundPgid() (int, error) { return 0, nil }126 func (*quietPTY) ForegroundPgid() (int, error) { return 0, nil }
127 127
128+// blockingOpenFactory keeps activation tests deterministically in Warming
129+// without starting pane reader/scheduler goroutines.
130+type blockingOpenFactory struct {
131+ release chan struct{}
132+ once sync.Once
133+}
134+
135+func newBlockingOpenFactory(t *testing.T, d *Daemon) *blockingOpenFactory {
136+ t.Helper()
137+ f := &blockingOpenFactory{release: make(chan struct{})}
138+ t.Cleanup(func() {
139+ f.once.Do(func() { close(f.release) })
140+ d.sessWg.Wait()
141+ })
142+ return f
143+}
144+
145+func (f *blockingOpenFactory) Open(string, []string, []string, string, domain.Size) (ports.PTY, error) {
146+ <-f.release
147+ return nil, io.ErrClosedPipe
148+}
149+
128 // newFactory keeps the supplied fixture for normal panes. Floating prewarms150 // newFactory keeps the supplied fixture for normal panes. Floating prewarms
129 // have a distinct (smaller) geometry and receive independent quiet PTYs, so151 // have a distinct (smaller) geometry and receive independent quiet PTYs, so
130 // they cannot consume test output or add post-test mock calls.152 // they cannot consume test output or add post-test mock calls.
M internal/usecase/daemon/input_test.go +44 -0
@@ -62,6 +62,22 @@ func TestAltDigitSwitchesBetweenThreeTabs(t *testing.T) {
6262 require.Eventually(t, func() bool { return len(writes3) == 1 }, 2*time.Second, 5*time.Millisecond)
6363 require.Equal(t, []byte("C"), <-writes3)
6464
65+ d.mu.Lock()
66+ var sess *session
67+ for _, candidate := range d.sessions {
68+ sess = candidate
69+ break
70+ }
71+ d.mu.Unlock()
72+ require.NotNil(t, sess)
73+ sess.mu.Lock()
74+ tabs := append([]*tab(nil), sess.tabs...)
75+ sess.mu.Unlock()
76+ require.Len(t, tabs, 3)
77+ for _, tb := range tabs {
78+ requireFloatingInitialized(t, tb)
79+ }
80+
6581 releaseConn()
6682 releasePTY1()
6783 releasePTY2()
@@ -81,6 +97,13 @@ func TestAltCForwardsToPTY(t *testing.T) {
8197 releasePTY()
8298 }
8399
100+func requireFloatingInitialized(t *testing.T, tb *tab) {
101+ t.Helper()
102+ tb.mu.Lock()
103+ defer tb.mu.Unlock()
104+ require.NotEqual(t, floatingUninitialized, tb.floating.state)
105+}
106+
84107 func installTestFloating(tb *tab, p *pane, visible bool) {
85108 tb.mu.Lock()
86109 defer tb.mu.Unlock()
@@ -167,6 +190,27 @@ func TestFloatingStaysTerminalTargetAfterDirectionalFocus(t *testing.T) {
167190 requirePTYWrite(t, floatingWrites, []byte("x"))
168191 }
169192
193+func TestFloatingVisibilityRemainsIndependentAcrossTabSwitches(t *testing.T) {
194+ d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
195+ first := sess.activeTab()
196+ second := newTab(nil, first.size)
197+ installTestFloating(first, newPane("floating", nil, domain.Size{Cols: 20, Rows: 5}), true)
198+ installTestFloating(second, newPane("floating", nil, domain.Size{Cols: 20, Rows: 5}), false)
199+ sess.mu.Lock()
200+ sess.tabs = append(sess.tabs, second)
201+ sess.mu.Unlock()
202+
203+ daemonKeyHandler{d: d, ac: ac}.Action(keys.ActionSwitchTab2)
204+ second.mu.Lock()
205+ require.Equal(t, floatingHidden, second.floating.state)
206+ second.mu.Unlock()
207+
208+ daemonKeyHandler{d: d, ac: ac}.Action(keys.ActionSwitchTab1)
209+ first.mu.Lock()
210+ require.Equal(t, floatingVisible, first.floating.state)
211+ first.mu.Unlock()
212+}
213+
170214 func TestFloatingMouseTranslatesSGRToInnerCoordinates(t *testing.T) {
171215 normal := portsmocks.NewMockPTY(t)
172216 floatingPTY := portsmocks.NewMockPTY(t)
@@ -62,6 +62,22 @@ func TestAltDigitSwitchesBetweenThreeTabs(t *testing.T) {
62 require.Eventually(t, func() bool { return len(writes3) == 1 }, 2*time.Second, 5*time.Millisecond)62 require.Eventually(t, func() bool { return len(writes3) == 1 }, 2*time.Second, 5*time.Millisecond)
63 require.Equal(t, []byte("C"), <-writes3)63 require.Equal(t, []byte("C"), <-writes3)
64 64
65+ d.mu.Lock()
66+ var sess *session
67+ for _, candidate := range d.sessions {
68+ sess = candidate
69+ break
70+ }
71+ d.mu.Unlock()
72+ require.NotNil(t, sess)
73+ sess.mu.Lock()
74+ tabs := append([]*tab(nil), sess.tabs...)
75+ sess.mu.Unlock()
76+ require.Len(t, tabs, 3)
77+ for _, tb := range tabs {
78+ requireFloatingInitialized(t, tb)
79+ }
80+
65 releaseConn()81 releaseConn()
66 releasePTY1()82 releasePTY1()
67 releasePTY2()83 releasePTY2()
@@ -81,6 +97,13 @@ func TestAltCForwardsToPTY(t *testing.T) {
81 releasePTY()97 releasePTY()
82 }98 }
83 99
100+func requireFloatingInitialized(t *testing.T, tb *tab) {
101+ t.Helper()
102+ tb.mu.Lock()
103+ defer tb.mu.Unlock()
104+ require.NotEqual(t, floatingUninitialized, tb.floating.state)
105+}
106+
84 func installTestFloating(tb *tab, p *pane, visible bool) {107 func installTestFloating(tb *tab, p *pane, visible bool) {
85 tb.mu.Lock()108 tb.mu.Lock()
86 defer tb.mu.Unlock()109 defer tb.mu.Unlock()
@@ -167,6 +190,27 @@ func TestFloatingStaysTerminalTargetAfterDirectionalFocus(t *testing.T) {
167 requirePTYWrite(t, floatingWrites, []byte("x"))190 requirePTYWrite(t, floatingWrites, []byte("x"))
168 }191 }
169 192
193+func TestFloatingVisibilityRemainsIndependentAcrossTabSwitches(t *testing.T) {
194+ d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
195+ first := sess.activeTab()
196+ second := newTab(nil, first.size)
197+ installTestFloating(first, newPane("floating", nil, domain.Size{Cols: 20, Rows: 5}), true)
198+ installTestFloating(second, newPane("floating", nil, domain.Size{Cols: 20, Rows: 5}), false)
199+ sess.mu.Lock()
200+ sess.tabs = append(sess.tabs, second)
201+ sess.mu.Unlock()
202+
203+ daemonKeyHandler{d: d, ac: ac}.Action(keys.ActionSwitchTab2)
204+ second.mu.Lock()
205+ require.Equal(t, floatingHidden, second.floating.state)
206+ second.mu.Unlock()
207+
208+ daemonKeyHandler{d: d, ac: ac}.Action(keys.ActionSwitchTab1)
209+ first.mu.Lock()
210+ require.Equal(t, floatingVisible, first.floating.state)
211+ first.mu.Unlock()
212+}
213+
170 func TestFloatingMouseTranslatesSGRToInnerCoordinates(t *testing.T) {214 func TestFloatingMouseTranslatesSGRToInnerCoordinates(t *testing.T) {
171 normal := portsmocks.NewMockPTY(t)215 normal := portsmocks.NewMockPTY(t)
172 floatingPTY := portsmocks.NewMockPTY(t)216 floatingPTY := portsmocks.NewMockPTY(t)
M internal/usecase/daemon/palette_test.go +26 -0
@@ -18,6 +18,10 @@ func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) {
1818 p1, release1 := newBlockingPTY(t)
1919 p2, release2 := newBlockingPTY(t)
2020 d, sess, ac, sends := newManualSessionWithPTYs(t, p1, p2)
21+ sess.mu.Lock()
22+ sess.client = ac
23+ sess.mu.Unlock()
24+ d.ptys = newBlockingOpenFactory(t, d)
2125 defer release1()
2226 defer release2()
2327
@@ -31,6 +35,7 @@ func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) {
3135 d.handleInput(sess, ac, []byte("NXT\r"))
3236 require.False(t, ac.overlays.paletteActive())
3337 require.Equal(t, 1, activeTabIndex(sess))
38+ requireFloatingInitialized(t, sess.activeTab())
3439 awaitFrame(t, sends, ports.MsgOutput)
3540
3641 d.handleInput(sess, ac, []byte("\x1b "))
@@ -40,6 +45,24 @@ func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) {
4045 awaitFrame(t, sends, ports.MsgOutput)
4146 }
4247
48+func TestPaletteFLTExecutesFloatingToggle(t *testing.T) {
49+ p, release := newBlockingPTY(t)
50+ defer release()
51+ d, sess, ac, sends := newManualSessionWithPTYs(t, p)
52+ tb := sess.activeTab()
53+ installTestFloating(tb, newPane("floating", nil, domain.Size{Cols: 20, Rows: 5}), false)
54+
55+ d.handleInput(sess, ac, []byte("\x1b "))
56+ awaitFrame(t, sends, ports.MsgOutput)
57+ d.handleInput(sess, ac, []byte("FLT\r"))
58+
59+ require.False(t, ac.overlays.paletteActive())
60+ tb.mu.Lock()
61+ require.Equal(t, floatingVisible, tb.floating.state)
62+ tb.mu.Unlock()
63+ awaitFrame(t, sends, ports.MsgOutput)
64+}
65+
4366 func TestPaletteCNSPromptsForSessionNameThenCreatesAndSwitches(t *testing.T) {
4467 p1, release1 := newBlockingPTY(t)
4568 p2, release2 := newBlockingPTY(t)
@@ -81,6 +104,9 @@ func TestPaletteCNSPromptsForSessionNameThenCreatesAndSwitches(t *testing.T) {
81104
82105 func TestPaletteReopensWithSuccessfulCommandFirst(t *testing.T) {
83106 d, sess, ac, sends, releases := newManualTabSession(t, 2)
107+ sess.mu.Lock()
108+ sess.client = ac
109+ sess.mu.Unlock()
84110 defer releases[0]()
85111 defer releases[1]()
86112
@@ -18,6 +18,10 @@ func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) {
18 p1, release1 := newBlockingPTY(t)18 p1, release1 := newBlockingPTY(t)
19 p2, release2 := newBlockingPTY(t)19 p2, release2 := newBlockingPTY(t)
20 d, sess, ac, sends := newManualSessionWithPTYs(t, p1, p2)20 d, sess, ac, sends := newManualSessionWithPTYs(t, p1, p2)
21+ sess.mu.Lock()
22+ sess.client = ac
23+ sess.mu.Unlock()
24+ d.ptys = newBlockingOpenFactory(t, d)
21 defer release1()25 defer release1()
22 defer release2()26 defer release2()
23 27
@@ -31,6 +35,7 @@ func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) {
31 d.handleInput(sess, ac, []byte("NXT\r"))35 d.handleInput(sess, ac, []byte("NXT\r"))
32 require.False(t, ac.overlays.paletteActive())36 require.False(t, ac.overlays.paletteActive())
33 require.Equal(t, 1, activeTabIndex(sess))37 require.Equal(t, 1, activeTabIndex(sess))
38+ requireFloatingInitialized(t, sess.activeTab())
34 awaitFrame(t, sends, ports.MsgOutput)39 awaitFrame(t, sends, ports.MsgOutput)
35 40
36 d.handleInput(sess, ac, []byte("\x1b "))41 d.handleInput(sess, ac, []byte("\x1b "))
@@ -40,6 +45,24 @@ func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) {
40 awaitFrame(t, sends, ports.MsgOutput)45 awaitFrame(t, sends, ports.MsgOutput)
41 }46 }
42 47
48+func TestPaletteFLTExecutesFloatingToggle(t *testing.T) {
49+ p, release := newBlockingPTY(t)
50+ defer release()
51+ d, sess, ac, sends := newManualSessionWithPTYs(t, p)
52+ tb := sess.activeTab()
53+ installTestFloating(tb, newPane("floating", nil, domain.Size{Cols: 20, Rows: 5}), false)
54+
55+ d.handleInput(sess, ac, []byte("\x1b "))
56+ awaitFrame(t, sends, ports.MsgOutput)
57+ d.handleInput(sess, ac, []byte("FLT\r"))
58+
59+ require.False(t, ac.overlays.paletteActive())
60+ tb.mu.Lock()
61+ require.Equal(t, floatingVisible, tb.floating.state)
62+ tb.mu.Unlock()
63+ awaitFrame(t, sends, ports.MsgOutput)
64+}
65+
43 func TestPaletteCNSPromptsForSessionNameThenCreatesAndSwitches(t *testing.T) {66 func TestPaletteCNSPromptsForSessionNameThenCreatesAndSwitches(t *testing.T) {
44 p1, release1 := newBlockingPTY(t)67 p1, release1 := newBlockingPTY(t)
45 p2, release2 := newBlockingPTY(t)68 p2, release2 := newBlockingPTY(t)
@@ -81,6 +104,9 @@ func TestPaletteCNSPromptsForSessionNameThenCreatesAndSwitches(t *testing.T) {
81 104
82 func TestPaletteReopensWithSuccessfulCommandFirst(t *testing.T) {105 func TestPaletteReopensWithSuccessfulCommandFirst(t *testing.T) {
83 d, sess, ac, sends, releases := newManualTabSession(t, 2)106 d, sess, ac, sends, releases := newManualTabSession(t, 2)
107+ sess.mu.Lock()
108+ sess.client = ac
109+ sess.mu.Unlock()
84 defer releases[0]()110 defer releases[0]()
85 defer releases[1]()111 defer releases[1]()
86 112
M internal/usecase/daemon/picker_test.go +5 -0
@@ -90,6 +90,10 @@ func TestPickerResumesStoppedSessionWithPersistedTabNames(t *testing.T) {
9090
9191 func TestPickerSameSessionNavigationSwitchAndEscClose(t *testing.T) {
9292 d, sess, ac, sends, releases := newManualTabSession(t, 2)
93+ sess.mu.Lock()
94+ sess.client = ac
95+ sess.mu.Unlock()
96+ d.ptys = newBlockingOpenFactory(t, d)
9397 defer func() {
9498 for _, release := range releases {
9599 release()
@@ -103,6 +107,7 @@ func TestPickerSameSessionNavigationSwitchAndEscClose(t *testing.T) {
103107 d.handleInput(sess, ac, []byte("\r"))
104108
105109 require.Equal(t, 1, activeTabIndex(sess))
110+ requireFloatingInitialized(t, sess.activeTab())
106111 awaitFrame(t, sends, ports.MsgOutput)
107112 d.enterPicker(sess, ac)
108113 awaitFrame(t, sends, ports.MsgOutput)
@@ -90,6 +90,10 @@ func TestPickerResumesStoppedSessionWithPersistedTabNames(t *testing.T) {
90 90
91 func TestPickerSameSessionNavigationSwitchAndEscClose(t *testing.T) {91 func TestPickerSameSessionNavigationSwitchAndEscClose(t *testing.T) {
92 d, sess, ac, sends, releases := newManualTabSession(t, 2)92 d, sess, ac, sends, releases := newManualTabSession(t, 2)
93+ sess.mu.Lock()
94+ sess.client = ac
95+ sess.mu.Unlock()
96+ d.ptys = newBlockingOpenFactory(t, d)
93 defer func() {97 defer func() {
94 for _, release := range releases {98 for _, release := range releases {
95 release()99 release()
@@ -103,6 +107,7 @@ func TestPickerSameSessionNavigationSwitchAndEscClose(t *testing.T) {
103 d.handleInput(sess, ac, []byte("\r"))107 d.handleInput(sess, ac, []byte("\r"))
104 108
105 require.Equal(t, 1, activeTabIndex(sess))109 require.Equal(t, 1, activeTabIndex(sess))
110+ requireFloatingInitialized(t, sess.activeTab())
106 awaitFrame(t, sends, ports.MsgOutput)111 awaitFrame(t, sends, ports.MsgOutput)
107 d.enterPicker(sess, ac)112 d.enterPicker(sess, ac)
108 awaitFrame(t, sends, ports.MsgOutput)113 awaitFrame(t, sends, ports.MsgOutput)
M internal/usecase/daemon/session_test.go +24 -0
@@ -67,6 +67,7 @@ func TestRoutePropagatesHelloCwdAndTabsInheritIt(t *testing.T) {
6767
6868 require.NoError(t, d.createTab(sess, sz))
6969 require.Equal(t, []string{"/tmp/work", "/tmp/work"}, dirs)
70+ requireFloatingInitialized(t, sess.activeTab())
7071 _ = d.killSession(sess, ports.ReasonSessionKilled, false)
7172 releaseFirst()
7273 releaseSecond()
@@ -909,6 +910,29 @@ func TestTabNamePersistenceTracksTabIndexShifts(t *testing.T) {
909910 }
910911 }
911912
913+func TestCloseActiveTabActivatesDestinationFloatingPane(t *testing.T) {
914+ d, sess, ac, _, releases := newManualTabSession(t, 2)
915+ sess.mu.Lock()
916+ sess.client = ac
917+ sess.mu.Unlock()
918+ d.ptys = newBlockingOpenFactory(t, d)
919+ defer releases[0]()
920+ defer releases[1]()
921+ sess.mu.Lock()
922+ first, closing := sess.tabs[0], sess.tabs[1]
923+ sess.active = 1
924+ sess.mu.Unlock()
925+ first.mu.Lock()
926+ stale := first.takeFloatingLocked()
927+ first.mu.Unlock()
928+ closeFloatingPane(stale)
929+
930+ d.closeTab(sess, closing, false)
931+
932+ require.Same(t, first, sess.activeTab())
933+ requireFloatingInitialized(t, first)
934+}
935+
912936 func TestRenameTabDoesNotPersistForEphemeralSession(t *testing.T) {
913937 sz := domain.Size{Cols: 80, Rows: 24}
914938 p, release := newBlockingPTY(t)
@@ -67,6 +67,7 @@ func TestRoutePropagatesHelloCwdAndTabsInheritIt(t *testing.T) {
67 67
68 require.NoError(t, d.createTab(sess, sz))68 require.NoError(t, d.createTab(sess, sz))
69 require.Equal(t, []string{"/tmp/work", "/tmp/work"}, dirs)69 require.Equal(t, []string{"/tmp/work", "/tmp/work"}, dirs)
70+ requireFloatingInitialized(t, sess.activeTab())
70 _ = d.killSession(sess, ports.ReasonSessionKilled, false)71 _ = d.killSession(sess, ports.ReasonSessionKilled, false)
71 releaseFirst()72 releaseFirst()
72 releaseSecond()73 releaseSecond()
@@ -909,6 +910,29 @@ func TestTabNamePersistenceTracksTabIndexShifts(t *testing.T) {
909 }910 }
910 }911 }
911 912
913+func TestCloseActiveTabActivatesDestinationFloatingPane(t *testing.T) {
914+ d, sess, ac, _, releases := newManualTabSession(t, 2)
915+ sess.mu.Lock()
916+ sess.client = ac
917+ sess.mu.Unlock()
918+ d.ptys = newBlockingOpenFactory(t, d)
919+ defer releases[0]()
920+ defer releases[1]()
921+ sess.mu.Lock()
922+ first, closing := sess.tabs[0], sess.tabs[1]
923+ sess.active = 1
924+ sess.mu.Unlock()
925+ first.mu.Lock()
926+ stale := first.takeFloatingLocked()
927+ first.mu.Unlock()
928+ closeFloatingPane(stale)
929+
930+ d.closeTab(sess, closing, false)
931+
932+ require.Same(t, first, sess.activeTab())
933+ requireFloatingInitialized(t, first)
934+}
935+
912 func TestRenameTabDoesNotPersistForEphemeralSession(t *testing.T) {936 func TestRenameTabDoesNotPersistForEphemeralSession(t *testing.T) {
913 sz := domain.Size{Cols: 80, Rows: 24}937 sz := domain.Size{Cols: 80, Rows: 24}
914 p, release := newBlockingPTY(t)938 p, release := newBlockingPTY(t)

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 2

Tags 13

↑↓ moveenter openesc close