1package daemon
2
3import (
4 "github.com/bnema/vev/internal/usecase/layout"
5)
6
7// floatingSpare is one pre-started floating shell owned by a session rather
8// than by a tab. The first tab that opens its floating pane claims it, and the
9// session then starts a new spare in the background, so a session keeps at
10// most one idle floating shell instead of one hidden shell per tab. A spare is
11// never persisted or restored. Its reader starts only once a tab installs it;
12// until then the kernel PTY buffer holds its first prompt. Its VEV identity
13// names the tab that started it; floating panes are not --self targets.
14//
15// A claim takes the claiming tab's geometry (resize before the reader starts)
16// and cwd: a spare started in another directory is discarded and the claim
17// launches a fresh shell instead.
18//
19// The spare is guarded by session.floatingLaunchMu, which is never held across
20// PTY operations or architecture locks.
21type floatingSpare struct {
22 pane *pane
23 cwd string
24 command string
25}
26
27// ensureFloatingSpare starts the session's spare when none exists or is
28// starting. tb supplies the initial geometry and cwd; a claim from another tab
29// resizes it or, when the cwd differs, replaces it.
30func (d *Daemon) ensureFloatingSpare(sess *session, tb *tab) {
31 if d == nil || sess == nil || tb == nil || d.ptys == nil {
32 return
33 }
34 cfg := d.currentFloatingConfig()
35 sess.floatingLaunchMu.Lock()
36 if sess.floatingLaunchStopping || sess.floatingSpare != nil || sess.floatingSpareStarting {
37 sess.floatingLaunchMu.Unlock()
38 return
39 }
40 sess.floatingSpareStarting = true
41 sess.floatingLaunchMu.Unlock()
42
43 spec, err := d.newFloatingLaunchSpec(sess, tb, cfg, false)
44 // The spare belongs to the session, so closing the tab that started it
45 // must not cancel it.
46 spec.parentCtx = sess.ctx
47 var launch *floatingLaunch
48 ok := err == nil && spec.parentCtx != nil
49 if ok {
50 launch, ok = sess.registerFloatingLaunch()
51 }
52 if !ok {
53 sess.floatingLaunchMu.Lock()
54 sess.floatingSpareStarting = false
55 sess.floatingLaunchMu.Unlock()
56 return
57 }
58 d.sessWg.Go(func() {
59 defer sess.finishFloatingLaunch(launch)
60 p := d.openFloatingPane(sess, spec)
61 sess.floatingLaunchMu.Lock()
62 sess.floatingSpareStarting = false
63 if p != nil && !sess.floatingLaunchStopping {
64 sess.floatingSpare = &floatingSpare{pane: p, cwd: spec.cwd, command: cfg.Command}
65 p = nil
66 }
67 sess.floatingLaunchMu.Unlock()
68 if p != nil {
69 closeFloatingPane(p)
70 }
71 })
72}
73
74// takeFloatingSpare removes the session spare. It returns the pane when the
75// spare was started for cwd and command; a mismatched spare is closed because
76// it can never serve this directory or config. had reports whether a spare
77// existed, so the caller knows to start its replacement.
78func (sess *session) takeFloatingSpare(cwd, command string) (p *pane, had bool) {
79 sess.floatingLaunchMu.Lock()
80 spare := sess.floatingSpare
81 sess.floatingSpare = nil
82 sess.floatingLaunchMu.Unlock()
83 if spare == nil {
84 return nil, false
85 }
86 if spare.cwd != cwd || spare.command != command {
87 closeFloatingPane(spare.pane)
88 return nil, true
89 }
90 return spare.pane, true
91}
92
93// closeFloatingSpare releases the spare on session teardown. The caller has
94// already set floatingLaunchStopping, so no launch can publish a new one.
95func (sess *session) closeFloatingSpare() {
96 sess.floatingLaunchMu.Lock()
97 spare := sess.floatingSpare
98 sess.floatingSpare = nil
99 sess.floatingLaunchMu.Unlock()
100 if spare != nil {
101 closeFloatingPane(spare.pane)
102 }
103}
104
105// openFloatingPane opens one floating PTY for spec and returns its initialized,
106// unpublished pane, or nil when the launch failed or was cancelled.
107func (d *Daemon) openFloatingPane(sess *session, spec floatingLaunchSpec) *pane {
108 if err := spec.parentCtx.Err(); err != nil {
109 return nil
110 }
111 lifetime := d.newPaneProcessLifetime(spec.parentCtx, sess.ctx)
112 pty, err := d.ptys.Open(lifetime.ctx, spec.command, spec.args, spec.env, spec.cwd, spec.ptyGeometry)
113 if err != nil {
114 lifetime.abort()
115 if pty != nil {
116 _ = pty.Close()
117 }
118 d.log.Debug("floating spare spawn failed", "err", err, "session", spec.sessionName)
119 return nil
120 }
121 p := newPaneWithStableIDAndTitle(layout.PaneID("floating"), spec.paneStableID, pty, spec.size, spec.fallback, d.currentHistoryConfig())
122 p.geometry = spec.ptyGeometry
123 setScreenGeometry(p.screen, spec.ptyGeometry)
124 p.rect = spec.geometry.ptyRect()
125 p.popupGeometry = spec.geometry
126 if !lifetime.publish(p) {
127 _ = pty.Close()
128 return nil
129 }
130 return p
131}
132
133// adoptFloatingSpare fits a claimed spare to the claiming tab's launch spec
134// and installs it in the current slot generation. It runs in the launch worker
135// without any architecture lock, because PTY.Resize is external.
136func (d *Daemon) adoptFloatingSpare(sess *session, tb *tab, p *pane, spec floatingLaunchSpec, generation uint64) {
137 if p.geometry != spec.ptyGeometry {
138 if err := p.pty.Resize(spec.ptyGeometry); err != nil {
139 closeFloatingPane(p)
140 d.failFloatingLaunch(sess, tb, generation, true, spec.sessionName, err)
141 return
142 }
143 }
144 p.mu.Lock()
145 p.geometry = spec.ptyGeometry
146 setScreenGeometry(p.screen, spec.ptyGeometry)
147 p.rect = spec.geometry.ptyRect()
148 p.popupGeometry = spec.geometry
149 p.mu.Unlock()
150 p.onExit = func() { d.reapInstalledFloating(p) }
151 d.installFloating(sess, tb, p, generation)
152}