feat(daemon): manage floating pane lifecycle
- commit
- 8ccec9f939fa6468ed87095090da07ee82a03590
- author
- brice · · 2026-07-09 23:52 UTC
- committer
- brice · · 2026-07-10 05:40 UTC
- parent
- e38876b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +218 -2
M internal/usecase/daemon/floating.go +199 -1
| @@ -1,6 +1,14 @@ | |||
| 1 | 1 | package daemon | |
| 2 | 2 | ||
| 3 | - | import "github.com/bnema/vev/internal/domain" | |
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | "path/filepath" | |
| 7 | + | "strings" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/vev/internal/domain" | |
| 10 | + | "github.com/bnema/vev/internal/usecase/layout" | |
| 11 | + | ) | |
| 4 | 12 | ||
| 5 | 13 | // floatingState describes the lifecycle of a tab's independent floating pane. | |
| 6 | 14 | // A floating slot is always guarded by its owning tab.mu. | |
| @@ -132,3 +140,193 @@ func floatingInnerAxis(available, percent int) int { | |||
| 132 | 140 | } | |
| 133 | 141 | return bounds | |
| 134 | 142 | } | |
| 143 | + | ||
| 144 | + | // ensureFloatingWarm starts the background prewarm exactly once for a tab. | |
| 145 | + | func (d *Daemon) ensureFloatingWarm(sess *session, tb *tab) { | |
| 146 | + | if d == nil || sess == nil || tb == nil || d.ptys == nil { | |
| 147 | + | return | |
| 148 | + | } | |
| 149 | + | d.startFloating(sess, tb, false) | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | // toggleFloating changes only this tab's slot. Opening an uninitialized slot | |
| 153 | + | // launches asynchronously; installed slots are retained when hidden. | |
| 154 | + | func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error { | |
| 155 | + | if d == nil || sess == nil { | |
| 156 | + | return fmt.Errorf("floating pane: session required") | |
| 157 | + | } | |
| 158 | + | tb := sess.activeTab() | |
| 159 | + | if tb == nil { | |
| 160 | + | return layout.ErrNotFound | |
| 161 | + | } | |
| 162 | + | cfg := d.currentFloatingConfig() | |
| 163 | + | tb.mu.Lock() | |
| 164 | + | start, generation := tb.toggleFloatingLocked(cfg) | |
| 165 | + | visible := tb.floating.desiredVisible | |
| 166 | + | tb.mu.Unlock() | |
| 167 | + | if start { | |
| 168 | + | d.launchFloating(sess, tb, cfg, generation, visible) | |
| 169 | + | return nil | |
| 170 | + | } | |
| 171 | + | if ac != nil { | |
| 172 | + | d.paint(sess, ac, true) | |
| 173 | + | } | |
| 174 | + | return nil | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | // startFloating starts a prewarm if its slot remains uninitialized. | |
| 178 | + | func (d *Daemon) startFloating(sess *session, tb *tab, visible bool) { | |
| 179 | + | cfg := d.currentFloatingConfig() | |
| 180 | + | tb.mu.Lock() | |
| 181 | + | generation := tb.beginFloatingWarmLocked(cfg, visible) | |
| 182 | + | tb.mu.Unlock() | |
| 183 | + | if generation != 0 { | |
| 184 | + | d.launchFloating(sess, tb, cfg, generation, visible) | |
| 185 | + | } | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | // launchFloating snapshots all launch inputs before starting the potentially | |
| 189 | + | // blocking factory call. In particular, no PTY operation occurs while tb.mu is | |
| 190 | + | // held, and config reloads cannot affect this launch. | |
| 191 | + | func (d *Daemon) launchFloating(sess *session, tb *tab, cfg domain.FloatingConfig, generation uint64, userOpen bool) { | |
| 192 | + | sess.mu.Lock() | |
| 193 | + | name, cwd, term := sess.name, sess.cwd, sess.terminal | |
| 194 | + | sess.mu.Unlock() | |
| 195 | + | tb.mu.Lock() | |
| 196 | + | sz := floatingInnerSize(tb.size, cfg) | |
| 197 | + | focused := tb.focusedPane() | |
| 198 | + | tabStableID := tb.stableID | |
| 199 | + | tabCtx := tb.ctx | |
| 200 | + | tb.mu.Unlock() | |
| 201 | + | if !sz.Valid() { | |
| 202 | + | sz = domain.Size{Cols: 1, Rows: 1} | |
| 203 | + | } | |
| 204 | + | if focused != nil && d.procCwd != nil && focused.pty != nil { | |
| 205 | + | if live, err := d.procCwd(focused.pty.Pid()); err == nil && live != "" { | |
| 206 | + | cwd = live | |
| 207 | + | } | |
| 208 | + | } | |
| 209 | + | if cwd == "" && d.dirOrHome != nil { | |
| 210 | + | cwd = d.dirOrHome("") | |
| 211 | + | } | |
| 212 | + | paneStableID, err := newStableID("p") | |
| 213 | + | if err != nil { | |
| 214 | + | d.failFloatingLaunch(sess, tb, generation, userOpen, err) | |
| 215 | + | return | |
| 216 | + | } | |
| 217 | + | env := d.childEnv(name, tabStableID, paneStableID, term) | |
| 218 | + | command, args := d.shell, append([]string(nil), d.shellArgs...) | |
| 219 | + | if cfg.Command != "" { | |
| 220 | + | args = []string{"-lc", cfg.Command} | |
| 221 | + | } | |
| 222 | + | fallback := floatingCommandFallback(cfg.Command, d.shell) | |
| 223 | + | go func() { | |
| 224 | + | pty, openErr := d.ptys.Open(command, args, env, cwd, sz) | |
| 225 | + | if openErr != nil { | |
| 226 | + | d.failFloatingLaunch(sess, tb, generation, userOpen, openErr) | |
| 227 | + | return | |
| 228 | + | } | |
| 229 | + | p := newPaneWithStableID(layout.PaneID("floating"), paneStableID, pty, sz) | |
| 230 | + | p.title.displayFallback = fallback | |
| 231 | + | if tabCtx == nil { | |
| 232 | + | tabCtx = sess.ctx | |
| 233 | + | } | |
| 234 | + | if tabCtx == nil { | |
| 235 | + | tabCtx = context.Background() | |
| 236 | + | } | |
| 237 | + | p.ctx, p.cancel = context.WithCancel(tabCtx) | |
| 238 | + | d.installFloating(sess, tb, p, generation) | |
| 239 | + | }() | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | func (d *Daemon) failFloatingLaunch(sess *session, tb *tab, generation uint64, userOpen bool, err error) { | |
| 243 | + | tb.mu.Lock() | |
| 244 | + | current := tb.failFloatingLocked(generation) | |
| 245 | + | tb.mu.Unlock() | |
| 246 | + | if current { | |
| 247 | + | kind := "background-prewarm" | |
| 248 | + | if userOpen { | |
| 249 | + | kind = "user-open" | |
| 250 | + | } | |
| 251 | + | d.log.Warn("floating pty spawn failed", "err", err, "session", sess.name, "kind", kind) | |
| 252 | + | } | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | // installFloating installs only the current generation. A stale successful | |
| 256 | + | // launch is cancelled and closed outside the tab lock. | |
| 257 | + | func (d *Daemon) installFloating(sess *session, tb *tab, p *pane, generation uint64) { | |
| 258 | + | tb.mu.Lock() | |
| 259 | + | installed := tb.installFloatingLocked(p, generation) | |
| 260 | + | visible := installed && tb.floating.state == floatingVisible | |
| 261 | + | tb.mu.Unlock() | |
| 262 | + | if !installed { | |
| 263 | + | if p.cancel != nil { | |
| 264 | + | p.cancel() | |
| 265 | + | } | |
| 266 | + | _ = p.pty.Close() | |
| 267 | + | return | |
| 268 | + | } | |
| 269 | + | p.onExit = func() { d.reapFloating(sess, tb, p, generation) } | |
| 270 | + | d.startPaneGoroutines(sess, tb, p) | |
| 271 | + | if visible { | |
| 272 | + | sess.mu.Lock() | |
| 273 | + | ac := sess.client | |
| 274 | + | sess.mu.Unlock() | |
| 275 | + | if ac != nil { | |
| 276 | + | d.paint(sess, ac, true) | |
| 277 | + | } | |
| 278 | + | } | |
| 279 | + | } | |
| 280 | + | ||
| 281 | + | // reapFloating ignores old readers and restores the background only when the | |
| 282 | + | // exiting popup was visible. | |
| 283 | + | func (d *Daemon) reapFloating(sess *session, tb *tab, p *pane, generation uint64) { | |
| 284 | + | tb.mu.Lock() | |
| 285 | + | visible := tb.floating.state == floatingVisible && tb.floating.pane == p && tb.floating.generation == generation | |
| 286 | + | cleared := tb.clearFloatingLocked(p, generation) | |
| 287 | + | tb.mu.Unlock() | |
| 288 | + | if !cleared { | |
| 289 | + | return | |
| 290 | + | } | |
| 291 | + | if p.cancel != nil { | |
| 292 | + | p.cancel() | |
| 293 | + | } | |
| 294 | + | if visible { | |
| 295 | + | sess.mu.Lock() | |
| 296 | + | ac := sess.client | |
| 297 | + | sess.mu.Unlock() | |
| 298 | + | if ac != nil { | |
| 299 | + | d.paint(sess, ac, true) | |
| 300 | + | } | |
| 301 | + | } | |
| 302 | + | } | |
| 303 | + | ||
| 304 | + | // teardownFloating invalidates first, then releases resources outside tb.mu. | |
| 305 | + | func (d *Daemon) teardownFloating(tb *tab) { | |
| 306 | + | if tb == nil { | |
| 307 | + | return | |
| 308 | + | } | |
| 309 | + | tb.mu.Lock() | |
| 310 | + | p := tb.floating.pane | |
| 311 | + | tb.floating.generation++ | |
| 312 | + | tb.floating.state = floatingUninitialized | |
| 313 | + | tb.floating.pane = nil | |
| 314 | + | tb.floating.desiredVisible = false | |
| 315 | + | tb.floating.launch = domain.FloatingConfig{} | |
| 316 | + | tb.mu.Unlock() | |
| 317 | + | if p != nil { | |
| 318 | + | if p.cancel != nil { | |
| 319 | + | p.cancel() | |
| 320 | + | } | |
| 321 | + | if p.pty != nil { | |
| 322 | + | _ = p.pty.Close() | |
| 323 | + | } | |
| 324 | + | } | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | func floatingCommandFallback(command, shell string) string { | |
| 328 | + | if fields := strings.Fields(command); len(fields) > 0 { | |
| 329 | + | return filepath.Base(fields[0]) | |
| 330 | + | } | |
| 331 | + | return filepath.Base(shell) | |
| 332 | + | } | |
| @@ -1,6 +1,14 @@ | |||||
| 1 | package daemon | 1 | package daemon | ||
| 2 | 2 | ||||
| 3 | - | import "github.com/bnema/vev/internal/domain" | 3 | + | import ( |
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | "path/filepath" | |||
| 7 | + | "strings" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/vev/internal/domain" | |||
| 10 | + | "github.com/bnema/vev/internal/usecase/layout" | |||
| 11 | + | ) | |||
| 4 | 12 | ||||
| 5 | // floatingState describes the lifecycle of a tab's independent floating pane. | 13 | // floatingState describes the lifecycle of a tab's independent floating pane. | ||
| 6 | // A floating slot is always guarded by its owning tab.mu. | 14 | // A floating slot is always guarded by its owning tab.mu. | ||
| @@ -132,3 +140,193 @@ func floatingInnerAxis(available, percent int) int { | |||||
| 132 | } | 140 | } | ||
| 133 | return bounds | 141 | return bounds | ||
| 134 | } | 142 | } | ||
| 143 | + | ||||
| 144 | + | // ensureFloatingWarm starts the background prewarm exactly once for a tab. | |||
| 145 | + | func (d *Daemon) ensureFloatingWarm(sess *session, tb *tab) { | |||
| 146 | + | if d == nil || sess == nil || tb == nil || d.ptys == nil { | |||
| 147 | + | return | |||
| 148 | + | } | |||
| 149 | + | d.startFloating(sess, tb, false) | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | // toggleFloating changes only this tab's slot. Opening an uninitialized slot | |||
| 153 | + | // launches asynchronously; installed slots are retained when hidden. | |||
| 154 | + | func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error { | |||
| 155 | + | if d == nil || sess == nil { | |||
| 156 | + | return fmt.Errorf("floating pane: session required") | |||
| 157 | + | } | |||
| 158 | + | tb := sess.activeTab() | |||
| 159 | + | if tb == nil { | |||
| 160 | + | return layout.ErrNotFound | |||
| 161 | + | } | |||
| 162 | + | cfg := d.currentFloatingConfig() | |||
| 163 | + | tb.mu.Lock() | |||
| 164 | + | start, generation := tb.toggleFloatingLocked(cfg) | |||
| 165 | + | visible := tb.floating.desiredVisible | |||
| 166 | + | tb.mu.Unlock() | |||
| 167 | + | if start { | |||
| 168 | + | d.launchFloating(sess, tb, cfg, generation, visible) | |||
| 169 | + | return nil | |||
| 170 | + | } | |||
| 171 | + | if ac != nil { | |||
| 172 | + | d.paint(sess, ac, true) | |||
| 173 | + | } | |||
| 174 | + | return nil | |||
| 175 | + | } | |||
| 176 | + | ||||
| 177 | + | // startFloating starts a prewarm if its slot remains uninitialized. | |||
| 178 | + | func (d *Daemon) startFloating(sess *session, tb *tab, visible bool) { | |||
| 179 | + | cfg := d.currentFloatingConfig() | |||
| 180 | + | tb.mu.Lock() | |||
| 181 | + | generation := tb.beginFloatingWarmLocked(cfg, visible) | |||
| 182 | + | tb.mu.Unlock() | |||
| 183 | + | if generation != 0 { | |||
| 184 | + | d.launchFloating(sess, tb, cfg, generation, visible) | |||
| 185 | + | } | |||
| 186 | + | } | |||
| 187 | + | ||||
| 188 | + | // launchFloating snapshots all launch inputs before starting the potentially | |||
| 189 | + | // blocking factory call. In particular, no PTY operation occurs while tb.mu is | |||
| 190 | + | // held, and config reloads cannot affect this launch. | |||
| 191 | + | func (d *Daemon) launchFloating(sess *session, tb *tab, cfg domain.FloatingConfig, generation uint64, userOpen bool) { | |||
| 192 | + | sess.mu.Lock() | |||
| 193 | + | name, cwd, term := sess.name, sess.cwd, sess.terminal | |||
| 194 | + | sess.mu.Unlock() | |||
| 195 | + | tb.mu.Lock() | |||
| 196 | + | sz := floatingInnerSize(tb.size, cfg) | |||
| 197 | + | focused := tb.focusedPane() | |||
| 198 | + | tabStableID := tb.stableID | |||
| 199 | + | tabCtx := tb.ctx | |||
| 200 | + | tb.mu.Unlock() | |||
| 201 | + | if !sz.Valid() { | |||
| 202 | + | sz = domain.Size{Cols: 1, Rows: 1} | |||
| 203 | + | } | |||
| 204 | + | if focused != nil && d.procCwd != nil && focused.pty != nil { | |||
| 205 | + | if live, err := d.procCwd(focused.pty.Pid()); err == nil && live != "" { | |||
| 206 | + | cwd = live | |||
| 207 | + | } | |||
| 208 | + | } | |||
| 209 | + | if cwd == "" && d.dirOrHome != nil { | |||
| 210 | + | cwd = d.dirOrHome("") | |||
| 211 | + | } | |||
| 212 | + | paneStableID, err := newStableID("p") | |||
| 213 | + | if err != nil { | |||
| 214 | + | d.failFloatingLaunch(sess, tb, generation, userOpen, err) | |||
| 215 | + | return | |||
| 216 | + | } | |||
| 217 | + | env := d.childEnv(name, tabStableID, paneStableID, term) | |||
| 218 | + | command, args := d.shell, append([]string(nil), d.shellArgs...) | |||
| 219 | + | if cfg.Command != "" { | |||
| 220 | + | args = []string{"-lc", cfg.Command} | |||
| 221 | + | } | |||
| 222 | + | fallback := floatingCommandFallback(cfg.Command, d.shell) | |||
| 223 | + | go func() { | |||
| 224 | + | pty, openErr := d.ptys.Open(command, args, env, cwd, sz) | |||
| 225 | + | if openErr != nil { | |||
| 226 | + | d.failFloatingLaunch(sess, tb, generation, userOpen, openErr) | |||
| 227 | + | return | |||
| 228 | + | } | |||
| 229 | + | p := newPaneWithStableID(layout.PaneID("floating"), paneStableID, pty, sz) | |||
| 230 | + | p.title.displayFallback = fallback | |||
| 231 | + | if tabCtx == nil { | |||
| 232 | + | tabCtx = sess.ctx | |||
| 233 | + | } | |||
| 234 | + | if tabCtx == nil { | |||
| 235 | + | tabCtx = context.Background() | |||
| 236 | + | } | |||
| 237 | + | p.ctx, p.cancel = context.WithCancel(tabCtx) | |||
| 238 | + | d.installFloating(sess, tb, p, generation) | |||
| 239 | + | }() | |||
| 240 | + | } | |||
| 241 | + | ||||
| 242 | + | func (d *Daemon) failFloatingLaunch(sess *session, tb *tab, generation uint64, userOpen bool, err error) { | |||
| 243 | + | tb.mu.Lock() | |||
| 244 | + | current := tb.failFloatingLocked(generation) | |||
| 245 | + | tb.mu.Unlock() | |||
| 246 | + | if current { | |||
| 247 | + | kind := "background-prewarm" | |||
| 248 | + | if userOpen { | |||
| 249 | + | kind = "user-open" | |||
| 250 | + | } | |||
| 251 | + | d.log.Warn("floating pty spawn failed", "err", err, "session", sess.name, "kind", kind) | |||
| 252 | + | } | |||
| 253 | + | } | |||
| 254 | + | ||||
| 255 | + | // installFloating installs only the current generation. A stale successful | |||
| 256 | + | // launch is cancelled and closed outside the tab lock. | |||
| 257 | + | func (d *Daemon) installFloating(sess *session, tb *tab, p *pane, generation uint64) { | |||
| 258 | + | tb.mu.Lock() | |||
| 259 | + | installed := tb.installFloatingLocked(p, generation) | |||
| 260 | + | visible := installed && tb.floating.state == floatingVisible | |||
| 261 | + | tb.mu.Unlock() | |||
| 262 | + | if !installed { | |||
| 263 | + | if p.cancel != nil { | |||
| 264 | + | p.cancel() | |||
| 265 | + | } | |||
| 266 | + | _ = p.pty.Close() | |||
| 267 | + | return | |||
| 268 | + | } | |||
| 269 | + | p.onExit = func() { d.reapFloating(sess, tb, p, generation) } | |||
| 270 | + | d.startPaneGoroutines(sess, tb, p) | |||
| 271 | + | if visible { | |||
| 272 | + | sess.mu.Lock() | |||
| 273 | + | ac := sess.client | |||
| 274 | + | sess.mu.Unlock() | |||
| 275 | + | if ac != nil { | |||
| 276 | + | d.paint(sess, ac, true) | |||
| 277 | + | } | |||
| 278 | + | } | |||
| 279 | + | } | |||
| 280 | + | ||||
| 281 | + | // reapFloating ignores old readers and restores the background only when the | |||
| 282 | + | // exiting popup was visible. | |||
| 283 | + | func (d *Daemon) reapFloating(sess *session, tb *tab, p *pane, generation uint64) { | |||
| 284 | + | tb.mu.Lock() | |||
| 285 | + | visible := tb.floating.state == floatingVisible && tb.floating.pane == p && tb.floating.generation == generation | |||
| 286 | + | cleared := tb.clearFloatingLocked(p, generation) | |||
| 287 | + | tb.mu.Unlock() | |||
| 288 | + | if !cleared { | |||
| 289 | + | return | |||
| 290 | + | } | |||
| 291 | + | if p.cancel != nil { | |||
| 292 | + | p.cancel() | |||
| 293 | + | } | |||
| 294 | + | if visible { | |||
| 295 | + | sess.mu.Lock() | |||
| 296 | + | ac := sess.client | |||
| 297 | + | sess.mu.Unlock() | |||
| 298 | + | if ac != nil { | |||
| 299 | + | d.paint(sess, ac, true) | |||
| 300 | + | } | |||
| 301 | + | } | |||
| 302 | + | } | |||
| 303 | + | ||||
| 304 | + | // teardownFloating invalidates first, then releases resources outside tb.mu. | |||
| 305 | + | func (d *Daemon) teardownFloating(tb *tab) { | |||
| 306 | + | if tb == nil { | |||
| 307 | + | return | |||
| 308 | + | } | |||
| 309 | + | tb.mu.Lock() | |||
| 310 | + | p := tb.floating.pane | |||
| 311 | + | tb.floating.generation++ | |||
| 312 | + | tb.floating.state = floatingUninitialized | |||
| 313 | + | tb.floating.pane = nil | |||
| 314 | + | tb.floating.desiredVisible = false | |||
| 315 | + | tb.floating.launch = domain.FloatingConfig{} | |||
| 316 | + | tb.mu.Unlock() | |||
| 317 | + | if p != nil { | |||
| 318 | + | if p.cancel != nil { | |||
| 319 | + | p.cancel() | |||
| 320 | + | } | |||
| 321 | + | if p.pty != nil { | |||
| 322 | + | _ = p.pty.Close() | |||
| 323 | + | } | |||
| 324 | + | } | |||
| 325 | + | } | |||
| 326 | + | ||||
| 327 | + | func floatingCommandFallback(command, shell string) string { | |||
| 328 | + | if fields := strings.Fields(command); len(fields) > 0 { | |||
| 329 | + | return filepath.Base(fields[0]) | |||
| 330 | + | } | |||
| 331 | + | return filepath.Base(shell) | |||
| 332 | + | } | |||
M internal/usecase/daemon/pane.go +3 -0
| @@ -29,6 +29,9 @@ type pane struct { | |||
| 29 | 29 | title paneTitleState | |
| 30 | 30 | ctx context.Context | |
| 31 | 31 | cancel context.CancelFunc | |
| 32 | + | // onExit is set before the reader starts and never changed. Floating panes | |
| 33 | + | // use it to reap their independent slot without touching the layout tree. | |
| 34 | + | onExit func() | |
| 32 | 35 | } | |
| 33 | 36 | ||
| 34 | 37 | func newPane(id layout.PaneID, pty ports.PTY, sz domain.Size) *pane { | |
| @@ -29,6 +29,9 @@ type pane struct { | |||||
| 29 | title paneTitleState | 29 | title paneTitleState | ||
| 30 | ctx context.Context | 30 | ctx context.Context | ||
| 31 | cancel context.CancelFunc | 31 | cancel context.CancelFunc | ||
| 32 | + | // onExit is set before the reader starts and never changed. Floating panes | |||
| 33 | + | // use it to reap their independent slot without touching the layout tree. | |||
| 34 | + | onExit func() | |||
| 32 | } | 35 | } | ||
| 33 | 36 | ||||
| 34 | func newPane(id layout.PaneID, pty ports.PTY, sz domain.Size) *pane { | 37 | func newPane(id layout.PaneID, pty ports.PTY, sz domain.Size) *pane { | ||
M internal/usecase/daemon/render.go +5 -1
| @@ -109,7 +109,11 @@ func (d *Daemon) ptyReader(sess *session, tb *tab, p *pane) { | |||
| 109 | 109 | signal(p.dirty) | |
| 110 | 110 | } | |
| 111 | 111 | if err != nil { | |
| 112 | - | d.reapPane(sess, tb, p) | |
| 112 | + | if p.onExit != nil { | |
| 113 | + | p.onExit() | |
| 114 | + | } else { | |
| 115 | + | d.reapPane(sess, tb, p) | |
| 116 | + | } | |
| 113 | 117 | return | |
| 114 | 118 | } | |
| 115 | 119 | } | |
| @@ -109,7 +109,11 @@ func (d *Daemon) ptyReader(sess *session, tb *tab, p *pane) { | |||||
| 109 | signal(p.dirty) | 109 | signal(p.dirty) | ||
| 110 | } | 110 | } | ||
| 111 | if err != nil { | 111 | if err != nil { | ||
| 112 | - | d.reapPane(sess, tb, p) | 112 | + | if p.onExit != nil { |
| 113 | + | p.onExit() | |||
| 114 | + | } else { | |||
| 115 | + | d.reapPane(sess, tb, p) | |||
| 116 | + | } | |||
| 113 | return | 117 | return | ||
| 114 | } | 118 | } | ||
| 115 | } | 119 | } | ||
M internal/usecase/daemon/session.go +11 -0
| @@ -373,7 +373,18 @@ func (tb *tab) panesSnapshot() []*pane { | |||
| 373 | 373 | func (tb *tab) closeAllPanes() { | |
| 374 | 374 | tb.mu.Lock() | |
| 375 | 375 | panes := tb.panesSnapshot() | |
| 376 | + | floating := tb.floating.pane | |
| 377 | + | // Invalidate before cancellation so a concurrently unblocked floating | |
| 378 | + | // reader cannot reap a subsequently reused slot. | |
| 379 | + | tb.floating.generation++ | |
| 380 | + | tb.floating.state = floatingUninitialized | |
| 381 | + | tb.floating.pane = nil | |
| 382 | + | tb.floating.desiredVisible = false | |
| 383 | + | tb.floating.launch = domain.FloatingConfig{} | |
| 376 | 384 | tb.mu.Unlock() | |
| 385 | + | if floating != nil { | |
| 386 | + | panes = append(panes, floating) | |
| 387 | + | } | |
| 377 | 388 | for _, p := range panes { | |
| 378 | 389 | if p.cancel != nil { | |
| 379 | 390 | p.cancel() | |
| @@ -373,7 +373,18 @@ func (tb *tab) panesSnapshot() []*pane { | |||||
| 373 | func (tb *tab) closeAllPanes() { | 373 | func (tb *tab) closeAllPanes() { | ||
| 374 | tb.mu.Lock() | 374 | tb.mu.Lock() | ||
| 375 | panes := tb.panesSnapshot() | 375 | panes := tb.panesSnapshot() | ||
| 376 | + | floating := tb.floating.pane | |||
| 377 | + | // Invalidate before cancellation so a concurrently unblocked floating | |||
| 378 | + | // reader cannot reap a subsequently reused slot. | |||
| 379 | + | tb.floating.generation++ | |||
| 380 | + | tb.floating.state = floatingUninitialized | |||
| 381 | + | tb.floating.pane = nil | |||
| 382 | + | tb.floating.desiredVisible = false | |||
| 383 | + | tb.floating.launch = domain.FloatingConfig{} | |||
| 376 | tb.mu.Unlock() | 384 | tb.mu.Unlock() | ||
| 385 | + | if floating != nil { | |||
| 386 | + | panes = append(panes, floating) | |||
| 387 | + | } | |||
| 377 | for _, p := range panes { | 388 | for _, p := range panes { | ||
| 378 | if p.cancel != nil { | 389 | if p.cancel != nil { | ||
| 379 | p.cancel() | 390 | p.cancel() | ||