detached b897e1c

fix(daemon): resize floating pane on show

commit
b897e1c4b9e62745192813fd620c7e53724bd8c8
author
brice · · 2026-07-10 06:23 UTC
parent
9f11203
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +49 -3
M internal/usecase/daemon/floating.go +3 -1
@@ -169,7 +169,9 @@ func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error {
169169 cfg := d.currentFloatingConfig()
170170 tb.mu.Lock()
171171 start, generation := tb.toggleFloatingLocked()
172- visible := tb.floating.desiredVisible
172+ // desiredVisible is meaningful only while a launch is warming. Installed
173+ // slots derive their resize and paint work from the resulting slot state.
174+ visible := tb.floating.state == floatingVisible
173175 tb.mu.Unlock()
174176 if start {
175177 d.launchFloating(sess, tb, cfg, generation, visible)
@@ -169,7 +169,9 @@ func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error {
169 cfg := d.currentFloatingConfig()169 cfg := d.currentFloatingConfig()
170 tb.mu.Lock()170 tb.mu.Lock()
171 start, generation := tb.toggleFloatingLocked()171 start, generation := tb.toggleFloatingLocked()
172- visible := tb.floating.desiredVisible172+ // desiredVisible is meaningful only while a launch is warming. Installed
173+ // slots derive their resize and paint work from the resulting slot state.
174+ visible := tb.floating.state == floatingVisible
173 tb.mu.Unlock()175 tb.mu.Unlock()
174 if start {176 if start {
175 d.launchFloating(sess, tb, cfg, generation, visible)177 d.launchFloating(sess, tb, cfg, generation, visible)
M internal/usecase/daemon/floating_render_test.go +45 -1
@@ -121,6 +121,45 @@ func TestDrawFloatingBorderOmitsTinyAxes(t *testing.T) {
121121 }
122122 }
123123
124+func TestToggleFloatingResizesHiddenPaneOnShowAndRetriesFailure(t *testing.T) {
125+ d := newTestDaemon(t, nil, stubClock{})
126+ d.ApplyConfig(domain.Config{Floating: domain.FloatingConfig{Width: 50, Height: 50}})
127+ tb := newTab(nil, domain.Size{Cols: 80, Rows: 24})
128+ sess := &session{tabs: []*tab{tb}}
129+
130+ initial := calculateFloatingGeometry(domain.Rect{Width: 80, Height: 24}, d.currentFloatingConfig()).Inner
131+ current := calculateFloatingGeometry(domain.Rect{Width: 100, Height: 40}, d.currentFloatingConfig()).Inner
132+ pty := &resizePTY{errs: []error{errors.New("first show fails"), nil}}
133+ floating := newPane("floating", pty, rectSize(initial))
134+ floating.rect = initial
135+
136+ // Prewarming installs a hidden pane at the initial geometry.
137+ tb.mu.Lock()
138+ generation := tb.beginFloatingWarmLocked(false)
139+ require.True(t, tb.installFloatingLocked(floating, generation))
140+ require.Equal(t, floatingHidden, tb.floating.state)
141+ require.False(t, tb.floating.desiredVisible)
142+ tb.mu.Unlock()
143+ // A client resize changes tab geometry but must leave the hidden PTY alone.
144+ d.resize(sess, nil, domain.Size{Cols: 100, Rows: 42})
145+ require.Empty(t, pty.sizes())
146+
147+ // Showing attempts the current size before paint. A failed resize leaves the
148+ // old screen and rect untouched, so a later hide/show can retry.
149+ require.NoError(t, d.toggleFloating(sess, nil))
150+ require.Equal(t, []domain.Size{rectSize(current)}, pty.sizes())
151+ require.Equal(t, initial, floating.rect)
152+ require.Equal(t, initial.Width, floating.screen.Frame.Width)
153+ require.Equal(t, initial.Height, floating.screen.Frame.Height)
154+
155+ require.NoError(t, d.toggleFloating(sess, nil)) // hide
156+ require.NoError(t, d.toggleFloating(sess, nil)) // retry show
157+ require.Equal(t, []domain.Size{rectSize(current), rectSize(current)}, pty.sizes())
158+ require.Equal(t, current, floating.rect)
159+ require.Equal(t, current.Width, floating.screen.Frame.Width)
160+ require.Equal(t, current.Height, floating.screen.Frame.Height)
161+}
162+
124163 func TestResizeFloatingPaneFailureAndSerialization(t *testing.T) {
125164 t.Run("failure preserves state", func(t *testing.T) {
126165 pty := &resizePTY{err: errors.New("nope")}
@@ -157,6 +196,7 @@ type resizePTY struct {
157196 mu sync.Mutex
158197 resizes []domain.Size
159198 err error
199+ errs []error
160200 entered chan struct{}
161201 release chan struct{}
162202 }
@@ -165,12 +205,16 @@ func (p *resizePTY) Resize(sz domain.Size) error {
165205 p.mu.Lock()
166206 p.resizes = append(p.resizes, sz)
167207 n := len(p.resizes)
208+ err := p.err
209+ if n <= len(p.errs) {
210+ err = p.errs[n-1]
211+ }
168212 p.mu.Unlock()
169213 if n == 1 && p.entered != nil {
170214 close(p.entered)
171215 <-p.release
172216 }
173- return p.err
217+ return err
174218 }
175219 func (p *resizePTY) calls() int { p.mu.Lock(); defer p.mu.Unlock(); return len(p.resizes) }
176220 func (p *resizePTY) sizes() []domain.Size {
@@ -121,6 +121,45 @@ func TestDrawFloatingBorderOmitsTinyAxes(t *testing.T) {
121 }121 }
122 }122 }
123 123
124+func TestToggleFloatingResizesHiddenPaneOnShowAndRetriesFailure(t *testing.T) {
125+ d := newTestDaemon(t, nil, stubClock{})
126+ d.ApplyConfig(domain.Config{Floating: domain.FloatingConfig{Width: 50, Height: 50}})
127+ tb := newTab(nil, domain.Size{Cols: 80, Rows: 24})
128+ sess := &session{tabs: []*tab{tb}}
129+
130+ initial := calculateFloatingGeometry(domain.Rect{Width: 80, Height: 24}, d.currentFloatingConfig()).Inner
131+ current := calculateFloatingGeometry(domain.Rect{Width: 100, Height: 40}, d.currentFloatingConfig()).Inner
132+ pty := &resizePTY{errs: []error{errors.New("first show fails"), nil}}
133+ floating := newPane("floating", pty, rectSize(initial))
134+ floating.rect = initial
135+
136+ // Prewarming installs a hidden pane at the initial geometry.
137+ tb.mu.Lock()
138+ generation := tb.beginFloatingWarmLocked(false)
139+ require.True(t, tb.installFloatingLocked(floating, generation))
140+ require.Equal(t, floatingHidden, tb.floating.state)
141+ require.False(t, tb.floating.desiredVisible)
142+ tb.mu.Unlock()
143+ // A client resize changes tab geometry but must leave the hidden PTY alone.
144+ d.resize(sess, nil, domain.Size{Cols: 100, Rows: 42})
145+ require.Empty(t, pty.sizes())
146+
147+ // Showing attempts the current size before paint. A failed resize leaves the
148+ // old screen and rect untouched, so a later hide/show can retry.
149+ require.NoError(t, d.toggleFloating(sess, nil))
150+ require.Equal(t, []domain.Size{rectSize(current)}, pty.sizes())
151+ require.Equal(t, initial, floating.rect)
152+ require.Equal(t, initial.Width, floating.screen.Frame.Width)
153+ require.Equal(t, initial.Height, floating.screen.Frame.Height)
154+
155+ require.NoError(t, d.toggleFloating(sess, nil)) // hide
156+ require.NoError(t, d.toggleFloating(sess, nil)) // retry show
157+ require.Equal(t, []domain.Size{rectSize(current), rectSize(current)}, pty.sizes())
158+ require.Equal(t, current, floating.rect)
159+ require.Equal(t, current.Width, floating.screen.Frame.Width)
160+ require.Equal(t, current.Height, floating.screen.Frame.Height)
161+}
162+
124 func TestResizeFloatingPaneFailureAndSerialization(t *testing.T) {163 func TestResizeFloatingPaneFailureAndSerialization(t *testing.T) {
125 t.Run("failure preserves state", func(t *testing.T) {164 t.Run("failure preserves state", func(t *testing.T) {
126 pty := &resizePTY{err: errors.New("nope")}165 pty := &resizePTY{err: errors.New("nope")}
@@ -157,6 +196,7 @@ type resizePTY struct {
157 mu sync.Mutex196 mu sync.Mutex
158 resizes []domain.Size197 resizes []domain.Size
159 err error198 err error
199+ errs []error
160 entered chan struct{}200 entered chan struct{}
161 release chan struct{}201 release chan struct{}
162 }202 }
@@ -165,12 +205,16 @@ func (p *resizePTY) Resize(sz domain.Size) error {
165 p.mu.Lock()205 p.mu.Lock()
166 p.resizes = append(p.resizes, sz)206 p.resizes = append(p.resizes, sz)
167 n := len(p.resizes)207 n := len(p.resizes)
208+ err := p.err
209+ if n <= len(p.errs) {
210+ err = p.errs[n-1]
211+ }
168 p.mu.Unlock()212 p.mu.Unlock()
169 if n == 1 && p.entered != nil {213 if n == 1 && p.entered != nil {
170 close(p.entered)214 close(p.entered)
171 <-p.release215 <-p.release
172 }216 }
173- return p.err217+ return err
174 }218 }
175 func (p *resizePTY) calls() int { p.mu.Lock(); defer p.mu.Unlock(); return len(p.resizes) }219 func (p *resizePTY) calls() int { p.mu.Lock(); defer p.mu.Unlock(); return len(p.resizes) }
176 func (p *resizePTY) sizes() []domain.Size {220 func (p *resizePTY) sizes() []domain.Size {
M internal/usecase/daemon/floating_test.go +1 -1
@@ -584,7 +584,7 @@ func TestFloatingToggleUsesVisibleTarget(t *testing.T) {
584584 sess := &session{tabs: []*tab{tb}, ctx: t.Context()}
585585 tb.mu.Lock()
586586 g := tb.beginFloatingWarmLocked(false)
587- floating := &pane{}
587+ floating := newPane(layout.PaneID("floating"), nil, domain.Size{Cols: 20, Rows: 8})
588588 require.True(t, tb.installFloatingLocked(floating, g))
589589 tb.mu.Unlock()
590590 require.NoError(t, d.toggleFloating(sess, nil))
@@ -584,7 +584,7 @@ func TestFloatingToggleUsesVisibleTarget(t *testing.T) {
584 sess := &session{tabs: []*tab{tb}, ctx: t.Context()}584 sess := &session{tabs: []*tab{tb}, ctx: t.Context()}
585 tb.mu.Lock()585 tb.mu.Lock()
586 g := tb.beginFloatingWarmLocked(false)586 g := tb.beginFloatingWarmLocked(false)
587- floating := &pane{}587+ floating := newPane(layout.PaneID("floating"), nil, domain.Size{Cols: 20, Rows: 8})
588 require.True(t, tb.installFloatingLocked(floating, g))588 require.True(t, tb.installFloatingLocked(floating, g))
589 tb.mu.Unlock()589 tb.mu.Unlock()
590 require.NoError(t, d.toggleFloating(sess, nil))590 require.NoError(t, d.toggleFloating(sess, nil))

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