v0.7.0 4c16dd8

floating_spare.go RawHistoryFolder

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}

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