pane.go RawHistoryFolder

1package daemon
2
3import (
4	"context"
5	"sync"
6	"sync/atomic"
7
8	vt "github.com/bnema/vev-vt"
9	"github.com/bnema/vev/internal/domain"
10	"github.com/bnema/vev/internal/domain/layout"
11	"github.com/bnema/vev/internal/ports"
12)
13
14// paneProcessLifetime owns the context passed to PTYFactory.Open. Its daemon
15// parent lets an installed pane move between sessions without changing process
16// lifetime. Temporary opening parents bound an unpublished Open; publish
17// atomically detaches those parents before transferring cancellation to pane.
18type paneProcessLifetime struct {
19	ctx    context.Context
20	cancel context.CancelFunc
21
22	mu             sync.Mutex
23	opening        bool
24	openingParents []context.Context
25	stopOpening    []func() bool
26}
27
28func (d *Daemon) newPaneProcessLifetime(openingParents ...context.Context) *paneProcessLifetime {
29	parent := d.paneProcessCtx
30	if parent == nil {
31		parent = context.Background()
32	}
33	ctx, rawCancel := context.WithCancel(parent)
34	var cancelOnce sync.Once
35	lifetime := &paneProcessLifetime{
36		ctx:            ctx,
37		cancel:         func() { cancelOnce.Do(rawCancel) },
38		opening:        true,
39		openingParents: append([]context.Context(nil), openingParents...),
40	}
41	for _, openingParent := range lifetime.openingParents {
42		if openingParent == nil {
43			continue
44		}
45		lifetime.stopOpening = append(lifetime.stopOpening, context.AfterFunc(openingParent, func() {
46			lifetime.mu.Lock()
47			if lifetime.opening {
48				lifetime.cancel()
49			}
50			lifetime.mu.Unlock()
51		}))
52	}
53	return lifetime
54}
55
56// publish transfers a successfully opened process to p. A cancellation that
57// wins before this call prevents publication; one that starts afterward no
58// longer belongs to the source session or tab.
59func (l *paneProcessLifetime) publish(p *pane) bool {
60	if l == nil || p == nil {
61		return false
62	}
63	l.mu.Lock()
64	defer l.mu.Unlock()
65	for _, parent := range l.openingParents {
66		if parent != nil && parent.Err() != nil {
67			l.opening = false
68			l.stopOpeningParentsLocked()
69			l.cancel()
70			return false
71		}
72	}
73	if l.ctx.Err() != nil {
74		l.opening = false
75		l.stopOpeningParentsLocked()
76		l.cancel()
77		return false
78	}
79	l.opening = false
80	l.stopOpeningParentsLocked()
81	p.ctx = l.ctx
82	p.cancel = l.cancel
83	return true
84}
85
86func (l *paneProcessLifetime) abort() {
87	if l == nil {
88		return
89	}
90	l.mu.Lock()
91	l.opening = false
92	l.stopOpeningParentsLocked()
93	l.cancel()
94	l.mu.Unlock()
95}
96
97func (l *paneProcessLifetime) stopOpeningParentsLocked() {
98	for _, stop := range l.stopOpening {
99		stop()
100	}
101	l.stopOpening = nil
102	l.openingParents = nil
103}
104
105// pane owns one PTY and its terminal screen state.
106// Lock order when multiple locks are held is:
107// attachedClient.sendMu > Daemon.mu > session.mu > tab.mu > pane.mu.
108// The PTY reader takes only pane.mu, so child output never waits on client IO.
109type pane struct {
110	id       layout.PaneID
111	stableID string
112	pty      ports.PTY
113	mu       sync.Mutex // guards screen, history, syncGen, rect, geometry, graphics scene geometry, resize state, PTY side effects, title, and owner publication
114	owner    atomic.Pointer[paneOwner]
115	// ownerGeneration is advanced only while mu is held. Readers consume the
116	// generation through the immutable owner pointer above.
117	ownerGeneration uint64
118	resizeMu        sync.Mutex // serializes PTY resizes without holding mu
119	screen          *vt.Screen
120	history         *vt.History
121	syncGen         uint64
122	rect            domain.Rect
123	geometry        domain.Geometry
124	// graphicsCoordinateGeometry is the pixel-to-cell unit basis of the current
125	// non-empty placement scene. Resizing the attachment changes screen geometry
126	// but must not reinterpret coordinates retained by that scene.
127	graphicsCoordinateGeometry domain.Geometry
128	// graphicsPlacementScene is meaningful only with the coordinate geometry
129	// above and records whether that frozen basis currently owns placements.
130	graphicsPlacementScene bool
131	// resizeApplying gates VT parsing across PTY.Resize. The reader continues
132	// draining into resizePending so output is replayed against the target (or
133	// retained old) screen only after apply resolves.
134	resizeApplying   bool
135	resizeRetry      bool // PTY resize failed for the committed rectangle; retry on the next accepted plan.
136	resizePending    []byte
137	ptyResponses     []byte
138	ptyClipboards    []string
139	ptyAttention     bool
140	popupGeometry    floatingGeometry // last geometry committed after a successful floating resize
141	title            paneTitleState
142	ctx              context.Context
143	cancel           context.CancelFunc
144	processCloseOnce sync.Once
145	// onExit is set before the reader starts and never changed. Floating panes
146	// use it to reap their independent slot without touching the layout tree.
147	onExit func()
148	// prestarted marks a floating spare whose reader runs before any owner is
149	// published, so terminal queries from a starting shell are answered.
150	// Set before the reader starts and never changed.
151	prestarted bool
152	// spareExited records that a prestarted spare's reader ended, so a claim
153	// racing that exit still reaps the slot it installed.
154	spareExited atomic.Bool
155}
156
157func closePaneProcess(p *pane) {
158	if p == nil {
159		return
160	}
161	p.processCloseOnce.Do(func() {
162		// Revocation shares pane.mu with VT parsing and owner publication, so no
163		// effect lease remains current once terminal teardown begins.
164		p.mu.Lock()
165		p.clearOwnerLocked()
166		p.mu.Unlock()
167		if p.cancel != nil {
168			p.cancel()
169		}
170		if p.pty != nil {
171			_ = p.pty.Close()
172		}
173	})
174}
175
176func newPane(id layout.PaneID, pty ports.PTY, sz domain.Size) *pane {
177	return newPaneWithStableID(id, fallbackStableID("p"), pty, sz)
178}
179
180func newPaneWithStableID(id layout.PaneID, stableID string, pty ports.PTY, sz domain.Size) *pane {
181	return newPaneWithStableIDAndTitle(id, stableID, pty, sz, defaultShellTitle, historyConfigFor(domain.DefaultScrollbackConfig()))
182}
183
184func newPaneWithStableIDAndTitle(id layout.PaneID, stableID string, pty ports.PTY, sz domain.Size, title string, historyConfig vt.HistoryConfig) *pane {
185	screen := vt.NewScreenWithHistory(sz.Cols, sz.Rows, historyConfig)
186	return &pane{
187		id:       id,
188		stableID: stableID,
189		pty:      pty,
190		screen:   screen,
191		history:  screen.History(),
192		rect:     domain.Rect{Width: sz.Cols, Height: sz.Rows},
193		title:    paneTitleState{displayFallback: title},
194	}
195}

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