v0.2.0 80c61e3

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/ports"
11	"github.com/bnema/vev/internal/usecase/layout"
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, resizeApplying, resizeRetry, resizePending, 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	// resizeApplying gates VT parsing across PTY.Resize. The reader continues
124	// draining into resizePending so output is replayed against the target (or
125	// retained old) screen only after apply resolves.
126	resizeApplying   bool
127	resizeRetry      bool // PTY resize failed for the committed rectangle; retry on the next accepted plan.
128	resizePending    []byte
129	ptyResponses     []byte
130	ptyClipboards    []string
131	ptyAttention     bool
132	popupGeometry    floatingGeometry // last geometry committed after a successful floating resize
133	title            paneTitleState
134	ctx              context.Context
135	cancel           context.CancelFunc
136	processCloseOnce sync.Once
137	// onExit is set before the reader starts and never changed. Floating panes
138	// use it to reap their independent slot without touching the layout tree.
139	onExit func()
140}
141
142func closePaneProcess(p *pane) {
143	if p == nil {
144		return
145	}
146	p.processCloseOnce.Do(func() {
147		// Revocation shares pane.mu with VT parsing and owner publication, so no
148		// effect lease remains current once terminal teardown begins.
149		p.mu.Lock()
150		p.clearOwnerLocked()
151		p.mu.Unlock()
152		if p.cancel != nil {
153			p.cancel()
154		}
155		if p.pty != nil {
156			_ = p.pty.Close()
157		}
158	})
159}
160
161func newPane(id layout.PaneID, pty ports.PTY, sz domain.Size) *pane {
162	return newPaneWithStableID(id, fallbackStableID("p"), pty, sz)
163}
164
165func newPaneWithStableID(id layout.PaneID, stableID string, pty ports.PTY, sz domain.Size) *pane {
166	screen := vt.NewScreenWithHistory(sz.Cols, sz.Rows, vt.HistoryConfig{MaxRows: defaultScrollbackRows, MaxCells: defaultScrollbackCells})
167	return &pane{
168		id:       id,
169		stableID: stableID,
170		pty:      pty,
171		screen:   screen,
172		history:  screen.History(),
173		rect:     domain.Rect{Width: sz.Cols, Height: sz.Rows},
174		title:    paneTitleState{displayFallback: "sh"},
175	}
176}

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