v0.8.1 1f54f61

floating_spare.go RawHistoryFolder

1package daemon
2
3import (
4	"strings"
5
6	"github.com/bnema/vev/internal/domain"
7	"github.com/bnema/vev/internal/domain/layout"
8)
9
10// floatingSpare is one pre-started floating shell owned by a session rather
11// than by a tab. The first tab that opens its floating pane claims it, and the
12// session then starts a new spare in the background, so a session keeps at
13// most one idle floating shell instead of one hidden shell per tab. A spare is
14// never persisted or restored. Its reader starts with the shell, so startup
15// terminal queries are answered before any claim; with no owner its screen
16// changes publish nothing. Its VEV identity names the tab that started it;
17// floating panes are not --self targets.
18//
19// A claim takes the claiming tab's geometry and requires the same cwd,
20// floating.command, shell and environment: a mismatched spare is discarded and
21// the claim launches a fresh shell instead.
22//
23// The spare is guarded by session.floatingLaunchMu, which is never held across
24// PTY operations or architecture locks.
25type floatingSpare struct {
26	pane *pane
27	key  floatingSpareKey
28}
29
30// floatingSpareKey is every launch input a claim must share with the spare.
31// env excludes the per-launch VEV identity.
32type floatingSpareKey struct {
33	cwd, floatingCommand, command, args, env string
34}
35
36func spareKey(spec floatingLaunchSpec) floatingSpareKey {
37	env := make([]string, 0, len(spec.env))
38	for _, entry := range spec.env {
39		if key, _, _ := environmentEntry(entry); key != "VEV" {
40			env = append(env, entry)
41		}
42	}
43	return floatingSpareKey{
44		cwd: spec.cwd, floatingCommand: spec.floatingCommand, command: spec.command,
45		args: strings.Join(spec.args, "\x00"), env: strings.Join(env, "\x00"),
46	}
47}
48
49// ensureFloatingSpare starts the session's spare when none exists or is
50// starting. tb supplies the initial geometry and cwd; a claim from another tab
51// resizes it or, when the cwd differs, replaces it.
52func (d *Daemon) ensureFloatingSpare(sess *session, tb *tab) {
53	if d == nil || sess == nil || tb == nil || d.ptys == nil {
54		return
55	}
56	cfg := d.currentFloatingConfig()
57	sess.floatingLaunchMu.Lock()
58	if sess.floatingLaunchStopping || sess.floatingSpare != nil || sess.floatingSpareStarting {
59		sess.floatingLaunchMu.Unlock()
60		return
61	}
62	sess.floatingSpareStarting = true
63	sess.floatingLaunchMu.Unlock()
64
65	spec, err := d.newFloatingLaunchSpec(sess, tb, cfg, false)
66	// The spare belongs to the session, so closing the tab that started it
67	// must not cancel it.
68	spec.parentCtx = sess.ctx
69	var launch *floatingLaunch
70	ok := err == nil && spec.parentCtx != nil
71	if ok {
72		launch, ok = sess.registerFloatingLaunch()
73	}
74	if !ok {
75		sess.floatingLaunchMu.Lock()
76		sess.floatingSpareStarting = false
77		sess.floatingLaunchMu.Unlock()
78		return
79	}
80	d.sessWg.Go(func() {
81		defer sess.finishFloatingLaunch(launch)
82		p := d.openFloatingPane(sess, spec)
83		if p != nil {
84			// The reader answers color and scheme queries, so the spare
85			// starts with the session theme, as a new tab does.
86			d.applySessionThemeToPane(sess, p)
87		}
88		var published *pane
89		sess.floatingLaunchMu.Lock()
90		sess.floatingSpareStarting = false
91		if p != nil && !sess.floatingLaunchStopping {
92			published = p
93			sess.floatingSpare = &floatingSpare{pane: p, key: spareKey(spec)}
94			p.prestarted = true
95			spare := p
96			spare.onExit = func() {
97				spare.spareExited.Store(true)
98				// Still unclaimed: drop it. A claim takes the spare under the
99				// same lock, then reaps after install when spareExited is set.
100				if sess.dropFloatingSpare(spare) {
101					return
102				}
103				d.reapInstalledFloating(spare)
104			}
105			reader := p
106			d.sessWg.Go(func() { d.readPanePTY(reader) })
107			p = nil
108		}
109		sess.floatingLaunchMu.Unlock()
110		if p != nil {
111			closeFloatingPane(p)
112		}
113		if published != nil {
114			// A theme change between the first apply and publication could
115			// not see the spare; any later one does, so apply once more.
116			d.applySessionThemeToPane(sess, published)
117		}
118	})
119}
120
121// takeFloatingSpare removes the session spare. It returns the pane when the
122// spare was started with the same launch inputs as spec; a mismatched spare is
123// closed because it can never serve this launch. had reports whether a spare
124// existed, so the caller knows to start its replacement.
125func (sess *session) takeFloatingSpare(spec floatingLaunchSpec) (p *pane, had bool) {
126	sess.floatingLaunchMu.Lock()
127	spare := sess.floatingSpare
128	sess.floatingSpare = nil
129	sess.floatingLaunchMu.Unlock()
130	if spare == nil {
131		return nil, false
132	}
133	if spare.key != spareKey(spec) {
134		closeFloatingPane(spare.pane)
135		return nil, true
136	}
137	return spare.pane, true
138}
139
140// dropFloatingSpare closes p when it is still the session's unclaimed spare
141// and reports whether it did. A claimed spare is left to its tab.
142func (sess *session) dropFloatingSpare(p *pane) bool {
143	sess.floatingLaunchMu.Lock()
144	unclaimed := sess.floatingSpare != nil && sess.floatingSpare.pane == p
145	if unclaimed {
146		sess.floatingSpare = nil
147	}
148	sess.floatingLaunchMu.Unlock()
149	if unclaimed {
150		closeFloatingPane(p)
151	}
152	return unclaimed
153}
154
155// applySessionThemeToPane gives a pane the theme of the session's first
156// attachment, as createTab does for a new tab. A headless session leaves the
157// pane unthemed until the next theme application.
158func (d *Daemon) applySessionThemeToPane(sess *session, p *pane) {
159	attachments := sess.snapshotAttachments()
160	if len(attachments) == 0 {
161		return
162	}
163	applyClientThemeToPane(d, attachments[0], p)
164}
165
166// applyClientThemeToPane reads ac's theme while holding p.mu, so a concurrent
167// theme application either is seen here or writes the pane after it; a stale
168// theme can never overwrite a newer one. ac.themeMu is a leaf lock.
169func applyClientThemeToPane(d *Daemon, ac *attachedClient, p *pane) {
170	p.mu.Lock()
171	defer p.mu.Unlock()
172	applyPaneThemeLocked(p, d.effectiveTheme(ac.getClientTheme()), false)
173}
174
175// floatingSparePane returns the session's unclaimed spare pane, if any.
176func (sess *session) floatingSparePane() *pane {
177	sess.floatingLaunchMu.Lock()
178	defer sess.floatingLaunchMu.Unlock()
179	if sess.floatingSpare == nil {
180		return nil
181	}
182	return sess.floatingSpare.pane
183}
184
185// closeFloatingSpare releases the spare on session teardown. The caller has
186// already set floatingLaunchStopping, so no launch can publish a new one.
187func (sess *session) closeFloatingSpare() {
188	sess.floatingLaunchMu.Lock()
189	spare := sess.floatingSpare
190	sess.floatingSpare = nil
191	sess.floatingLaunchMu.Unlock()
192	if spare != nil {
193		closeFloatingPane(spare.pane)
194	}
195}
196
197// openFloatingPane opens one floating PTY for spec and returns its initialized,
198// unpublished pane, or nil when the launch failed or was cancelled.
199func (d *Daemon) openFloatingPane(sess *session, spec floatingLaunchSpec) *pane {
200	if err := spec.parentCtx.Err(); err != nil {
201		return nil
202	}
203	lifetime := d.newPaneProcessLifetime(spec.parentCtx, sess.ctx)
204	pty, err := d.ptys.Open(lifetime.ctx, spec.command, spec.args, spec.env, spec.cwd, spec.ptyGeometry)
205	if err != nil {
206		lifetime.abort()
207		if pty != nil {
208			_ = pty.Close()
209		}
210		d.log.Debug("floating spare spawn failed", "err", err, "session", spec.sessionName)
211		return nil
212	}
213	p := newPaneWithStableIDAndTitle(layout.PaneID("floating"), spec.paneStableID, pty, spec.size, spec.fallback, d.currentHistoryConfig())
214	p.geometry = spec.ptyGeometry
215	setScreenGeometry(p.screen, spec.ptyGeometry)
216	p.rect = spec.geometry.ptyRect()
217	p.popupGeometry = spec.geometry
218	if !lifetime.publish(p) {
219		_ = pty.Close()
220		return nil
221	}
222	return p
223}
224
225// adoptFloatingSpare fits a claimed spare to the claiming tab's launch spec
226// and installs it in the current slot generation. It runs in the launch worker
227// without any architecture lock, because PTY.Resize is external.
228func (d *Daemon) adoptFloatingSpare(sess *session, tb *tab, p *pane, spec floatingLaunchSpec, generation uint64) {
229	p.resizeMu.Lock()
230	p.mu.Lock()
231	resize := p.geometry != spec.ptyGeometry
232	if resize {
233		// The spare's reader is running: buffer its output across the
234		// resize so the redraw is parsed at the new size.
235		p.resizeApplying = true
236	}
237	p.mu.Unlock()
238	if resize {
239		if err := resizePTYGeometry(p.pty, spec.ptyGeometry); err != nil {
240			d.replayResizePending(sess, tb, p, false, domain.Rect{})
241			p.resizeMu.Unlock()
242			closeFloatingPane(p)
243			d.failFloatingLaunch(sess, tb, generation, true, spec.sessionName, err)
244			return
245		}
246	}
247	p.mu.Lock()
248	p.geometry = spec.ptyGeometry
249	setScreenGeometry(p.screen, spec.ptyGeometry)
250	p.rect = spec.geometry.ptyRect()
251	p.popupGeometry = spec.geometry
252	p.mu.Unlock()
253	if resize {
254		d.replayResizePending(sess, tb, p, false, domain.Rect{})
255	}
256	p.resizeMu.Unlock()
257	d.installFloating(sess, tb, p, generation)
258	if p.spareExited.Load() {
259		// The shell exited before the owner was published, so its reader
260		// could not reap the slot.
261		d.reapInstalledFloating(p)
262	}
263}

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