feat(daemon): activate floating panes per tab
- commit
- eaec9dbd40409ab416ddaf0f981a35fa81a194e9
- author
- brice · · 2026-07-10 06:57 UTC
- parent
- 9065670
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
12 files · +187 -28
M internal/usecase/daemon/attention.go +1 -0
| @@ -48,6 +48,7 @@ func (d *Daemon) jumpAttention(sess *session, ac *attachedClient) { | |||
| 48 | 48 | } | |
| 49 | 49 | if idx, ok := oldestAttentionTab(sess); ok { | |
| 50 | 50 | if sess.switchTab(idx) { | |
| 51 | + | d.activateTab(sess, sess.activeTab()) | |
| 51 | 52 | d.paint(sess, ac, true) | |
| 52 | 53 | } | |
| 53 | 54 | return | |
| @@ -48,6 +48,7 @@ func (d *Daemon) jumpAttention(sess *session, ac *attachedClient) { | |||||
| 48 | } | 48 | } | ||
| 49 | if idx, ok := oldestAttentionTab(sess); ok { | 49 | if idx, ok := oldestAttentionTab(sess); ok { | ||
| 50 | if sess.switchTab(idx) { | 50 | if sess.switchTab(idx) { | ||
| 51 | + | d.activateTab(sess, sess.activeTab()) | |||
| 51 | d.paint(sess, ac, true) | 52 | d.paint(sess, ac, true) | ||
| 52 | } | 53 | } | ||
| 53 | return | 54 | return | ||
M internal/usecase/daemon/client.go +2 -0
| @@ -442,10 +442,12 @@ func (d *Daemon) firstPaint(sess *session, ac *attachedClient, clientSize domain | |||
| 442 | 442 | ||
| 443 | 443 | if clientSize.Valid() && wsz != tabSize(clientSize) { | |
| 444 | 444 | d.resize(sess, ac, clientSize) | |
| 445 | + | d.activateTab(sess, sess.activeTab()) | |
| 445 | 446 | return | |
| 446 | 447 | } | |
| 447 | 448 | d.refreshBarScriptsIfDue(sess, d.clock.Now(), true) | |
| 448 | 449 | d.paint(sess, ac, true) | |
| 450 | + | d.activateTab(sess, tb) | |
| 449 | 451 | } | |
| 450 | 452 | ||
| 451 | 453 | // runConnLoop is the per-connection input router: it pumps client messages | |
| @@ -442,10 +442,12 @@ func (d *Daemon) firstPaint(sess *session, ac *attachedClient, clientSize domain | |||||
| 442 | 442 | ||||
| 443 | if clientSize.Valid() && wsz != tabSize(clientSize) { | 443 | if clientSize.Valid() && wsz != tabSize(clientSize) { | ||
| 444 | d.resize(sess, ac, clientSize) | 444 | d.resize(sess, ac, clientSize) | ||
| 445 | + | d.activateTab(sess, sess.activeTab()) | |||
| 445 | return | 446 | return | ||
| 446 | } | 447 | } | ||
| 447 | d.refreshBarScriptsIfDue(sess, d.clock.Now(), true) | 448 | d.refreshBarScriptsIfDue(sess, d.clock.Now(), true) | ||
| 448 | d.paint(sess, ac, true) | 449 | d.paint(sess, ac, true) | ||
| 450 | + | d.activateTab(sess, tb) | |||
| 449 | } | 451 | } | ||
| 450 | 452 | ||||
| 451 | // runConnLoop is the per-connection input router: it pumps client messages | 453 | // runConnLoop is the per-connection input router: it pumps client messages | ||
M internal/usecase/daemon/daemon_test.go +44 -3
| @@ -105,10 +105,44 @@ func newBlockingPTYWithWrites(t *testing.T, writes chan<- []byte) (*portsmocks.M | |||
| 105 | 105 | return p, unblock | |
| 106 | 106 | } | |
| 107 | 107 | ||
| 108 | + | // quietPTY is an independently closable PTY for background floating prewarms. | |
| 109 | + | // It intentionally has no testify expectations: tests using the factories below | |
| 110 | + | // are asserting their regular panes, not incidental asynchronous prewarms. | |
| 111 | + | type quietPTY struct{ done chan struct{} } | |
| 112 | + | ||
| 113 | + | func newQuietPTY() *quietPTY { return &quietPTY{done: make(chan struct{})} } | |
| 114 | + | func (p *quietPTY) Read([]byte) (int, error) { <-p.done; return 0, io.EOF } | |
| 115 | + | func (*quietPTY) Write(b []byte) (int, error) { return len(b), nil } | |
| 116 | + | func (p *quietPTY) Close() error { | |
| 117 | + | select { | |
| 118 | + | case <-p.done: | |
| 119 | + | default: | |
| 120 | + | close(p.done) | |
| 121 | + | } | |
| 122 | + | return nil | |
| 123 | + | } | |
| 124 | + | func (*quietPTY) Resize(domain.Size) error { return nil } | |
| 125 | + | func (*quietPTY) Pid() int { return 0 } | |
| 126 | + | func (*quietPTY) ForegroundPgid() (int, error) { return 0, nil } | |
| 127 | + | ||
| 128 | + | // newFactory keeps the supplied fixture for normal panes. Floating prewarms | |
| 129 | + | // have a distinct (smaller) geometry and receive independent quiet PTYs, so | |
| 130 | + | // they cannot consume test output or add post-test mock calls. | |
| 108 | 131 | func newFactory(t *testing.T, p ports.PTY) *portsmocks.MockPTYFactory { | |
| 109 | 132 | t.Helper() | |
| 110 | 133 | f := portsmocks.NewMockPTYFactory(t) | |
| 111 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(p, nil).Maybe() | |
| 134 | + | var normal domain.Size | |
| 135 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 136 | + | func(_ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { | |
| 137 | + | if !normal.Valid() { | |
| 138 | + | normal = sz | |
| 139 | + | } | |
| 140 | + | if sz == normal { | |
| 141 | + | return p, nil | |
| 142 | + | } | |
| 143 | + | return newQuietPTY(), nil | |
| 144 | + | }, | |
| 145 | + | ).Maybe() | |
| 112 | 146 | return f | |
| 113 | 147 | } | |
| 114 | 148 | ||
| @@ -116,13 +150,20 @@ func newFactorySeq(t *testing.T, ptys ...ports.PTY) *portsmocks.MockPTYFactory { | |||
| 116 | 150 | t.Helper() | |
| 117 | 151 | f := portsmocks.NewMockPTYFactory(t) | |
| 118 | 152 | var mu sync.Mutex | |
| 153 | + | var normal domain.Size | |
| 119 | 154 | next := 0 | |
| 120 | 155 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 121 | - | func(string, []string, []string, string, domain.Size) (ports.PTY, error) { | |
| 156 | + | func(_ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { | |
| 122 | 157 | mu.Lock() | |
| 123 | 158 | defer mu.Unlock() | |
| 159 | + | if !normal.Valid() { | |
| 160 | + | normal = sz | |
| 161 | + | } | |
| 162 | + | if sz != normal { | |
| 163 | + | return newQuietPTY(), nil | |
| 164 | + | } | |
| 124 | 165 | if next >= len(ptys) { | |
| 125 | - | t.Fatalf("unexpected PTY open #%d", next+1) | |
| 166 | + | return newQuietPTY(), nil | |
| 126 | 167 | } | |
| 127 | 168 | p := ptys[next] | |
| 128 | 169 | next++ | |
| @@ -105,10 +105,44 @@ func newBlockingPTYWithWrites(t *testing.T, writes chan<- []byte) (*portsmocks.M | |||||
| 105 | return p, unblock | 105 | return p, unblock | ||
| 106 | } | 106 | } | ||
| 107 | 107 | ||||
| 108 | + | // quietPTY is an independently closable PTY for background floating prewarms. | |||
| 109 | + | // It intentionally has no testify expectations: tests using the factories below | |||
| 110 | + | // are asserting their regular panes, not incidental asynchronous prewarms. | |||
| 111 | + | type quietPTY struct{ done chan struct{} } | |||
| 112 | + | ||||
| 113 | + | func newQuietPTY() *quietPTY { return &quietPTY{done: make(chan struct{})} } | |||
| 114 | + | func (p *quietPTY) Read([]byte) (int, error) { <-p.done; return 0, io.EOF } | |||
| 115 | + | func (*quietPTY) Write(b []byte) (int, error) { return len(b), nil } | |||
| 116 | + | func (p *quietPTY) Close() error { | |||
| 117 | + | select { | |||
| 118 | + | case <-p.done: | |||
| 119 | + | default: | |||
| 120 | + | close(p.done) | |||
| 121 | + | } | |||
| 122 | + | return nil | |||
| 123 | + | } | |||
| 124 | + | func (*quietPTY) Resize(domain.Size) error { return nil } | |||
| 125 | + | func (*quietPTY) Pid() int { return 0 } | |||
| 126 | + | func (*quietPTY) ForegroundPgid() (int, error) { return 0, nil } | |||
| 127 | + | ||||
| 128 | + | // newFactory keeps the supplied fixture for normal panes. Floating prewarms | |||
| 129 | + | // have a distinct (smaller) geometry and receive independent quiet PTYs, so | |||
| 130 | + | // they cannot consume test output or add post-test mock calls. | |||
| 108 | func newFactory(t *testing.T, p ports.PTY) *portsmocks.MockPTYFactory { | 131 | func newFactory(t *testing.T, p ports.PTY) *portsmocks.MockPTYFactory { | ||
| 109 | t.Helper() | 132 | t.Helper() | ||
| 110 | f := portsmocks.NewMockPTYFactory(t) | 133 | f := portsmocks.NewMockPTYFactory(t) | ||
| 111 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(p, nil).Maybe() | 134 | + | var normal domain.Size |
| 135 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |||
| 136 | + | func(_ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { | |||
| 137 | + | if !normal.Valid() { | |||
| 138 | + | normal = sz | |||
| 139 | + | } | |||
| 140 | + | if sz == normal { | |||
| 141 | + | return p, nil | |||
| 142 | + | } | |||
| 143 | + | return newQuietPTY(), nil | |||
| 144 | + | }, | |||
| 145 | + | ).Maybe() | |||
| 112 | return f | 146 | return f | ||
| 113 | } | 147 | } | ||
| 114 | 148 | ||||
| @@ -116,13 +150,20 @@ func newFactorySeq(t *testing.T, ptys ...ports.PTY) *portsmocks.MockPTYFactory { | |||||
| 116 | t.Helper() | 150 | t.Helper() | ||
| 117 | f := portsmocks.NewMockPTYFactory(t) | 151 | f := portsmocks.NewMockPTYFactory(t) | ||
| 118 | var mu sync.Mutex | 152 | var mu sync.Mutex | ||
| 153 | + | var normal domain.Size | |||
| 119 | next := 0 | 154 | next := 0 | ||
| 120 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 155 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | ||
| 121 | - | func(string, []string, []string, string, domain.Size) (ports.PTY, error) { | 156 | + | func(_ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { |
| 122 | mu.Lock() | 157 | mu.Lock() | ||
| 123 | defer mu.Unlock() | 158 | defer mu.Unlock() | ||
| 159 | + | if !normal.Valid() { | |||
| 160 | + | normal = sz | |||
| 161 | + | } | |||
| 162 | + | if sz != normal { | |||
| 163 | + | return newQuietPTY(), nil | |||
| 164 | + | } | |||
| 124 | if next >= len(ptys) { | 165 | if next >= len(ptys) { | ||
| 125 | - | t.Fatalf("unexpected PTY open #%d", next+1) | 166 | + | return newQuietPTY(), nil |
| 126 | } | 167 | } | ||
| 127 | p := ptys[next] | 168 | p := ptys[next] | ||
| 128 | next++ | 169 | next++ | ||
M internal/usecase/daemon/floating.go +24 -1
| @@ -133,6 +133,26 @@ func (d *Daemon) ensureFloatingWarm(sess *session, tb *tab) { | |||
| 133 | 133 | d.startFloating(sess, tb, false) | |
| 134 | 134 | } | |
| 135 | 135 | ||
| 136 | + | // activateTab performs the work associated with making a tab the destination. | |
| 137 | + | // It is deliberately idempotent: every tab transition can use this single | |
| 138 | + | // hook, while inactive restored tabs remain cold until actually selected. | |
| 139 | + | func (d *Daemon) activateTab(sess *session, tb *tab) { | |
| 140 | + | if d == nil || sess == nil || tb == nil { | |
| 141 | + | return | |
| 142 | + | } | |
| 143 | + | // A headless session has no actual terminal destination. Deferring warmup | |
| 144 | + | // until firstPaint keeps restored tabs cold and avoids launching children | |
| 145 | + | // merely because a tab was manipulated during teardown. | |
| 146 | + | sess.mu.Lock() | |
| 147 | + | attached := sess.client != nil | |
| 148 | + | sess.mu.Unlock() | |
| 149 | + | if !attached { | |
| 150 | + | return | |
| 151 | + | } | |
| 152 | + | d.ensureFloatingWarm(sess, tb) | |
| 153 | + | d.resizeInstalledFloating(tb) | |
| 154 | + | } | |
| 155 | + | ||
| 136 | 156 | // toggleFloating changes only this tab's slot. Opening an uninitialized slot | |
| 137 | 157 | // launches asynchronously; installed slots are retained when hidden. | |
| 138 | 158 | func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error { | |
| @@ -151,7 +171,10 @@ func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error { | |||
| 151 | 171 | visible := tb.floating.state == floatingVisible | |
| 152 | 172 | tb.mu.Unlock() | |
| 153 | 173 | if start { | |
| 154 | - | d.launchFloating(sess, tb, cfg, generation, visible) | |
| 174 | + | d.launchFloating(sess, tb, cfg, generation, true) | |
| 175 | + | if ac != nil { | |
| 176 | + | d.paint(sess, ac, true) | |
| 177 | + | } | |
| 155 | 178 | return nil | |
| 156 | 179 | } | |
| 157 | 180 | if visible { | |
| @@ -133,6 +133,26 @@ func (d *Daemon) ensureFloatingWarm(sess *session, tb *tab) { | |||||
| 133 | d.startFloating(sess, tb, false) | 133 | d.startFloating(sess, tb, false) | ||
| 134 | } | 134 | } | ||
| 135 | 135 | ||||
| 136 | + | // activateTab performs the work associated with making a tab the destination. | |||
| 137 | + | // It is deliberately idempotent: every tab transition can use this single | |||
| 138 | + | // hook, while inactive restored tabs remain cold until actually selected. | |||
| 139 | + | func (d *Daemon) activateTab(sess *session, tb *tab) { | |||
| 140 | + | if d == nil || sess == nil || tb == nil { | |||
| 141 | + | return | |||
| 142 | + | } | |||
| 143 | + | // A headless session has no actual terminal destination. Deferring warmup | |||
| 144 | + | // until firstPaint keeps restored tabs cold and avoids launching children | |||
| 145 | + | // merely because a tab was manipulated during teardown. | |||
| 146 | + | sess.mu.Lock() | |||
| 147 | + | attached := sess.client != nil | |||
| 148 | + | sess.mu.Unlock() | |||
| 149 | + | if !attached { | |||
| 150 | + | return | |||
| 151 | + | } | |||
| 152 | + | d.ensureFloatingWarm(sess, tb) | |||
| 153 | + | d.resizeInstalledFloating(tb) | |||
| 154 | + | } | |||
| 155 | + | ||||
| 136 | // toggleFloating changes only this tab's slot. Opening an uninitialized slot | 156 | // toggleFloating changes only this tab's slot. Opening an uninitialized slot | ||
| 137 | // launches asynchronously; installed slots are retained when hidden. | 157 | // launches asynchronously; installed slots are retained when hidden. | ||
| 138 | func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error { | 158 | func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error { | ||
| @@ -151,7 +171,10 @@ func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error { | |||||
| 151 | visible := tb.floating.state == floatingVisible | 171 | visible := tb.floating.state == floatingVisible | ||
| 152 | tb.mu.Unlock() | 172 | tb.mu.Unlock() | ||
| 153 | if start { | 173 | if start { | ||
| 154 | - | d.launchFloating(sess, tb, cfg, generation, visible) | 174 | + | d.launchFloating(sess, tb, cfg, generation, true) |
| 175 | + | if ac != nil { | |||
| 176 | + | d.paint(sess, ac, true) | |||
| 177 | + | } | |||
| 155 | return nil | 178 | return nil | ||
| 156 | } | 179 | } | ||
| 157 | if visible { | 180 | if visible { | ||
M internal/usecase/daemon/floating_render.go +18 -5
| @@ -167,19 +167,32 @@ func (d *Daemon) resizeFloatingPane(p *pane, inner domain.Rect) bool { | |||
| 167 | 167 | return true | |
| 168 | 168 | } | |
| 169 | 169 | ||
| 170 | - | // resizeActiveFloating updates only a visible floating pane. Hidden panes | |
| 171 | - | // retain their remembered geometry until they are shown again. | |
| 172 | - | func (d *Daemon) resizeActiveFloating(tb *tab) bool { | |
| 170 | + | // resizeInstalledFloating updates an installed floating pane, including a | |
| 171 | + | // retained hidden pane when its tab becomes active. | |
| 172 | + | func (d *Daemon) resizeInstalledFloating(tb *tab) bool { | |
| 173 | 173 | if d == nil || tb == nil { | |
| 174 | 174 | return false | |
| 175 | 175 | } | |
| 176 | 176 | tb.mu.Lock() | |
| 177 | - | visible := tb.floating.state == floatingVisible | |
| 178 | 177 | p := tb.floating.pane | |
| 179 | 178 | content := domain.Rect{Width: tb.size.Cols, Height: tb.size.Rows} | |
| 180 | 179 | tb.mu.Unlock() | |
| 181 | - | if !visible || p == nil { | |
| 180 | + | if p == nil { | |
| 182 | 181 | return false | |
| 183 | 182 | } | |
| 184 | 183 | return d.resizeFloatingPane(p, calculateFloatingGeometry(content, d.currentFloatingConfig()).Inner) | |
| 185 | 184 | } | |
| 185 | + | ||
| 186 | + | // resizeActiveFloating updates a visible floating pane during a client resize. | |
| 187 | + | func (d *Daemon) resizeActiveFloating(tb *tab) bool { | |
| 188 | + | if d == nil || tb == nil { | |
| 189 | + | return false | |
| 190 | + | } | |
| 191 | + | tb.mu.Lock() | |
| 192 | + | visible := tb.floating.state == floatingVisible | |
| 193 | + | tb.mu.Unlock() | |
| 194 | + | if !visible { | |
| 195 | + | return false | |
| 196 | + | } | |
| 197 | + | return d.resizeInstalledFloating(tb) | |
| 198 | + | } | |
| @@ -167,19 +167,32 @@ func (d *Daemon) resizeFloatingPane(p *pane, inner domain.Rect) bool { | |||||
| 167 | return true | 167 | return true | ||
| 168 | } | 168 | } | ||
| 169 | 169 | ||||
| 170 | - | // resizeActiveFloating updates only a visible floating pane. Hidden panes | 170 | + | // resizeInstalledFloating updates an installed floating pane, including a |
| 171 | - | // retain their remembered geometry until they are shown again. | 171 | + | // retained hidden pane when its tab becomes active. |
| 172 | - | func (d *Daemon) resizeActiveFloating(tb *tab) bool { | 172 | + | func (d *Daemon) resizeInstalledFloating(tb *tab) bool { |
| 173 | if d == nil || tb == nil { | 173 | if d == nil || tb == nil { | ||
| 174 | return false | 174 | return false | ||
| 175 | } | 175 | } | ||
| 176 | tb.mu.Lock() | 176 | tb.mu.Lock() | ||
| 177 | - | visible := tb.floating.state == floatingVisible | |||
| 178 | p := tb.floating.pane | 177 | p := tb.floating.pane | ||
| 179 | content := domain.Rect{Width: tb.size.Cols, Height: tb.size.Rows} | 178 | content := domain.Rect{Width: tb.size.Cols, Height: tb.size.Rows} | ||
| 180 | tb.mu.Unlock() | 179 | tb.mu.Unlock() | ||
| 181 | - | if !visible || p == nil { | 180 | + | if p == nil { |
| 182 | return false | 181 | return false | ||
| 183 | } | 182 | } | ||
| 184 | return d.resizeFloatingPane(p, calculateFloatingGeometry(content, d.currentFloatingConfig()).Inner) | 183 | return d.resizeFloatingPane(p, calculateFloatingGeometry(content, d.currentFloatingConfig()).Inner) | ||
| 185 | } | 184 | } | ||
| 185 | + | ||||
| 186 | + | // resizeActiveFloating updates a visible floating pane during a client resize. | |||
| 187 | + | func (d *Daemon) resizeActiveFloating(tb *tab) bool { | |||
| 188 | + | if d == nil || tb == nil { | |||
| 189 | + | return false | |||
| 190 | + | } | |||
| 191 | + | tb.mu.Lock() | |||
| 192 | + | visible := tb.floating.state == floatingVisible | |||
| 193 | + | tb.mu.Unlock() | |||
| 194 | + | if !visible { | |||
| 195 | + | return false | |||
| 196 | + | } | |||
| 197 | + | return d.resizeInstalledFloating(tb) | |||
| 198 | + | } | |||
M internal/usecase/daemon/input.go +5 -0
| @@ -363,6 +363,10 @@ func (h daemonKeyHandler) Action(action keys.Action) { | |||
| 363 | 363 | h.d.enterPalette(sess, h.ac) | |
| 364 | 364 | case keys.ActionJumpAttention: | |
| 365 | 365 | h.d.jumpAttention(sess, h.ac) | |
| 366 | + | case keys.ActionToggleFloatingPane: | |
| 367 | + | if err := h.d.toggleFloating(sess, h.ac); err != nil { | |
| 368 | + | h.d.log.Warn("toggle floating pane failed", "err", err) | |
| 369 | + | } | |
| 366 | 370 | case keys.ActionFocusPaneLeft: | |
| 367 | 371 | _ = h.d.focusDir(sess, h.ac, layout.Left) | |
| 368 | 372 | case keys.ActionFocusPaneRight: | |
| @@ -376,6 +380,7 @@ func (h daemonKeyHandler) Action(action keys.Action) { | |||
| 376 | 380 | keys.ActionSwitchTab7, keys.ActionSwitchTab8, keys.ActionSwitchTab9: | |
| 377 | 381 | idx := int(action - keys.ActionSwitchTab1) | |
| 378 | 382 | if sess.switchTab(idx) { | |
| 383 | + | h.d.activateTab(sess, sess.activeTab()) | |
| 379 | 384 | h.d.paint(sess, h.ac, true) | |
| 380 | 385 | } | |
| 381 | 386 | } | |
| @@ -363,6 +363,10 @@ func (h daemonKeyHandler) Action(action keys.Action) { | |||||
| 363 | h.d.enterPalette(sess, h.ac) | 363 | h.d.enterPalette(sess, h.ac) | ||
| 364 | case keys.ActionJumpAttention: | 364 | case keys.ActionJumpAttention: | ||
| 365 | h.d.jumpAttention(sess, h.ac) | 365 | h.d.jumpAttention(sess, h.ac) | ||
| 366 | + | case keys.ActionToggleFloatingPane: | |||
| 367 | + | if err := h.d.toggleFloating(sess, h.ac); err != nil { | |||
| 368 | + | h.d.log.Warn("toggle floating pane failed", "err", err) | |||
| 369 | + | } | |||
| 366 | case keys.ActionFocusPaneLeft: | 370 | case keys.ActionFocusPaneLeft: | ||
| 367 | _ = h.d.focusDir(sess, h.ac, layout.Left) | 371 | _ = h.d.focusDir(sess, h.ac, layout.Left) | ||
| 368 | case keys.ActionFocusPaneRight: | 372 | case keys.ActionFocusPaneRight: | ||
| @@ -376,6 +380,7 @@ func (h daemonKeyHandler) Action(action keys.Action) { | |||||
| 376 | keys.ActionSwitchTab7, keys.ActionSwitchTab8, keys.ActionSwitchTab9: | 380 | keys.ActionSwitchTab7, keys.ActionSwitchTab8, keys.ActionSwitchTab9: | ||
| 377 | idx := int(action - keys.ActionSwitchTab1) | 381 | idx := int(action - keys.ActionSwitchTab1) | ||
| 378 | if sess.switchTab(idx) { | 382 | if sess.switchTab(idx) { | ||
| 383 | + | h.d.activateTab(sess, sess.activeTab()) | |||
| 379 | h.d.paint(sess, h.ac, true) | 384 | h.d.paint(sess, h.ac, true) | ||
| 380 | } | 385 | } | ||
| 381 | } | 386 | } | ||
M internal/usecase/daemon/palette.go +10 -2
| @@ -230,17 +230,25 @@ func (e paletteExec) FocusPaneDown() error { | |||
| 230 | 230 | } | |
| 231 | 231 | ||
| 232 | 232 | func (e paletteExec) NextTab() error { | |
| 233 | - | e.sess.switchRelative(1) | |
| 233 | + | if e.sess.switchRelative(1) { | |
| 234 | + | e.d.activateTab(e.sess, e.sess.activeTab()) | |
| 235 | + | } | |
| 234 | 236 | e.d.paint(e.sess, e.ac, true) | |
| 235 | 237 | return nil | |
| 236 | 238 | } | |
| 237 | 239 | ||
| 238 | 240 | func (e paletteExec) PrevTab() error { | |
| 239 | - | e.sess.switchRelative(-1) | |
| 241 | + | if e.sess.switchRelative(-1) { | |
| 242 | + | e.d.activateTab(e.sess, e.sess.activeTab()) | |
| 243 | + | } | |
| 240 | 244 | e.d.paint(e.sess, e.ac, true) | |
| 241 | 245 | return nil | |
| 242 | 246 | } | |
| 243 | 247 | ||
| 248 | + | func (e paletteExec) ToggleFloatingPane() error { | |
| 249 | + | return e.d.toggleFloating(e.sess, e.ac) | |
| 250 | + | } | |
| 251 | + | ||
| 244 | 252 | func (e paletteExec) BackSession() error { | |
| 245 | 253 | e.d.navigateRecentSession(e.sess, e.ac, 1) | |
| 246 | 254 | return nil | |
| @@ -230,17 +230,25 @@ func (e paletteExec) FocusPaneDown() error { | |||||
| 230 | } | 230 | } | ||
| 231 | 231 | ||||
| 232 | func (e paletteExec) NextTab() error { | 232 | func (e paletteExec) NextTab() error { | ||
| 233 | - | e.sess.switchRelative(1) | 233 | + | if e.sess.switchRelative(1) { |
| 234 | + | e.d.activateTab(e.sess, e.sess.activeTab()) | |||
| 235 | + | } | |||
| 234 | e.d.paint(e.sess, e.ac, true) | 236 | e.d.paint(e.sess, e.ac, true) | ||
| 235 | return nil | 237 | return nil | ||
| 236 | } | 238 | } | ||
| 237 | 239 | ||||
| 238 | func (e paletteExec) PrevTab() error { | 240 | func (e paletteExec) PrevTab() error { | ||
| 239 | - | e.sess.switchRelative(-1) | 241 | + | if e.sess.switchRelative(-1) { |
| 242 | + | e.d.activateTab(e.sess, e.sess.activeTab()) | |||
| 243 | + | } | |||
| 240 | e.d.paint(e.sess, e.ac, true) | 244 | e.d.paint(e.sess, e.ac, true) | ||
| 241 | return nil | 245 | return nil | ||
| 242 | } | 246 | } | ||
| 243 | 247 | ||||
| 248 | + | func (e paletteExec) ToggleFloatingPane() error { | |||
| 249 | + | return e.d.toggleFloating(e.sess, e.ac) | |||
| 250 | + | } | |||
| 251 | + | ||||
| 244 | func (e paletteExec) BackSession() error { | 252 | func (e paletteExec) BackSession() error { | ||
| 245 | e.d.navigateRecentSession(e.sess, e.ac, 1) | 253 | e.d.navigateRecentSession(e.sess, e.ac, 1) | ||
| 246 | return nil | 254 | return nil | ||
M internal/usecase/daemon/palette_test.go +3 -5
| @@ -46,9 +46,7 @@ func TestPaletteCNSPromptsForSessionNameThenCreatesAndSwitches(t *testing.T) { | |||
| 46 | 46 | d, sess, ac, sends := newManualSessionWithPTYs(t, p1) | |
| 47 | 47 | defer release1() | |
| 48 | 48 | defer release2() | |
| 49 | - | ptys := portsmocks.NewMockPTYFactory(t) | |
| 50 | - | ptys.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(p2, nil).Once() | |
| 51 | - | d.ptys = ptys | |
| 49 | + | d.ptys = newFactorySeq(t, p2) | |
| 52 | 50 | ||
| 53 | 51 | d.handleInput(sess, ac, []byte("\x1b ")) | |
| 54 | 52 | awaitFrame(t, sends, ports.MsgOutput) | |
| @@ -112,12 +110,12 @@ func TestPaletteRecentCommandsNewestFirstThenRegistryOrder(t *testing.T) { | |||
| 112 | 110 | for i, cmd := range commands { | |
| 113 | 111 | codes[i] = cmd.Code | |
| 114 | 112 | } | |
| 115 | - | require.Equal(t, []string{"SSP", "NXT", "CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "PVT", "BSK", "FSK", "VIS", "RNS", "RNT", "DET"}, codes) | |
| 113 | + | require.Equal(t, []string{"SSP", "NXT", "CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "FLT", "CLP", "FPL", "FPR", "FPU", "FPD", "PVT", "BSK", "FSK", "VIS", "RNS", "RNT", "DET"}, codes) | |
| 116 | 114 | } | |
| 117 | 115 | ||
| 118 | 116 | func TestPaletteRecencyCanBeUpdatedConcurrently(t *testing.T) { | |
| 119 | 117 | d := &Daemon{} | |
| 120 | - | codes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "BSK", "FSK", "SSP", "VIS", "RNS", "RNT", "DET"} | |
| 118 | + | codes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "FLT", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "BSK", "FSK", "SSP", "VIS", "RNS", "RNT", "DET"} | |
| 121 | 119 | ||
| 122 | 120 | var wg sync.WaitGroup | |
| 123 | 121 | for range 50 { | |
| @@ -46,9 +46,7 @@ func TestPaletteCNSPromptsForSessionNameThenCreatesAndSwitches(t *testing.T) { | |||||
| 46 | d, sess, ac, sends := newManualSessionWithPTYs(t, p1) | 46 | d, sess, ac, sends := newManualSessionWithPTYs(t, p1) | ||
| 47 | defer release1() | 47 | defer release1() | ||
| 48 | defer release2() | 48 | defer release2() | ||
| 49 | - | ptys := portsmocks.NewMockPTYFactory(t) | 49 | + | d.ptys = newFactorySeq(t, p2) |
| 50 | - | ptys.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(p2, nil).Once() | |||
| 51 | - | d.ptys = ptys | |||
| 52 | 50 | ||||
| 53 | d.handleInput(sess, ac, []byte("\x1b ")) | 51 | d.handleInput(sess, ac, []byte("\x1b ")) | ||
| 54 | awaitFrame(t, sends, ports.MsgOutput) | 52 | awaitFrame(t, sends, ports.MsgOutput) | ||
| @@ -112,12 +110,12 @@ func TestPaletteRecentCommandsNewestFirstThenRegistryOrder(t *testing.T) { | |||||
| 112 | for i, cmd := range commands { | 110 | for i, cmd := range commands { | ||
| 113 | codes[i] = cmd.Code | 111 | codes[i] = cmd.Code | ||
| 114 | } | 112 | } | ||
| 115 | - | require.Equal(t, []string{"SSP", "NXT", "CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "PVT", "BSK", "FSK", "VIS", "RNS", "RNT", "DET"}, codes) | 113 | + | require.Equal(t, []string{"SSP", "NXT", "CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "FLT", "CLP", "FPL", "FPR", "FPU", "FPD", "PVT", "BSK", "FSK", "VIS", "RNS", "RNT", "DET"}, codes) |
| 116 | } | 114 | } | ||
| 117 | 115 | ||||
| 118 | func TestPaletteRecencyCanBeUpdatedConcurrently(t *testing.T) { | 116 | func TestPaletteRecencyCanBeUpdatedConcurrently(t *testing.T) { | ||
| 119 | d := &Daemon{} | 117 | d := &Daemon{} | ||
| 120 | - | codes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "BSK", "FSK", "SSP", "VIS", "RNS", "RNT", "DET"} | 118 | + | codes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "FLT", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "BSK", "FSK", "SSP", "VIS", "RNS", "RNT", "DET"} |
| 121 | 119 | ||||
| 122 | var wg sync.WaitGroup | 120 | var wg sync.WaitGroup | ||
| 123 | for range 50 { | 121 | for range 50 { | ||
M internal/usecase/daemon/picker.go +3 -1
| @@ -360,7 +360,9 @@ func (d *Daemon) switchToTarget(sess *session, ac *attachedClient, target picker | |||
| 360 | 360 | return | |
| 361 | 361 | } | |
| 362 | 362 | if targetSess == sess { | |
| 363 | - | sess.switchTab(target.TabIndex) | |
| 363 | + | if sess.switchTab(target.TabIndex) { | |
| 364 | + | d.activateTab(sess, sess.activeTab()) | |
| 365 | + | } | |
| 364 | 366 | d.paint(sess, ac, true) | |
| 365 | 367 | return | |
| 366 | 368 | } | |
| @@ -360,7 +360,9 @@ func (d *Daemon) switchToTarget(sess *session, ac *attachedClient, target picker | |||||
| 360 | return | 360 | return | ||
| 361 | } | 361 | } | ||
| 362 | if targetSess == sess { | 362 | if targetSess == sess { | ||
| 363 | - | sess.switchTab(target.TabIndex) | 363 | + | if sess.switchTab(target.TabIndex) { |
| 364 | + | d.activateTab(sess, sess.activeTab()) | |||
| 365 | + | } | |||
| 364 | d.paint(sess, ac, true) | 366 | d.paint(sess, ac, true) | ||
| 365 | return | 367 | return | ||
| 366 | } | 368 | } | ||
M internal/usecase/daemon/picker_test.go +21 -4
| @@ -315,11 +315,14 @@ func TestPickerCrossSessionSwitchCopiesTerminalEnvForFutureTabs(t *testing.T) { | |||
| 315 | 315 | var openedEnv []string | |
| 316 | 316 | f := portsmocks.NewMockPTYFactory(t) | |
| 317 | 317 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 318 | - | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 318 | + | func(_ string, _ []string, env []string, _ string, sz domain.Size) (ports.PTY, error) { | |
| 319 | + | if sz != (domain.Size{Cols: 80, Rows: 22}) { | |
| 320 | + | return newQuietPTY(), nil | |
| 321 | + | } | |
| 319 | 322 | openedEnv = append([]string(nil), env...) | |
| 320 | 323 | return p3, nil | |
| 321 | 324 | }, | |
| 322 | - | ).Once() | |
| 325 | + | ).Maybe() | |
| 323 | 326 | d := newTestDaemon(t, f, stubClock{}) | |
| 324 | 327 | tr1, _ := newCapturingTransport(t) | |
| 325 | 328 | tr2, _ := newCapturingTransport(t) | |
| @@ -532,7 +535,8 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 532 | 535 | defer release2() | |
| 533 | 536 | var opens [][]string | |
| 534 | 537 | f := portsmocks.NewMockPTYFactory(t) | |
| 535 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 538 | + | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | |
| 539 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 536 | 540 | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 537 | 541 | opens = append(opens, append([]string(nil), env...)) | |
| 538 | 542 | if len(opens) == 1 { | |
| @@ -541,6 +545,10 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 541 | 545 | return p2, nil | |
| 542 | 546 | }, | |
| 543 | 547 | ).Twice() | |
| 548 | + | floating := newQuietPTY() | |
| 549 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 550 | + | return got != normalSize && got.Valid() | |
| 551 | + | })).Return(floating, nil).Once() | |
| 544 | 552 | d := newTestDaemon(t, f, stubClock{}) | |
| 545 | 553 | d.stopped["old"] = stoppedSession{name: "old", cwd: t.TempDir(), createdAt: 1} | |
| 546 | 554 | tr := portsmocks.NewMockTransport(t) | |
| @@ -552,10 +560,19 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 552 | 560 | got := ac.sess.Get() | |
| 553 | 561 | require.NotNil(t, got) | |
| 554 | 562 | got.mu.Lock() | |
| 555 | - | defer got.mu.Unlock() | |
| 556 | 563 | require.True(t, got.terminal.TrueColor) | |
| 564 | + | got.mu.Unlock() | |
| 557 | 565 | require.Len(t, opens, 2) | |
| 558 | 566 | require.Contains(t, opens[1], "TERM=xterm-direct") | |
| 559 | 567 | require.Contains(t, opens[1], "COLORTERM=truecolor") | |
| 560 | 568 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | |
| 569 | + | _ = d.killSession(got, ports.ReasonSessionKilled, false) | |
| 570 | + | release1() | |
| 571 | + | release2() | |
| 572 | + | d.sessWg.Wait() | |
| 573 | + | select { | |
| 574 | + | case <-floating.done: | |
| 575 | + | default: | |
| 576 | + | t.Fatal("floating prewarm PTY was not closed") | |
| 577 | + | } | |
| 561 | 578 | } | |
| @@ -315,11 +315,14 @@ func TestPickerCrossSessionSwitchCopiesTerminalEnvForFutureTabs(t *testing.T) { | |||||
| 315 | var openedEnv []string | 315 | var openedEnv []string | ||
| 316 | f := portsmocks.NewMockPTYFactory(t) | 316 | f := portsmocks.NewMockPTYFactory(t) | ||
| 317 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 317 | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | ||
| 318 | - | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 318 | + | func(_ string, _ []string, env []string, _ string, sz domain.Size) (ports.PTY, error) { |
| 319 | + | if sz != (domain.Size{Cols: 80, Rows: 22}) { | |||
| 320 | + | return newQuietPTY(), nil | |||
| 321 | + | } | |||
| 319 | openedEnv = append([]string(nil), env...) | 322 | openedEnv = append([]string(nil), env...) | ||
| 320 | return p3, nil | 323 | return p3, nil | ||
| 321 | }, | 324 | }, | ||
| 322 | - | ).Once() | 325 | + | ).Maybe() |
| 323 | d := newTestDaemon(t, f, stubClock{}) | 326 | d := newTestDaemon(t, f, stubClock{}) | ||
| 324 | tr1, _ := newCapturingTransport(t) | 327 | tr1, _ := newCapturingTransport(t) | ||
| 325 | tr2, _ := newCapturingTransport(t) | 328 | tr2, _ := newCapturingTransport(t) | ||
| @@ -532,7 +535,8 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 532 | defer release2() | 535 | defer release2() | ||
| 533 | var opens [][]string | 536 | var opens [][]string | ||
| 534 | f := portsmocks.NewMockPTYFactory(t) | 537 | f := portsmocks.NewMockPTYFactory(t) | ||
| 535 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 538 | + | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} |
| 539 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |||
| 536 | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 540 | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | ||
| 537 | opens = append(opens, append([]string(nil), env...)) | 541 | opens = append(opens, append([]string(nil), env...)) | ||
| 538 | if len(opens) == 1 { | 542 | if len(opens) == 1 { | ||
| @@ -541,6 +545,10 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 541 | return p2, nil | 545 | return p2, nil | ||
| 542 | }, | 546 | }, | ||
| 543 | ).Twice() | 547 | ).Twice() | ||
| 548 | + | floating := newQuietPTY() | |||
| 549 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |||
| 550 | + | return got != normalSize && got.Valid() | |||
| 551 | + | })).Return(floating, nil).Once() | |||
| 544 | d := newTestDaemon(t, f, stubClock{}) | 552 | d := newTestDaemon(t, f, stubClock{}) | ||
| 545 | d.stopped["old"] = stoppedSession{name: "old", cwd: t.TempDir(), createdAt: 1} | 553 | d.stopped["old"] = stoppedSession{name: "old", cwd: t.TempDir(), createdAt: 1} | ||
| 546 | tr := portsmocks.NewMockTransport(t) | 554 | tr := portsmocks.NewMockTransport(t) | ||
| @@ -552,10 +560,19 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 552 | got := ac.sess.Get() | 560 | got := ac.sess.Get() | ||
| 553 | require.NotNil(t, got) | 561 | require.NotNil(t, got) | ||
| 554 | got.mu.Lock() | 562 | got.mu.Lock() | ||
| 555 | - | defer got.mu.Unlock() | |||
| 556 | require.True(t, got.terminal.TrueColor) | 563 | require.True(t, got.terminal.TrueColor) | ||
| 564 | + | got.mu.Unlock() | |||
| 557 | require.Len(t, opens, 2) | 565 | require.Len(t, opens, 2) | ||
| 558 | require.Contains(t, opens[1], "TERM=xterm-direct") | 566 | require.Contains(t, opens[1], "TERM=xterm-direct") | ||
| 559 | require.Contains(t, opens[1], "COLORTERM=truecolor") | 567 | require.Contains(t, opens[1], "COLORTERM=truecolor") | ||
| 560 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | 568 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | ||
| 569 | + | _ = d.killSession(got, ports.ReasonSessionKilled, false) | |||
| 570 | + | release1() | |||
| 571 | + | release2() | |||
| 572 | + | d.sessWg.Wait() | |||
| 573 | + | select { | |||
| 574 | + | case <-floating.done: | |||
| 575 | + | default: | |||
| 576 | + | t.Fatal("floating prewarm PTY was not closed") | |||
| 577 | + | } | |||
| 561 | } | 578 | } | ||
M internal/usecase/daemon/session.go +6 -0
| @@ -332,6 +332,7 @@ func (d *Daemon) createTab(sess *session, sz domain.Size) error { | |||
| 332 | 332 | d.log.Info("tab created", "session", name, "tab", tabIndex) | |
| 333 | 333 | d.startTabGoroutines(sess, tb) | |
| 334 | 334 | d.mu.Unlock() | |
| 335 | + | d.activateTab(sess, tb) | |
| 335 | 336 | markSnapshotDirty(sess) | |
| 336 | 337 | return nil | |
| 337 | 338 | } | |
| @@ -610,12 +611,14 @@ func (d *Daemon) closeTab(sess *session, tb *tab, repaint bool) { | |||
| 610 | 611 | return | |
| 611 | 612 | } | |
| 612 | 613 | ringing := tb.attention | |
| 614 | + | wasActive := idx == sess.active | |
| 613 | 615 | sess.tabs = append(sess.tabs[:idx], sess.tabs[idx+1:]...) | |
| 614 | 616 | if sess.active >= len(sess.tabs) { | |
| 615 | 617 | sess.active = len(sess.tabs) - 1 | |
| 616 | 618 | } else if idx < sess.active { | |
| 617 | 619 | sess.active-- | |
| 618 | 620 | } | |
| 621 | + | destination := sess.tabs[sess.active] | |
| 619 | 622 | ac := sess.client | |
| 620 | 623 | name := sess.name | |
| 621 | 624 | record := sess.persistRecordLocked(time.Now().UnixNano()) | |
| @@ -635,6 +638,9 @@ func (d *Daemon) closeTab(sess *session, tb *tab, repaint bool) { | |||
| 635 | 638 | tb.cancel() | |
| 636 | 639 | } | |
| 637 | 640 | tb.closeAllPanes() | |
| 641 | + | if wasActive { | |
| 642 | + | d.activateTab(sess, destination) | |
| 643 | + | } | |
| 638 | 644 | if repaint && ac != nil { | |
| 639 | 645 | d.paint(sess, ac, true) | |
| 640 | 646 | } | |
| @@ -332,6 +332,7 @@ func (d *Daemon) createTab(sess *session, sz domain.Size) error { | |||||
| 332 | d.log.Info("tab created", "session", name, "tab", tabIndex) | 332 | d.log.Info("tab created", "session", name, "tab", tabIndex) | ||
| 333 | d.startTabGoroutines(sess, tb) | 333 | d.startTabGoroutines(sess, tb) | ||
| 334 | d.mu.Unlock() | 334 | d.mu.Unlock() | ||
| 335 | + | d.activateTab(sess, tb) | |||
| 335 | markSnapshotDirty(sess) | 336 | markSnapshotDirty(sess) | ||
| 336 | return nil | 337 | return nil | ||
| 337 | } | 338 | } | ||
| @@ -610,12 +611,14 @@ func (d *Daemon) closeTab(sess *session, tb *tab, repaint bool) { | |||||
| 610 | return | 611 | return | ||
| 611 | } | 612 | } | ||
| 612 | ringing := tb.attention | 613 | ringing := tb.attention | ||
| 614 | + | wasActive := idx == sess.active | |||
| 613 | sess.tabs = append(sess.tabs[:idx], sess.tabs[idx+1:]...) | 615 | sess.tabs = append(sess.tabs[:idx], sess.tabs[idx+1:]...) | ||
| 614 | if sess.active >= len(sess.tabs) { | 616 | if sess.active >= len(sess.tabs) { | ||
| 615 | sess.active = len(sess.tabs) - 1 | 617 | sess.active = len(sess.tabs) - 1 | ||
| 616 | } else if idx < sess.active { | 618 | } else if idx < sess.active { | ||
| 617 | sess.active-- | 619 | sess.active-- | ||
| 618 | } | 620 | } | ||
| 621 | + | destination := sess.tabs[sess.active] | |||
| 619 | ac := sess.client | 622 | ac := sess.client | ||
| 620 | name := sess.name | 623 | name := sess.name | ||
| 621 | record := sess.persistRecordLocked(time.Now().UnixNano()) | 624 | record := sess.persistRecordLocked(time.Now().UnixNano()) | ||
| @@ -635,6 +638,9 @@ func (d *Daemon) closeTab(sess *session, tb *tab, repaint bool) { | |||||
| 635 | tb.cancel() | 638 | tb.cancel() | ||
| 636 | } | 639 | } | ||
| 637 | tb.closeAllPanes() | 640 | tb.closeAllPanes() | ||
| 641 | + | if wasActive { | |||
| 642 | + | d.activateTab(sess, destination) | |||
| 643 | + | } | |||
| 638 | if repaint && ac != nil { | 644 | if repaint && ac != nil { | ||
| 639 | d.paint(sess, ac, true) | 645 | d.paint(sess, ac, true) | ||
| 640 | } | 646 | } | ||
M internal/usecase/daemon/session_test.go +50 -7
| @@ -33,7 +33,8 @@ func TestRoutePropagatesHelloCwdAndTabsInheritIt(t *testing.T) { | |||
| 33 | 33 | defer releaseSecond() | |
| 34 | 34 | f := portsmocks.NewMockPTYFactory(t) | |
| 35 | 35 | var dirs []string | |
| 36 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 36 | + | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | |
| 37 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 37 | 38 | func(_ string, _ []string, _ []string, dir string, _ domain.Size) (ports.PTY, error) { | |
| 38 | 39 | dirs = append(dirs, dir) | |
| 39 | 40 | if len(dirs) == 1 { | |
| @@ -42,6 +43,10 @@ func TestRoutePropagatesHelloCwdAndTabsInheritIt(t *testing.T) { | |||
| 42 | 43 | return second, nil | |
| 43 | 44 | }, | |
| 44 | 45 | ).Twice() | |
| 46 | + | floating := newQuietPTY() | |
| 47 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 48 | + | return got != normalSize && got.Valid() | |
| 49 | + | })).Return(floating, nil).Once() | |
| 45 | 50 | ||
| 46 | 51 | d := newTestDaemon(t, f, stubClock{}) | |
| 47 | 52 | tr := portsmocks.NewMockTransport(t) | |
| @@ -62,6 +67,15 @@ func TestRoutePropagatesHelloCwdAndTabsInheritIt(t *testing.T) { | |||
| 62 | 67 | ||
| 63 | 68 | require.NoError(t, d.createTab(sess, sz)) | |
| 64 | 69 | require.Equal(t, []string{"/tmp/work", "/tmp/work"}, dirs) | |
| 70 | + | _ = d.killSession(sess, ports.ReasonSessionKilled, false) | |
| 71 | + | releaseFirst() | |
| 72 | + | releaseSecond() | |
| 73 | + | d.sessWg.Wait() | |
| 74 | + | select { | |
| 75 | + | case <-floating.done: | |
| 76 | + | default: | |
| 77 | + | t.Fatal("floating prewarm PTY was not closed") | |
| 78 | + | } | |
| 65 | 79 | } | |
| 66 | 80 | ||
| 67 | 81 | func TestHandshakeEphemeralHappy(t *testing.T) { | |
| @@ -197,8 +211,13 @@ func TestCreateTabClosesPTYIfSessionKilledDuringOpen(t *testing.T) { | |||
| 197 | 211 | p2.EXPECT().Pid().Return(4242).Maybe() | |
| 198 | 212 | ||
| 199 | 213 | f := portsmocks.NewMockPTYFactory(t) | |
| 200 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(p1, nil).Once() | |
| 201 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 214 | + | normalSize := domain.Size{Cols: 80, Rows: 22} | |
| 215 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).Return(p1, nil).Once() | |
| 216 | + | floating := newQuietPTY() | |
| 217 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 218 | + | return got != normalSize && got.Valid() | |
| 219 | + | })).Return(floating, nil).Once() | |
| 220 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 202 | 221 | func(string, []string, []string, string, domain.Size) (ports.PTY, error) { | |
| 203 | 222 | close(opened) | |
| 204 | 223 | <-releaseOpen | |
| @@ -231,6 +250,11 @@ func TestCreateTabClosesPTYIfSessionKilledDuringOpen(t *testing.T) { | |||
| 231 | 250 | releaseConn() | |
| 232 | 251 | hg.Wait() | |
| 233 | 252 | d.sessWg.Wait() | |
| 253 | + | select { | |
| 254 | + | case <-floating.done: | |
| 255 | + | default: | |
| 256 | + | t.Fatal("floating prewarm PTY was not closed") | |
| 257 | + | } | |
| 234 | 258 | } | |
| 235 | 259 | ||
| 236 | 260 | // --- ephemeral numbering ---------------------------------------------------- | |
| @@ -1194,7 +1218,8 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||
| 1194 | 1218 | ||
| 1195 | 1219 | var opens [][]string | |
| 1196 | 1220 | f := portsmocks.NewMockPTYFactory(t) | |
| 1197 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 1221 | + | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | |
| 1222 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 1198 | 1223 | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1199 | 1224 | opens = append(opens, append([]string(nil), env...)) | |
| 1200 | 1225 | if len(opens) == 1 { | |
| @@ -1203,6 +1228,10 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||
| 1203 | 1228 | return p2, nil | |
| 1204 | 1229 | }, | |
| 1205 | 1230 | ).Twice() | |
| 1231 | + | floating := newQuietPTY() | |
| 1232 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 1233 | + | return got != normalSize && got.Valid() | |
| 1234 | + | })).Return(floating, nil).Once() | |
| 1206 | 1235 | ||
| 1207 | 1236 | d := newTestDaemon(t, f, stubClock{}) | |
| 1208 | 1237 | d.baseEnv = []string{"KEEP=1", "COLORTERM=old"} | |
| @@ -1234,7 +1263,8 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||
| 1234 | 1263 | ||
| 1235 | 1264 | var opens [][]string | |
| 1236 | 1265 | f := portsmocks.NewMockPTYFactory(t) | |
| 1237 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 1266 | + | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | |
| 1267 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 1238 | 1268 | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1239 | 1269 | opens = append(opens, append([]string(nil), env...)) | |
| 1240 | 1270 | if len(opens) == 1 { | |
| @@ -1243,6 +1273,10 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||
| 1243 | 1273 | return p2, nil | |
| 1244 | 1274 | }, | |
| 1245 | 1275 | ).Twice() | |
| 1276 | + | floating := newQuietPTY() | |
| 1277 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 1278 | + | return got != normalSize && got.Valid() | |
| 1279 | + | })).Return(floating, nil).Once() | |
| 1246 | 1280 | ||
| 1247 | 1281 | d := newTestDaemon(t, f, stubClock{}) | |
| 1248 | 1282 | tr1 := portsmocks.NewMockTransport(t) | |
| @@ -1276,7 +1310,8 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 1276 | 1310 | defer release2() | |
| 1277 | 1311 | var opens [][]string | |
| 1278 | 1312 | f := portsmocks.NewMockPTYFactory(t) | |
| 1279 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 1313 | + | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | |
| 1314 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 1280 | 1315 | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1281 | 1316 | opens = append(opens, append([]string(nil), env...)) | |
| 1282 | 1317 | if len(opens) == 1 { | |
| @@ -1285,6 +1320,10 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 1285 | 1320 | return p2, nil | |
| 1286 | 1321 | }, | |
| 1287 | 1322 | ).Twice() | |
| 1323 | + | floating := newQuietPTY() | |
| 1324 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 1325 | + | return got != normalSize && got.Valid() | |
| 1326 | + | })).Return(floating, nil).Once() | |
| 1288 | 1327 | d := newTestDaemon(t, f, stubClock{}) | |
| 1289 | 1328 | tr := portsmocks.NewMockTransport(t) | |
| 1290 | 1329 | tr.EXPECT().Send(mock.Anything).Return(nil).Maybe() | |
| @@ -1295,10 +1334,14 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 1295 | 1334 | got := ac.sess.Get() | |
| 1296 | 1335 | require.NotNil(t, got) | |
| 1297 | 1336 | got.mu.Lock() | |
| 1298 | - | defer got.mu.Unlock() | |
| 1299 | 1337 | require.True(t, got.terminal.TrueColor) | |
| 1338 | + | got.mu.Unlock() | |
| 1300 | 1339 | require.Len(t, opens, 2) | |
| 1301 | 1340 | require.Contains(t, opens[1], "TERM=xterm-direct") | |
| 1302 | 1341 | require.Contains(t, opens[1], "COLORTERM=truecolor") | |
| 1303 | 1342 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | |
| 1343 | + | _ = d.killSession(got, ports.ReasonSessionKilled, false) | |
| 1344 | + | release1() | |
| 1345 | + | release2() | |
| 1346 | + | d.sessWg.Wait() | |
| 1304 | 1347 | } | |
| @@ -33,7 +33,8 @@ func TestRoutePropagatesHelloCwdAndTabsInheritIt(t *testing.T) { | |||||
| 33 | defer releaseSecond() | 33 | defer releaseSecond() | ||
| 34 | f := portsmocks.NewMockPTYFactory(t) | 34 | f := portsmocks.NewMockPTYFactory(t) | ||
| 35 | var dirs []string | 35 | var dirs []string | ||
| 36 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 36 | + | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} |
| 37 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |||
| 37 | func(_ string, _ []string, _ []string, dir string, _ domain.Size) (ports.PTY, error) { | 38 | func(_ string, _ []string, _ []string, dir string, _ domain.Size) (ports.PTY, error) { | ||
| 38 | dirs = append(dirs, dir) | 39 | dirs = append(dirs, dir) | ||
| 39 | if len(dirs) == 1 { | 40 | if len(dirs) == 1 { | ||
| @@ -42,6 +43,10 @@ func TestRoutePropagatesHelloCwdAndTabsInheritIt(t *testing.T) { | |||||
| 42 | return second, nil | 43 | return second, nil | ||
| 43 | }, | 44 | }, | ||
| 44 | ).Twice() | 45 | ).Twice() | ||
| 46 | + | floating := newQuietPTY() | |||
| 47 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |||
| 48 | + | return got != normalSize && got.Valid() | |||
| 49 | + | })).Return(floating, nil).Once() | |||
| 45 | 50 | ||||
| 46 | d := newTestDaemon(t, f, stubClock{}) | 51 | d := newTestDaemon(t, f, stubClock{}) | ||
| 47 | tr := portsmocks.NewMockTransport(t) | 52 | tr := portsmocks.NewMockTransport(t) | ||
| @@ -62,6 +67,15 @@ func TestRoutePropagatesHelloCwdAndTabsInheritIt(t *testing.T) { | |||||
| 62 | 67 | ||||
| 63 | require.NoError(t, d.createTab(sess, sz)) | 68 | require.NoError(t, d.createTab(sess, sz)) | ||
| 64 | require.Equal(t, []string{"/tmp/work", "/tmp/work"}, dirs) | 69 | require.Equal(t, []string{"/tmp/work", "/tmp/work"}, dirs) | ||
| 70 | + | _ = d.killSession(sess, ports.ReasonSessionKilled, false) | |||
| 71 | + | releaseFirst() | |||
| 72 | + | releaseSecond() | |||
| 73 | + | d.sessWg.Wait() | |||
| 74 | + | select { | |||
| 75 | + | case <-floating.done: | |||
| 76 | + | default: | |||
| 77 | + | t.Fatal("floating prewarm PTY was not closed") | |||
| 78 | + | } | |||
| 65 | } | 79 | } | ||
| 66 | 80 | ||||
| 67 | func TestHandshakeEphemeralHappy(t *testing.T) { | 81 | func TestHandshakeEphemeralHappy(t *testing.T) { | ||
| @@ -197,8 +211,13 @@ func TestCreateTabClosesPTYIfSessionKilledDuringOpen(t *testing.T) { | |||||
| 197 | p2.EXPECT().Pid().Return(4242).Maybe() | 211 | p2.EXPECT().Pid().Return(4242).Maybe() | ||
| 198 | 212 | ||||
| 199 | f := portsmocks.NewMockPTYFactory(t) | 213 | f := portsmocks.NewMockPTYFactory(t) | ||
| 200 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(p1, nil).Once() | 214 | + | normalSize := domain.Size{Cols: 80, Rows: 22} |
| 201 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 215 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).Return(p1, nil).Once() |
| 216 | + | floating := newQuietPTY() | |||
| 217 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |||
| 218 | + | return got != normalSize && got.Valid() | |||
| 219 | + | })).Return(floating, nil).Once() | |||
| 220 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |||
| 202 | func(string, []string, []string, string, domain.Size) (ports.PTY, error) { | 221 | func(string, []string, []string, string, domain.Size) (ports.PTY, error) { | ||
| 203 | close(opened) | 222 | close(opened) | ||
| 204 | <-releaseOpen | 223 | <-releaseOpen | ||
| @@ -231,6 +250,11 @@ func TestCreateTabClosesPTYIfSessionKilledDuringOpen(t *testing.T) { | |||||
| 231 | releaseConn() | 250 | releaseConn() | ||
| 232 | hg.Wait() | 251 | hg.Wait() | ||
| 233 | d.sessWg.Wait() | 252 | d.sessWg.Wait() | ||
| 253 | + | select { | |||
| 254 | + | case <-floating.done: | |||
| 255 | + | default: | |||
| 256 | + | t.Fatal("floating prewarm PTY was not closed") | |||
| 257 | + | } | |||
| 234 | } | 258 | } | ||
| 235 | 259 | ||||
| 236 | // --- ephemeral numbering ---------------------------------------------------- | 260 | // --- ephemeral numbering ---------------------------------------------------- | ||
| @@ -1194,7 +1218,8 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||||
| 1194 | 1218 | ||||
| 1195 | var opens [][]string | 1219 | var opens [][]string | ||
| 1196 | f := portsmocks.NewMockPTYFactory(t) | 1220 | f := portsmocks.NewMockPTYFactory(t) | ||
| 1197 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 1221 | + | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} |
| 1222 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |||
| 1198 | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 1223 | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | ||
| 1199 | opens = append(opens, append([]string(nil), env...)) | 1224 | opens = append(opens, append([]string(nil), env...)) | ||
| 1200 | if len(opens) == 1 { | 1225 | if len(opens) == 1 { | ||
| @@ -1203,6 +1228,10 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||||
| 1203 | return p2, nil | 1228 | return p2, nil | ||
| 1204 | }, | 1229 | }, | ||
| 1205 | ).Twice() | 1230 | ).Twice() | ||
| 1231 | + | floating := newQuietPTY() | |||
| 1232 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |||
| 1233 | + | return got != normalSize && got.Valid() | |||
| 1234 | + | })).Return(floating, nil).Once() | |||
| 1206 | 1235 | ||||
| 1207 | d := newTestDaemon(t, f, stubClock{}) | 1236 | d := newTestDaemon(t, f, stubClock{}) | ||
| 1208 | d.baseEnv = []string{"KEEP=1", "COLORTERM=old"} | 1237 | d.baseEnv = []string{"KEEP=1", "COLORTERM=old"} | ||
| @@ -1234,7 +1263,8 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||||
| 1234 | 1263 | ||||
| 1235 | var opens [][]string | 1264 | var opens [][]string | ||
| 1236 | f := portsmocks.NewMockPTYFactory(t) | 1265 | f := portsmocks.NewMockPTYFactory(t) | ||
| 1237 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 1266 | + | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} |
| 1267 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |||
| 1238 | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 1268 | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | ||
| 1239 | opens = append(opens, append([]string(nil), env...)) | 1269 | opens = append(opens, append([]string(nil), env...)) | ||
| 1240 | if len(opens) == 1 { | 1270 | if len(opens) == 1 { | ||
| @@ -1243,6 +1273,10 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||||
| 1243 | return p2, nil | 1273 | return p2, nil | ||
| 1244 | }, | 1274 | }, | ||
| 1245 | ).Twice() | 1275 | ).Twice() | ||
| 1276 | + | floating := newQuietPTY() | |||
| 1277 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |||
| 1278 | + | return got != normalSize && got.Valid() | |||
| 1279 | + | })).Return(floating, nil).Once() | |||
| 1246 | 1280 | ||||
| 1247 | d := newTestDaemon(t, f, stubClock{}) | 1281 | d := newTestDaemon(t, f, stubClock{}) | ||
| 1248 | tr1 := portsmocks.NewMockTransport(t) | 1282 | tr1 := portsmocks.NewMockTransport(t) | ||
| @@ -1276,7 +1310,8 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 1276 | defer release2() | 1310 | defer release2() | ||
| 1277 | var opens [][]string | 1311 | var opens [][]string | ||
| 1278 | f := portsmocks.NewMockPTYFactory(t) | 1312 | f := portsmocks.NewMockPTYFactory(t) | ||
| 1279 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 1313 | + | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} |
| 1314 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |||
| 1280 | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 1315 | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | ||
| 1281 | opens = append(opens, append([]string(nil), env...)) | 1316 | opens = append(opens, append([]string(nil), env...)) | ||
| 1282 | if len(opens) == 1 { | 1317 | if len(opens) == 1 { | ||
| @@ -1285,6 +1320,10 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 1285 | return p2, nil | 1320 | return p2, nil | ||
| 1286 | }, | 1321 | }, | ||
| 1287 | ).Twice() | 1322 | ).Twice() | ||
| 1323 | + | floating := newQuietPTY() | |||
| 1324 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |||
| 1325 | + | return got != normalSize && got.Valid() | |||
| 1326 | + | })).Return(floating, nil).Once() | |||
| 1288 | d := newTestDaemon(t, f, stubClock{}) | 1327 | d := newTestDaemon(t, f, stubClock{}) | ||
| 1289 | tr := portsmocks.NewMockTransport(t) | 1328 | tr := portsmocks.NewMockTransport(t) | ||
| 1290 | tr.EXPECT().Send(mock.Anything).Return(nil).Maybe() | 1329 | tr.EXPECT().Send(mock.Anything).Return(nil).Maybe() | ||
| @@ -1295,10 +1334,14 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 1295 | got := ac.sess.Get() | 1334 | got := ac.sess.Get() | ||
| 1296 | require.NotNil(t, got) | 1335 | require.NotNil(t, got) | ||
| 1297 | got.mu.Lock() | 1336 | got.mu.Lock() | ||
| 1298 | - | defer got.mu.Unlock() | |||
| 1299 | require.True(t, got.terminal.TrueColor) | 1337 | require.True(t, got.terminal.TrueColor) | ||
| 1338 | + | got.mu.Unlock() | |||
| 1300 | require.Len(t, opens, 2) | 1339 | require.Len(t, opens, 2) | ||
| 1301 | require.Contains(t, opens[1], "TERM=xterm-direct") | 1340 | require.Contains(t, opens[1], "TERM=xterm-direct") | ||
| 1302 | require.Contains(t, opens[1], "COLORTERM=truecolor") | 1341 | require.Contains(t, opens[1], "COLORTERM=truecolor") | ||
| 1303 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | 1342 | require.Contains(t, opens[1], "TERM_PROGRAM=vev") | ||
| 1343 | + | _ = d.killSession(got, ports.ReasonSessionKilled, false) | |||
| 1344 | + | release1() | |||
| 1345 | + | release2() | |||
| 1346 | + | d.sessWg.Wait() | |||
| 1304 | } | 1347 | } | ||