detached 59ce3a4

refactor(daemon): simplify floating pane launch

commit
59ce3a48690bf457646745de67c701321aa79019
author
brice · · 2026-07-10 05:47 UTC
parent
5fdd7fe
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +92 -65
M internal/usecase/daemon/floating.go +75 -46
@@ -29,32 +29,30 @@ type floatingSlot struct {
2929 pane *pane
3030 desiredVisible bool
3131 generation uint64
32- launch domain.FloatingConfig
3332 }
3433
3534 // beginFloatingWarmLocked starts a single background launch. The caller must
3635 // hold tb.mu and must only call it for an uninitialized slot.
37-func (tb *tab) beginFloatingWarmLocked(launch domain.FloatingConfig, desiredVisible bool) uint64 {
36+func (tb *tab) beginFloatingWarmLocked(desiredVisible bool) uint64 {
3837 if tb == nil || tb.floating.state != floatingUninitialized {
3938 return 0
4039 }
4140 tb.floating.generation++
4241 tb.floating.state = floatingWarming
4342 tb.floating.desiredVisible = desiredVisible
44- tb.floating.launch = launch
4543 return tb.floating.generation
4644 }
4745
4846 // toggleFloatingLocked applies the user action. Its result says whether a new
4947 // PTY launch is required and returns the generation associated with that launch
5048 // or current slot.
51-func (tb *tab) toggleFloatingLocked(launch domain.FloatingConfig) (bool, uint64) {
49+func (tb *tab) toggleFloatingLocked() (bool, uint64) {
5250 if tb == nil {
5351 return false, 0
5452 }
5553 switch tb.floating.state {
5654 case floatingUninitialized:
57- generation := tb.beginFloatingWarmLocked(launch, true)
55+ generation := tb.beginFloatingWarmLocked(true)
5856 return generation != 0, generation
5957 case floatingWarming:
6058 tb.floating.desiredVisible = !tb.floating.desiredVisible
@@ -90,7 +88,6 @@ func (tb *tab) failFloatingLocked(generation uint64) bool {
9088 tb.floating.state = floatingUninitialized
9189 tb.floating.pane = nil
9290 tb.floating.desiredVisible = false
93- tb.floating.launch = domain.FloatingConfig{}
9491 return true
9592 }
9693
@@ -116,7 +113,6 @@ func (tb *tab) takeFloatingLocked() *pane {
116113 tb.floating.state = floatingUninitialized
117114 tb.floating.pane = nil
118115 tb.floating.desiredVisible = false
119- tb.floating.launch = domain.FloatingConfig{}
120116 return p
121117 }
122118
@@ -172,7 +168,7 @@ func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error {
172168 }
173169 cfg := d.currentFloatingConfig()
174170 tb.mu.Lock()
175- start, generation := tb.toggleFloatingLocked(cfg)
171+ start, generation := tb.toggleFloatingLocked()
176172 visible := tb.floating.desiredVisible
177173 tb.mu.Unlock()
178174 if start {
@@ -189,28 +185,53 @@ func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error {
189185 func (d *Daemon) startFloating(sess *session, tb *tab, visible bool) {
190186 cfg := d.currentFloatingConfig()
191187 tb.mu.Lock()
192- generation := tb.beginFloatingWarmLocked(cfg, visible)
188+ generation := tb.beginFloatingWarmLocked(visible)
193189 tb.mu.Unlock()
194190 if generation != 0 {
195191 d.launchFloating(sess, tb, cfg, generation, visible)
196192 }
197193 }
198194
199-// launchFloating snapshots all launch inputs before starting the potentially
200-// blocking factory call. In particular, no PTY operation occurs while tb.mu is
201-// held, and config reloads cannot affect this launch.
195+// floatingLaunchSpec is an immutable snapshot of one launch. It is built
196+// before the worker starts so config reloads cannot affect that launch.
197+type floatingLaunchSpec struct {
198+ sessionName string
199+ cwd string
200+ size domain.Size
201+ paneStableID string
202+ env []string
203+ command string
204+ args []string
205+ fallback string
206+ parentCtx context.Context
207+ userOpen bool
208+}
209+
210+// launchFloating snapshots launch inputs, then accounts for a worker through
211+// Open, pane initialization, publication, and reader/scheduler startup.
202212 func (d *Daemon) launchFloating(sess *session, tb *tab, cfg domain.FloatingConfig, generation uint64, userOpen bool) {
213+ spec, err := d.newFloatingLaunchSpec(sess, tb, cfg, userOpen)
214+ if err != nil {
215+ d.failFloatingLaunch(tb, generation, userOpen, spec.sessionName, err)
216+ return
217+ }
218+ // Count the launch before its goroutine starts. It remains counted through
219+ // install, including the reader/scheduler Adds, so shutdown cannot return
220+ // before a late Open completion has either been rejected or fully joined.
221+ d.sessWg.Go(func() { d.openAndInstallFloating(sess, tb, spec, generation) })
222+}
223+
224+func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.FloatingConfig, userOpen bool) (floatingLaunchSpec, error) {
203225 sess.mu.Lock()
204- name, cwd, term := sess.name, sess.cwd, sess.terminal
226+ name, cwd, term, sessCtx := sess.name, sess.cwd, sess.terminal, sess.ctx
205227 sess.mu.Unlock()
206228 tb.mu.Lock()
207- sz := floatingInnerSize(tb.size, cfg)
229+ size := floatingInnerSize(tb.size, cfg)
208230 focused := tb.focusedPane()
209- tabStableID := tb.stableID
210- tabCtx := tb.ctx
231+ tabStableID, tabCtx := tb.stableID, tb.ctx
211232 tb.mu.Unlock()
212- if !sz.Valid() {
213- sz = domain.Size{Cols: 1, Rows: 1}
233+ if !size.Valid() {
234+ size = domain.Size{Cols: 1, Rows: 1}
214235 }
215236 if focused != nil && d.procCwd != nil && focused.pty != nil {
216237 if live, err := d.procCwd(focused.pty.Pid()); err == nil && live != "" {
@@ -222,39 +243,47 @@ func (d *Daemon) launchFloating(sess *session, tb *tab, cfg domain.FloatingConfi
222243 }
223244 paneStableID, err := newStableID("p")
224245 if err != nil {
225- d.failFloatingLaunch(tb, generation, userOpen, name, err)
226- return
246+ return floatingLaunchSpec{sessionName: name}, err
247+ }
248+ if tabCtx == nil {
249+ tabCtx = sessCtx
250+ }
251+ if tabCtx == nil {
252+ tabCtx = context.Background()
227253 }
228- env := d.childEnv(name, tabStableID, paneStableID, term)
229254 command, args := d.shell, append([]string(nil), d.shellArgs...)
230255 if cfg.Command != "" {
231256 args = []string{"-lc", cfg.Command}
232257 }
233- fallback := floatingCommandFallback(cfg.Command, d.shell)
234- // Count the launch before its goroutine starts. It remains counted through
235- // install, including the reader/scheduler Adds, so shutdown cannot return
236- // before a late Open completion has either been rejected or fully joined.
237- d.sessWg.Go(func() {
238- pty, openErr := d.ptys.Open(command, args, env, cwd, sz)
239- if openErr != nil {
240- d.failFloatingLaunch(tb, generation, userOpen, name, openErr)
241- return
242- }
243- p := newPaneWithStableID(layout.PaneID("floating"), paneStableID, pty, sz)
244- p.title.displayFallback = fallback
245- paneCtx := tabCtx
246- if paneCtx == nil {
247- paneCtx = sess.ctx
248- }
249- if paneCtx == nil {
250- paneCtx = context.Background()
251- }
252- p.ctx, p.cancel = context.WithCancel(paneCtx)
253- // The reader may run as soon as install returns; make its exit policy
254- // immutable before publishing the pane to the slot.
255- p.onExit = func() { d.reapFloating(sess, tb, p, generation) }
256- d.installFloating(sess, tb, p, generation)
257- })
258+ return floatingLaunchSpec{
259+ sessionName: name,
260+ cwd: cwd,
261+ size: size,
262+ paneStableID: paneStableID,
263+ env: d.childEnv(name, tabStableID, paneStableID, term),
264+ command: command,
265+ args: args,
266+ fallback: floatingCommandFallback(cfg.Command, d.shell),
267+ parentCtx: tabCtx,
268+ userOpen: userOpen,
269+ }, nil
270+}
271+
272+// openAndInstallFloating runs entirely in the launch worker. No PTY operation
273+// occurs under tb.mu; the pane is fully initialized before publication.
274+func (d *Daemon) openAndInstallFloating(sess *session, tb *tab, spec floatingLaunchSpec, generation uint64) {
275+ pty, err := d.ptys.Open(spec.command, spec.args, spec.env, spec.cwd, spec.size)
276+ if err != nil {
277+ d.failFloatingLaunch(tb, generation, spec.userOpen, spec.sessionName, err)
278+ return
279+ }
280+ p := newPaneWithStableID(layout.PaneID("floating"), spec.paneStableID, pty, spec.size)
281+ p.title.displayFallback = spec.fallback
282+ p.ctx, p.cancel = context.WithCancel(spec.parentCtx)
283+ // The reader may run as soon as install returns; make its exit policy
284+ // immutable before publishing the pane to the slot.
285+ p.onExit = func() { d.reapFloating(sess, tb, p, generation) }
286+ d.installFloating(sess, tb, p, generation)
258287 }
259288
260289 func (d *Daemon) failFloatingLaunch(tb *tab, generation uint64, userOpen bool, sessionName string, err error) {
@@ -29,32 +29,30 @@ type floatingSlot struct {
29 pane *pane29 pane *pane
30 desiredVisible bool30 desiredVisible bool
31 generation uint6431 generation uint64
32- launch domain.FloatingConfig
33 }32 }
34 33
35 // beginFloatingWarmLocked starts a single background launch. The caller must34 // beginFloatingWarmLocked starts a single background launch. The caller must
36 // hold tb.mu and must only call it for an uninitialized slot.35 // hold tb.mu and must only call it for an uninitialized slot.
37-func (tb *tab) beginFloatingWarmLocked(launch domain.FloatingConfig, desiredVisible bool) uint64 {36+func (tb *tab) beginFloatingWarmLocked(desiredVisible bool) uint64 {
38 if tb == nil || tb.floating.state != floatingUninitialized {37 if tb == nil || tb.floating.state != floatingUninitialized {
39 return 038 return 0
40 }39 }
41 tb.floating.generation++40 tb.floating.generation++
42 tb.floating.state = floatingWarming41 tb.floating.state = floatingWarming
43 tb.floating.desiredVisible = desiredVisible42 tb.floating.desiredVisible = desiredVisible
44- tb.floating.launch = launch
45 return tb.floating.generation43 return tb.floating.generation
46 }44 }
47 45
48 // toggleFloatingLocked applies the user action. Its result says whether a new46 // toggleFloatingLocked applies the user action. Its result says whether a new
49 // PTY launch is required and returns the generation associated with that launch47 // PTY launch is required and returns the generation associated with that launch
50 // or current slot.48 // or current slot.
51-func (tb *tab) toggleFloatingLocked(launch domain.FloatingConfig) (bool, uint64) {49+func (tb *tab) toggleFloatingLocked() (bool, uint64) {
52 if tb == nil {50 if tb == nil {
53 return false, 051 return false, 0
54 }52 }
55 switch tb.floating.state {53 switch tb.floating.state {
56 case floatingUninitialized:54 case floatingUninitialized:
57- generation := tb.beginFloatingWarmLocked(launch, true)55+ generation := tb.beginFloatingWarmLocked(true)
58 return generation != 0, generation56 return generation != 0, generation
59 case floatingWarming:57 case floatingWarming:
60 tb.floating.desiredVisible = !tb.floating.desiredVisible58 tb.floating.desiredVisible = !tb.floating.desiredVisible
@@ -90,7 +88,6 @@ func (tb *tab) failFloatingLocked(generation uint64) bool {
90 tb.floating.state = floatingUninitialized88 tb.floating.state = floatingUninitialized
91 tb.floating.pane = nil89 tb.floating.pane = nil
92 tb.floating.desiredVisible = false90 tb.floating.desiredVisible = false
93- tb.floating.launch = domain.FloatingConfig{}
94 return true91 return true
95 }92 }
96 93
@@ -116,7 +113,6 @@ func (tb *tab) takeFloatingLocked() *pane {
116 tb.floating.state = floatingUninitialized113 tb.floating.state = floatingUninitialized
117 tb.floating.pane = nil114 tb.floating.pane = nil
118 tb.floating.desiredVisible = false115 tb.floating.desiredVisible = false
119- tb.floating.launch = domain.FloatingConfig{}
120 return p116 return p
121 }117 }
122 118
@@ -172,7 +168,7 @@ func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error {
172 }168 }
173 cfg := d.currentFloatingConfig()169 cfg := d.currentFloatingConfig()
174 tb.mu.Lock()170 tb.mu.Lock()
175- start, generation := tb.toggleFloatingLocked(cfg)171+ start, generation := tb.toggleFloatingLocked()
176 visible := tb.floating.desiredVisible172 visible := tb.floating.desiredVisible
177 tb.mu.Unlock()173 tb.mu.Unlock()
178 if start {174 if start {
@@ -189,28 +185,53 @@ func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error {
189 func (d *Daemon) startFloating(sess *session, tb *tab, visible bool) {185 func (d *Daemon) startFloating(sess *session, tb *tab, visible bool) {
190 cfg := d.currentFloatingConfig()186 cfg := d.currentFloatingConfig()
191 tb.mu.Lock()187 tb.mu.Lock()
192- generation := tb.beginFloatingWarmLocked(cfg, visible)188+ generation := tb.beginFloatingWarmLocked(visible)
193 tb.mu.Unlock()189 tb.mu.Unlock()
194 if generation != 0 {190 if generation != 0 {
195 d.launchFloating(sess, tb, cfg, generation, visible)191 d.launchFloating(sess, tb, cfg, generation, visible)
196 }192 }
197 }193 }
198 194
199-// launchFloating snapshots all launch inputs before starting the potentially195+// floatingLaunchSpec is an immutable snapshot of one launch. It is built
200-// blocking factory call. In particular, no PTY operation occurs while tb.mu is196+// before the worker starts so config reloads cannot affect that launch.
201-// held, and config reloads cannot affect this launch.197+type floatingLaunchSpec struct {
198+ sessionName string
199+ cwd string
200+ size domain.Size
201+ paneStableID string
202+ env []string
203+ command string
204+ args []string
205+ fallback string
206+ parentCtx context.Context
207+ userOpen bool
208+}
209+
210+// launchFloating snapshots launch inputs, then accounts for a worker through
211+// Open, pane initialization, publication, and reader/scheduler startup.
202 func (d *Daemon) launchFloating(sess *session, tb *tab, cfg domain.FloatingConfig, generation uint64, userOpen bool) {212 func (d *Daemon) launchFloating(sess *session, tb *tab, cfg domain.FloatingConfig, generation uint64, userOpen bool) {
213+ spec, err := d.newFloatingLaunchSpec(sess, tb, cfg, userOpen)
214+ if err != nil {
215+ d.failFloatingLaunch(tb, generation, userOpen, spec.sessionName, err)
216+ return
217+ }
218+ // Count the launch before its goroutine starts. It remains counted through
219+ // install, including the reader/scheduler Adds, so shutdown cannot return
220+ // before a late Open completion has either been rejected or fully joined.
221+ d.sessWg.Go(func() { d.openAndInstallFloating(sess, tb, spec, generation) })
222+}
223+
224+func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.FloatingConfig, userOpen bool) (floatingLaunchSpec, error) {
203 sess.mu.Lock()225 sess.mu.Lock()
204- name, cwd, term := sess.name, sess.cwd, sess.terminal226+ name, cwd, term, sessCtx := sess.name, sess.cwd, sess.terminal, sess.ctx
205 sess.mu.Unlock()227 sess.mu.Unlock()
206 tb.mu.Lock()228 tb.mu.Lock()
207- sz := floatingInnerSize(tb.size, cfg)229+ size := floatingInnerSize(tb.size, cfg)
208 focused := tb.focusedPane()230 focused := tb.focusedPane()
209- tabStableID := tb.stableID231+ tabStableID, tabCtx := tb.stableID, tb.ctx
210- tabCtx := tb.ctx
211 tb.mu.Unlock()232 tb.mu.Unlock()
212- if !sz.Valid() {233+ if !size.Valid() {
213- sz = domain.Size{Cols: 1, Rows: 1}234+ size = domain.Size{Cols: 1, Rows: 1}
214 }235 }
215 if focused != nil && d.procCwd != nil && focused.pty != nil {236 if focused != nil && d.procCwd != nil && focused.pty != nil {
216 if live, err := d.procCwd(focused.pty.Pid()); err == nil && live != "" {237 if live, err := d.procCwd(focused.pty.Pid()); err == nil && live != "" {
@@ -222,39 +243,47 @@ func (d *Daemon) launchFloating(sess *session, tb *tab, cfg domain.FloatingConfi
222 }243 }
223 paneStableID, err := newStableID("p")244 paneStableID, err := newStableID("p")
224 if err != nil {245 if err != nil {
225- d.failFloatingLaunch(tb, generation, userOpen, name, err)246+ return floatingLaunchSpec{sessionName: name}, err
226- return247+ }
248+ if tabCtx == nil {
249+ tabCtx = sessCtx
250+ }
251+ if tabCtx == nil {
252+ tabCtx = context.Background()
227 }253 }
228- env := d.childEnv(name, tabStableID, paneStableID, term)
229 command, args := d.shell, append([]string(nil), d.shellArgs...)254 command, args := d.shell, append([]string(nil), d.shellArgs...)
230 if cfg.Command != "" {255 if cfg.Command != "" {
231 args = []string{"-lc", cfg.Command}256 args = []string{"-lc", cfg.Command}
232 }257 }
233- fallback := floatingCommandFallback(cfg.Command, d.shell)258+ return floatingLaunchSpec{
234- // Count the launch before its goroutine starts. It remains counted through259+ sessionName: name,
235- // install, including the reader/scheduler Adds, so shutdown cannot return260+ cwd: cwd,
236- // before a late Open completion has either been rejected or fully joined.261+ size: size,
237- d.sessWg.Go(func() {262+ paneStableID: paneStableID,
238- pty, openErr := d.ptys.Open(command, args, env, cwd, sz)263+ env: d.childEnv(name, tabStableID, paneStableID, term),
239- if openErr != nil {264+ command: command,
240- d.failFloatingLaunch(tb, generation, userOpen, name, openErr)265+ args: args,
241- return266+ fallback: floatingCommandFallback(cfg.Command, d.shell),
242- }267+ parentCtx: tabCtx,
243- p := newPaneWithStableID(layout.PaneID("floating"), paneStableID, pty, sz)268+ userOpen: userOpen,
244- p.title.displayFallback = fallback269+ }, nil
245- paneCtx := tabCtx270+}
246- if paneCtx == nil {271+
247- paneCtx = sess.ctx272+// openAndInstallFloating runs entirely in the launch worker. No PTY operation
248- }273+// occurs under tb.mu; the pane is fully initialized before publication.
249- if paneCtx == nil {274+func (d *Daemon) openAndInstallFloating(sess *session, tb *tab, spec floatingLaunchSpec, generation uint64) {
250- paneCtx = context.Background()275+ pty, err := d.ptys.Open(spec.command, spec.args, spec.env, spec.cwd, spec.size)
251- }276+ if err != nil {
252- p.ctx, p.cancel = context.WithCancel(paneCtx)277+ d.failFloatingLaunch(tb, generation, spec.userOpen, spec.sessionName, err)
253- // The reader may run as soon as install returns; make its exit policy278+ return
254- // immutable before publishing the pane to the slot.279+ }
255- p.onExit = func() { d.reapFloating(sess, tb, p, generation) }280+ p := newPaneWithStableID(layout.PaneID("floating"), spec.paneStableID, pty, spec.size)
256- d.installFloating(sess, tb, p, generation)281+ p.title.displayFallback = spec.fallback
257- })282+ p.ctx, p.cancel = context.WithCancel(spec.parentCtx)
283+ // The reader may run as soon as install returns; make its exit policy
284+ // immutable before publishing the pane to the slot.
285+ p.onExit = func() { d.reapFloating(sess, tb, p, generation) }
286+ d.installFloating(sess, tb, p, generation)
258 }287 }
259 288
260 func (d *Daemon) failFloatingLaunch(tb *tab, generation uint64, userOpen bool, sessionName string, err error) {289 func (d *Daemon) failFloatingLaunch(tb *tab, generation uint64, userOpen bool, sessionName string, err error) {
M internal/usecase/daemon/floating_test.go +16 -18
@@ -22,8 +22,6 @@ import (
2222 func TestFloatingSlotTransitions(t *testing.T) {
2323 first := &pane{}
2424 second := &pane{}
25- launch := domain.FloatingConfig{Command: "top", Width: 80, Height: 80}
26-
2725 tests := []struct {
2826 name string
2927 run func(t *testing.T, tb *tab)
@@ -32,12 +30,12 @@ func TestFloatingSlotTransitions(t *testing.T) {
3230 name: "warming double toggle retains hidden completion",
3331 run: func(t *testing.T, tb *tab) {
3432 tb.mu.Lock()
35- generation := tb.beginFloatingWarmLocked(launch, false)
36- start, gotGeneration := tb.toggleFloatingLocked(launch)
33+ generation := tb.beginFloatingWarmLocked(false)
34+ start, gotGeneration := tb.toggleFloatingLocked()
3735 if start || gotGeneration != generation || !tb.floating.desiredVisible {
3836 t.Fatalf("first warming toggle = start %v, generation %d, desired %v", start, gotGeneration, tb.floating.desiredVisible)
3937 }
40- start, gotGeneration = tb.toggleFloatingLocked(launch)
38+ start, gotGeneration = tb.toggleFloatingLocked()
4139 if start || gotGeneration != generation || tb.floating.desiredVisible {
4240 t.Fatalf("second warming toggle = start %v, generation %d, desired %v", start, gotGeneration, tb.floating.desiredVisible)
4341 }
@@ -51,15 +49,15 @@ func TestFloatingSlotTransitions(t *testing.T) {
5149 name: "open and hide retain same pane",
5250 run: func(t *testing.T, tb *tab) {
5351 tb.mu.Lock()
54- generation := tb.beginFloatingWarmLocked(launch, false)
52+ generation := tb.beginFloatingWarmLocked(false)
5553 if !tb.installFloatingLocked(first, generation) {
5654 t.Fatal("install failed")
5755 }
58- start, _ := tb.toggleFloatingLocked(launch)
56+ start, _ := tb.toggleFloatingLocked()
5957 if start || tb.floating.state != floatingVisible || tb.floating.pane != first {
6058 t.Fatal("hidden open did not retain pane")
6159 }
62- tb.toggleFloatingLocked(launch)
60+ tb.toggleFloatingLocked()
6361 if tb.floating.state != floatingHidden || tb.floating.pane != first {
6462 t.Fatal("visible hide did not retain pane")
6563 }
@@ -70,7 +68,7 @@ func TestFloatingSlotTransitions(t *testing.T) {
7068 name: "current launch failure clears desired visibility",
7169 run: func(t *testing.T, tb *tab) {
7270 tb.mu.Lock()
73- generation := tb.beginFloatingWarmLocked(launch, true)
71+ generation := tb.beginFloatingWarmLocked(true)
7472 if !tb.failFloatingLocked(generation) || tb.floating.state != floatingUninitialized || tb.floating.desiredVisible || tb.floating.pane != nil {
7573 t.Fatal("current launch failure did not clear slot")
7674 }
@@ -81,14 +79,14 @@ func TestFloatingSlotTransitions(t *testing.T) {
8179 name: "current exit clears and stale completion is ignored",
8280 run: func(t *testing.T, tb *tab) {
8381 tb.mu.Lock()
84- generation := tb.beginFloatingWarmLocked(launch, true)
82+ generation := tb.beginFloatingWarmLocked(true)
8583 if !tb.installFloatingLocked(first, generation) {
8684 t.Fatal("install failed")
8785 }
8886 if !tb.clearFloatingLocked(first, generation) || tb.floating.state != floatingUninitialized || tb.floating.pane != nil {
8987 t.Fatal("current exit did not clear slot")
9088 }
91- newGeneration := tb.beginFloatingWarmLocked(launch, true)
89+ newGeneration := tb.beginFloatingWarmLocked(true)
9290 if tb.installFloatingLocked(second, generation) {
9391 t.Fatal("stale completion installed")
9492 }
@@ -266,11 +264,11 @@ func TestFloatingLifecycleStaleSuccessAndOldExitCannotReplaceCurrentSlot(t *test
266264 first := &pane{}
267265 second := &pane{}
268266 tb.mu.Lock()
269- g1 := tb.beginFloatingWarmLocked(domain.FloatingConfig{}, true)
267+ g1 := tb.beginFloatingWarmLocked(true)
270268 require.True(t, tb.installFloatingLocked(first, g1))
271269 g2 := tb.clearFloatingLocked(first, g1)
272270 require.True(t, g2)
273- generation := tb.beginFloatingWarmLocked(domain.FloatingConfig{}, true)
271+ generation := tb.beginFloatingWarmLocked(true)
274272 require.True(t, tb.installFloatingLocked(second, generation))
275273 tb.mu.Unlock()
276274 d.reapFloating(sess, tb, first, g1)
@@ -284,7 +282,7 @@ func TestFloatingReapAndTeardownCloseMatchingPaneOnce(t *testing.T) {
284282 tb := newFloatingTestTab(t)
285283 p := newPane(layout.PaneID("floating"), pty, domain.Size{Cols: 10, Rows: 10})
286284 tb.mu.Lock()
287- g := tb.beginFloatingWarmLocked(domain.FloatingConfig{}, true)
285+ g := tb.beginFloatingWarmLocked(true)
288286 require.True(t, tb.installFloatingLocked(p, g))
289287 tb.mu.Unlock()
290288 d := newTestDaemon(t, nil, stubClock{})
@@ -298,7 +296,7 @@ func TestFloatingTeardownClosesInstalledPaneOnce(t *testing.T) {
298296 tb := newFloatingTestTab(t)
299297 p := newPane(layout.PaneID("floating"), pty, domain.Size{Cols: 10, Rows: 10})
300298 tb.mu.Lock()
301- g := tb.beginFloatingWarmLocked(domain.FloatingConfig{}, false)
299+ g := tb.beginFloatingWarmLocked(false)
302300 require.True(t, tb.installFloatingLocked(p, g))
303301 tb.mu.Unlock()
304302 d := newTestDaemon(t, nil, stubClock{})
@@ -328,7 +326,7 @@ func TestFloatingEOFRepaintsVisibleSlotOnly(t *testing.T) {
328326 return 0, io.EOF
329327 }).Once()
330328 floating := newPane(layout.PaneID("floating"), floatingPTY, domain.Size{Cols: 20, Rows: 8})
331- generation := tb.beginFloatingWarmLocked(domain.FloatingConfig{}, state == floatingVisible)
329+ generation := tb.beginFloatingWarmLocked(state == floatingVisible)
332330 require.True(t, tb.installFloatingLocked(floating, generation))
333331 if state == floatingHidden {
334332 require.Equal(t, floatingHidden, tb.floating.state)
@@ -395,7 +393,7 @@ func TestFloatingEOFRepaintsVisibleSlotOnly(t *testing.T) {
395393 generation := tb.floating.generation
396394 require.True(t, tb.clearFloatingLocked(stale, generation))
397395 current := newPane(layout.PaneID("replacement"), newBlockingPanePTY(t), domain.Size{Cols: 20, Rows: 8})
398- currentGeneration := tb.beginFloatingWarmLocked(domain.FloatingConfig{}, true)
396+ currentGeneration := tb.beginFloatingWarmLocked(true)
399397 require.True(t, tb.installFloatingLocked(current, currentGeneration))
400398 tb.mu.Unlock()
401399 exit()
@@ -530,7 +528,7 @@ func TestFloatingToggleUsesVisibleTarget(t *testing.T) {
530528 tb := newFloatingTestTab(t)
531529 sess := &session{tabs: []*tab{tb}, ctx: t.Context()}
532530 tb.mu.Lock()
533- g := tb.beginFloatingWarmLocked(domain.FloatingConfig{}, false)
531+ g := tb.beginFloatingWarmLocked(false)
534532 floating := &pane{}
535533 require.True(t, tb.installFloatingLocked(floating, g))
536534 tb.mu.Unlock()
@@ -22,8 +22,6 @@ import (
22 func TestFloatingSlotTransitions(t *testing.T) {22 func TestFloatingSlotTransitions(t *testing.T) {
23 first := &pane{}23 first := &pane{}
24 second := &pane{}24 second := &pane{}
25- launch := domain.FloatingConfig{Command: "top", Width: 80, Height: 80}
26-
27 tests := []struct {25 tests := []struct {
28 name string26 name string
29 run func(t *testing.T, tb *tab)27 run func(t *testing.T, tb *tab)
@@ -32,12 +30,12 @@ func TestFloatingSlotTransitions(t *testing.T) {
32 name: "warming double toggle retains hidden completion",30 name: "warming double toggle retains hidden completion",
33 run: func(t *testing.T, tb *tab) {31 run: func(t *testing.T, tb *tab) {
34 tb.mu.Lock()32 tb.mu.Lock()
35- generation := tb.beginFloatingWarmLocked(launch, false)33+ generation := tb.beginFloatingWarmLocked(false)
36- start, gotGeneration := tb.toggleFloatingLocked(launch)34+ start, gotGeneration := tb.toggleFloatingLocked()
37 if start || gotGeneration != generation || !tb.floating.desiredVisible {35 if start || gotGeneration != generation || !tb.floating.desiredVisible {
38 t.Fatalf("first warming toggle = start %v, generation %d, desired %v", start, gotGeneration, tb.floating.desiredVisible)36 t.Fatalf("first warming toggle = start %v, generation %d, desired %v", start, gotGeneration, tb.floating.desiredVisible)
39 }37 }
40- start, gotGeneration = tb.toggleFloatingLocked(launch)38+ start, gotGeneration = tb.toggleFloatingLocked()
41 if start || gotGeneration != generation || tb.floating.desiredVisible {39 if start || gotGeneration != generation || tb.floating.desiredVisible {
42 t.Fatalf("second warming toggle = start %v, generation %d, desired %v", start, gotGeneration, tb.floating.desiredVisible)40 t.Fatalf("second warming toggle = start %v, generation %d, desired %v", start, gotGeneration, tb.floating.desiredVisible)
43 }41 }
@@ -51,15 +49,15 @@ func TestFloatingSlotTransitions(t *testing.T) {
51 name: "open and hide retain same pane",49 name: "open and hide retain same pane",
52 run: func(t *testing.T, tb *tab) {50 run: func(t *testing.T, tb *tab) {
53 tb.mu.Lock()51 tb.mu.Lock()
54- generation := tb.beginFloatingWarmLocked(launch, false)52+ generation := tb.beginFloatingWarmLocked(false)
55 if !tb.installFloatingLocked(first, generation) {53 if !tb.installFloatingLocked(first, generation) {
56 t.Fatal("install failed")54 t.Fatal("install failed")
57 }55 }
58- start, _ := tb.toggleFloatingLocked(launch)56+ start, _ := tb.toggleFloatingLocked()
59 if start || tb.floating.state != floatingVisible || tb.floating.pane != first {57 if start || tb.floating.state != floatingVisible || tb.floating.pane != first {
60 t.Fatal("hidden open did not retain pane")58 t.Fatal("hidden open did not retain pane")
61 }59 }
62- tb.toggleFloatingLocked(launch)60+ tb.toggleFloatingLocked()
63 if tb.floating.state != floatingHidden || tb.floating.pane != first {61 if tb.floating.state != floatingHidden || tb.floating.pane != first {
64 t.Fatal("visible hide did not retain pane")62 t.Fatal("visible hide did not retain pane")
65 }63 }
@@ -70,7 +68,7 @@ func TestFloatingSlotTransitions(t *testing.T) {
70 name: "current launch failure clears desired visibility",68 name: "current launch failure clears desired visibility",
71 run: func(t *testing.T, tb *tab) {69 run: func(t *testing.T, tb *tab) {
72 tb.mu.Lock()70 tb.mu.Lock()
73- generation := tb.beginFloatingWarmLocked(launch, true)71+ generation := tb.beginFloatingWarmLocked(true)
74 if !tb.failFloatingLocked(generation) || tb.floating.state != floatingUninitialized || tb.floating.desiredVisible || tb.floating.pane != nil {72 if !tb.failFloatingLocked(generation) || tb.floating.state != floatingUninitialized || tb.floating.desiredVisible || tb.floating.pane != nil {
75 t.Fatal("current launch failure did not clear slot")73 t.Fatal("current launch failure did not clear slot")
76 }74 }
@@ -81,14 +79,14 @@ func TestFloatingSlotTransitions(t *testing.T) {
81 name: "current exit clears and stale completion is ignored",79 name: "current exit clears and stale completion is ignored",
82 run: func(t *testing.T, tb *tab) {80 run: func(t *testing.T, tb *tab) {
83 tb.mu.Lock()81 tb.mu.Lock()
84- generation := tb.beginFloatingWarmLocked(launch, true)82+ generation := tb.beginFloatingWarmLocked(true)
85 if !tb.installFloatingLocked(first, generation) {83 if !tb.installFloatingLocked(first, generation) {
86 t.Fatal("install failed")84 t.Fatal("install failed")
87 }85 }
88 if !tb.clearFloatingLocked(first, generation) || tb.floating.state != floatingUninitialized || tb.floating.pane != nil {86 if !tb.clearFloatingLocked(first, generation) || tb.floating.state != floatingUninitialized || tb.floating.pane != nil {
89 t.Fatal("current exit did not clear slot")87 t.Fatal("current exit did not clear slot")
90 }88 }
91- newGeneration := tb.beginFloatingWarmLocked(launch, true)89+ newGeneration := tb.beginFloatingWarmLocked(true)
92 if tb.installFloatingLocked(second, generation) {90 if tb.installFloatingLocked(second, generation) {
93 t.Fatal("stale completion installed")91 t.Fatal("stale completion installed")
94 }92 }
@@ -266,11 +264,11 @@ func TestFloatingLifecycleStaleSuccessAndOldExitCannotReplaceCurrentSlot(t *test
266 first := &pane{}264 first := &pane{}
267 second := &pane{}265 second := &pane{}
268 tb.mu.Lock()266 tb.mu.Lock()
269- g1 := tb.beginFloatingWarmLocked(domain.FloatingConfig{}, true)267+ g1 := tb.beginFloatingWarmLocked(true)
270 require.True(t, tb.installFloatingLocked(first, g1))268 require.True(t, tb.installFloatingLocked(first, g1))
271 g2 := tb.clearFloatingLocked(first, g1)269 g2 := tb.clearFloatingLocked(first, g1)
272 require.True(t, g2)270 require.True(t, g2)
273- generation := tb.beginFloatingWarmLocked(domain.FloatingConfig{}, true)271+ generation := tb.beginFloatingWarmLocked(true)
274 require.True(t, tb.installFloatingLocked(second, generation))272 require.True(t, tb.installFloatingLocked(second, generation))
275 tb.mu.Unlock()273 tb.mu.Unlock()
276 d.reapFloating(sess, tb, first, g1)274 d.reapFloating(sess, tb, first, g1)
@@ -284,7 +282,7 @@ func TestFloatingReapAndTeardownCloseMatchingPaneOnce(t *testing.T) {
284 tb := newFloatingTestTab(t)282 tb := newFloatingTestTab(t)
285 p := newPane(layout.PaneID("floating"), pty, domain.Size{Cols: 10, Rows: 10})283 p := newPane(layout.PaneID("floating"), pty, domain.Size{Cols: 10, Rows: 10})
286 tb.mu.Lock()284 tb.mu.Lock()
287- g := tb.beginFloatingWarmLocked(domain.FloatingConfig{}, true)285+ g := tb.beginFloatingWarmLocked(true)
288 require.True(t, tb.installFloatingLocked(p, g))286 require.True(t, tb.installFloatingLocked(p, g))
289 tb.mu.Unlock()287 tb.mu.Unlock()
290 d := newTestDaemon(t, nil, stubClock{})288 d := newTestDaemon(t, nil, stubClock{})
@@ -298,7 +296,7 @@ func TestFloatingTeardownClosesInstalledPaneOnce(t *testing.T) {
298 tb := newFloatingTestTab(t)296 tb := newFloatingTestTab(t)
299 p := newPane(layout.PaneID("floating"), pty, domain.Size{Cols: 10, Rows: 10})297 p := newPane(layout.PaneID("floating"), pty, domain.Size{Cols: 10, Rows: 10})
300 tb.mu.Lock()298 tb.mu.Lock()
301- g := tb.beginFloatingWarmLocked(domain.FloatingConfig{}, false)299+ g := tb.beginFloatingWarmLocked(false)
302 require.True(t, tb.installFloatingLocked(p, g))300 require.True(t, tb.installFloatingLocked(p, g))
303 tb.mu.Unlock()301 tb.mu.Unlock()
304 d := newTestDaemon(t, nil, stubClock{})302 d := newTestDaemon(t, nil, stubClock{})
@@ -328,7 +326,7 @@ func TestFloatingEOFRepaintsVisibleSlotOnly(t *testing.T) {
328 return 0, io.EOF326 return 0, io.EOF
329 }).Once()327 }).Once()
330 floating := newPane(layout.PaneID("floating"), floatingPTY, domain.Size{Cols: 20, Rows: 8})328 floating := newPane(layout.PaneID("floating"), floatingPTY, domain.Size{Cols: 20, Rows: 8})
331- generation := tb.beginFloatingWarmLocked(domain.FloatingConfig{}, state == floatingVisible)329+ generation := tb.beginFloatingWarmLocked(state == floatingVisible)
332 require.True(t, tb.installFloatingLocked(floating, generation))330 require.True(t, tb.installFloatingLocked(floating, generation))
333 if state == floatingHidden {331 if state == floatingHidden {
334 require.Equal(t, floatingHidden, tb.floating.state)332 require.Equal(t, floatingHidden, tb.floating.state)
@@ -395,7 +393,7 @@ func TestFloatingEOFRepaintsVisibleSlotOnly(t *testing.T) {
395 generation := tb.floating.generation393 generation := tb.floating.generation
396 require.True(t, tb.clearFloatingLocked(stale, generation))394 require.True(t, tb.clearFloatingLocked(stale, generation))
397 current := newPane(layout.PaneID("replacement"), newBlockingPanePTY(t), domain.Size{Cols: 20, Rows: 8})395 current := newPane(layout.PaneID("replacement"), newBlockingPanePTY(t), domain.Size{Cols: 20, Rows: 8})
398- currentGeneration := tb.beginFloatingWarmLocked(domain.FloatingConfig{}, true)396+ currentGeneration := tb.beginFloatingWarmLocked(true)
399 require.True(t, tb.installFloatingLocked(current, currentGeneration))397 require.True(t, tb.installFloatingLocked(current, currentGeneration))
400 tb.mu.Unlock()398 tb.mu.Unlock()
401 exit()399 exit()
@@ -530,7 +528,7 @@ func TestFloatingToggleUsesVisibleTarget(t *testing.T) {
530 tb := newFloatingTestTab(t)528 tb := newFloatingTestTab(t)
531 sess := &session{tabs: []*tab{tb}, ctx: t.Context()}529 sess := &session{tabs: []*tab{tb}, ctx: t.Context()}
532 tb.mu.Lock()530 tb.mu.Lock()
533- g := tb.beginFloatingWarmLocked(domain.FloatingConfig{}, false)531+ g := tb.beginFloatingWarmLocked(false)
534 floating := &pane{}532 floating := &pane{}
535 require.True(t, tb.installFloatingLocked(floating, g))533 require.True(t, tb.installFloatingLocked(floating, g))
536 tb.mu.Unlock()534 tb.mu.Unlock()
M internal/usecase/daemon/snapshot_test.go +1 -1
@@ -72,7 +72,7 @@ func TestSnapshotCaptureExcludesFloatingRuntime(t *testing.T) {
7272 floatingPTY, _ := newBlockingPTY(t)
7373 floating := newPane(layout.PaneID("floating"), floatingPTY, domain.Size{Cols: 20, Rows: 8})
7474 tb.mu.Lock()
75- generation := tb.beginFloatingWarmLocked(domain.FloatingConfig{Command: "top"}, false)
75+ generation := tb.beginFloatingWarmLocked(false)
7676 require.True(t, tb.installFloatingLocked(floating, generation))
7777 tb.mu.Unlock()
7878
@@ -72,7 +72,7 @@ func TestSnapshotCaptureExcludesFloatingRuntime(t *testing.T) {
72 floatingPTY, _ := newBlockingPTY(t)72 floatingPTY, _ := newBlockingPTY(t)
73 floating := newPane(layout.PaneID("floating"), floatingPTY, domain.Size{Cols: 20, Rows: 8})73 floating := newPane(layout.PaneID("floating"), floatingPTY, domain.Size{Cols: 20, Rows: 8})
74 tb.mu.Lock()74 tb.mu.Lock()
75- generation := tb.beginFloatingWarmLocked(domain.FloatingConfig{Command: "top"}, false)75+ generation := tb.beginFloatingWarmLocked(false)
76 require.True(t, tb.installFloatingLocked(floating, generation))76 require.True(t, tb.installFloatingLocked(floating, generation))
77 tb.mu.Unlock()77 tb.mu.Unlock()
78 78

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