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		sess.floatingLaunchMu.Lock()
89		sess.floatingSpareStarting = false
90		if p != nil && !sess.floatingLaunchStopping {
91			sess.floatingSpare = &floatingSpare{pane: p, key: spareKey(spec)}
92			p.prestarted = true
93			spare := p
94			spare.onExit = func() {
95				spare.spareExited.Store(true)
96				// Still unclaimed: drop it. A claim takes the spare under the
97				// same lock, then reaps after install when spareExited is set.
98				if sess.dropFloatingSpare(spare) {
99					return
100				}
101				d.reapInstalledFloating(spare)
102			}
103			reader := p
104			d.sessWg.Go(func() { d.readPanePTY(reader) })
105			p = nil
106		}
107		sess.floatingLaunchMu.Unlock()
108		if p != nil {
109			closeFloatingPane(p)
110		}
111	})
112}
113
114// takeFloatingSpare removes the session spare. It returns the pane when the
115// spare was started with the same launch inputs as spec; a mismatched spare is
116// closed because it can never serve this launch. had reports whether a spare
117// existed, so the caller knows to start its replacement.
118func (sess *session) takeFloatingSpare(spec floatingLaunchSpec) (p *pane, had bool) {
119	sess.floatingLaunchMu.Lock()
120	spare := sess.floatingSpare
121	sess.floatingSpare = nil
122	sess.floatingLaunchMu.Unlock()
123	if spare == nil {
124		return nil, false
125	}
126	if spare.key != spareKey(spec) {
127		closeFloatingPane(spare.pane)
128		return nil, true
129	}
130	return spare.pane, true
131}
132
133// dropFloatingSpare closes p when it is still the session's unclaimed spare
134// and reports whether it did. A claimed spare is left to its tab.
135func (sess *session) dropFloatingSpare(p *pane) bool {
136	sess.floatingLaunchMu.Lock()
137	unclaimed := sess.floatingSpare != nil && sess.floatingSpare.pane == p
138	if unclaimed {
139		sess.floatingSpare = nil
140	}
141	sess.floatingLaunchMu.Unlock()
142	if unclaimed {
143		closeFloatingPane(p)
144	}
145	return unclaimed
146}
147
148// applySessionThemeToPane gives a pane the theme of the session's first
149// attachment, as createTab does for a new tab. A headless session leaves the
150// pane unthemed until the next theme application.
151func (d *Daemon) applySessionThemeToPane(sess *session, p *pane) {
152	attachments := sess.snapshotAttachments()
153	if len(attachments) == 0 {
154		return
155	}
156	t := d.effectiveTheme(attachments[0].getClientTheme())
157	p.mu.Lock()
158	applyPaneThemeLocked(p, t, false)
159	p.mu.Unlock()
160}
161
162// floatingSparePane returns the session's unclaimed spare pane, if any.
163func (sess *session) floatingSparePane() *pane {
164	sess.floatingLaunchMu.Lock()
165	defer sess.floatingLaunchMu.Unlock()
166	if sess.floatingSpare == nil {
167		return nil
168	}
169	return sess.floatingSpare.pane
170}
171
172// closeFloatingSpare releases the spare on session teardown. The caller has
173// already set floatingLaunchStopping, so no launch can publish a new one.
174func (sess *session) closeFloatingSpare() {
175	sess.floatingLaunchMu.Lock()
176	spare := sess.floatingSpare
177	sess.floatingSpare = nil
178	sess.floatingLaunchMu.Unlock()
179	if spare != nil {
180		closeFloatingPane(spare.pane)
181	}
182}
183
184// openFloatingPane opens one floating PTY for spec and returns its initialized,
185// unpublished pane, or nil when the launch failed or was cancelled.
186func (d *Daemon) openFloatingPane(sess *session, spec floatingLaunchSpec) *pane {
187	if err := spec.parentCtx.Err(); err != nil {
188		return nil
189	}
190	lifetime := d.newPaneProcessLifetime(spec.parentCtx, sess.ctx)
191	pty, err := d.ptys.Open(lifetime.ctx, spec.command, spec.args, spec.env, spec.cwd, spec.ptyGeometry)
192	if err != nil {
193		lifetime.abort()
194		if pty != nil {
195			_ = pty.Close()
196		}
197		d.log.Debug("floating spare spawn failed", "err", err, "session", spec.sessionName)
198		return nil
199	}
200	p := newPaneWithStableIDAndTitle(layout.PaneID("floating"), spec.paneStableID, pty, spec.size, spec.fallback, d.currentHistoryConfig())
201	p.geometry = spec.ptyGeometry
202	setScreenGeometry(p.screen, spec.ptyGeometry)
203	p.rect = spec.geometry.ptyRect()
204	p.popupGeometry = spec.geometry
205	if !lifetime.publish(p) {
206		_ = pty.Close()
207		return nil
208	}
209	return p
210}
211
212// adoptFloatingSpare fits a claimed spare to the claiming tab's launch spec
213// and installs it in the current slot generation. It runs in the launch worker
214// without any architecture lock, because PTY.Resize is external.
215func (d *Daemon) adoptFloatingSpare(sess *session, tb *tab, p *pane, spec floatingLaunchSpec, generation uint64) {
216	p.resizeMu.Lock()
217	p.mu.Lock()
218	resize := p.geometry != spec.ptyGeometry
219	if resize {
220		// The spare's reader is running: buffer its output across the
221		// resize so the redraw is parsed at the new size.
222		p.resizeApplying = true
223	}
224	p.mu.Unlock()
225	if resize {
226		if err := resizePTYGeometry(p.pty, spec.ptyGeometry); err != nil {
227			d.replayResizePending(sess, tb, p, false, domain.Rect{})
228			p.resizeMu.Unlock()
229			closeFloatingPane(p)
230			d.failFloatingLaunch(sess, tb, generation, true, spec.sessionName, err)
231			return
232		}
233	}
234	p.mu.Lock()
235	p.geometry = spec.ptyGeometry
236	setScreenGeometry(p.screen, spec.ptyGeometry)
237	p.rect = spec.geometry.ptyRect()
238	p.popupGeometry = spec.geometry
239	p.mu.Unlock()
240	if resize {
241		d.replayResizePending(sess, tb, p, false, domain.Rect{})
242	}
243	p.resizeMu.Unlock()
244	d.installFloating(sess, tb, p, generation)
245	if p.spareExited.Load() {
246		// The shell exited before the owner was published, so its reader
247		// could not reap the slot.
248		d.reapInstalledFloating(p)
249	}
250}

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