remote_preview.go RawHistoryFolder

1package daemon
2
3import (
4	"errors"
5
6	renderer "github.com/bnema/vev-vt"
7	"github.com/bnema/vev/internal/domain"
8	"github.com/bnema/vev/internal/protocol"
9)
10
11var errRemotePreviewNoSuchTarget = errors.New("daemon: remote preview target does not exist")
12
13// remotePreviewSession resolves the exact live session lifecycle a preview
14// names, or nil once that lifecycle is gone.
15func (d *Daemon) remotePreviewSession(name string, lifecycle domain.SessionLifecycleID) *session {
16	d.mu.Lock()
17	defer d.mu.Unlock()
18	sess := d.findByNameLocked(name)
19	if sess == nil || sess.incarnation != lifecycle {
20		return nil
21	}
22	return sess
23}
24
25// captureRemotePreview crops the focused pane of the exact target tab. It
26// never touches VT damage or attachment render state.
27func (d *Daemon) captureRemotePreview(request protocol.RemotePreviewRequest) (protocol.RemotePreview, error) {
28	target := request.Target
29	sess := d.remotePreviewSession(target.SessionName, target.LifecycleID)
30	if sess == nil {
31		return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
32	}
33	index, ok := remoteTargetTabIndexLocked(sess, protocol.SessionAttachTargetFromRemote(target))
34	if !ok {
35		return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
36	}
37
38	sess.mu.Lock()
39	if index < 0 || index >= len(sess.tabs) {
40		sess.mu.Unlock()
41		return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
42	}
43	tab := sess.tabs[index]
44	if tab == nil || domain.TabStableID(tab.stableID) != target.LiveTabID {
45		sess.mu.Unlock()
46		return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
47	}
48	sess.mu.Unlock()
49	tab.mu.Lock()
50	pane := tab.focusedPane()
51	if pane == nil {
52		tab.mu.Unlock()
53		return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
54	}
55	pane.mu.Lock()
56	if pane.screen == nil {
57		pane.mu.Unlock()
58		tab.mu.Unlock()
59		return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
60	}
61	source := pane.screen.Snapshot()
62	revision := pane.syncGen
63	pane.mu.Unlock()
64	tab.mu.Unlock()
65	if revision == 0 {
66		revision = 1
67	}
68
69	width := min(int(request.Width), source.Columns())
70	height := min(int(request.Height), source.Rows())
71	if width <= 0 || height <= 0 {
72		return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
73	}
74	startY, endY := source.Rows()-height, source.Rows()
75	if last := remotePreviewLastContentRow(source, width, startY, endY); last >= startY {
76		endY = last + 1
77	} else if last := remotePreviewLastContentRow(source, width, 0, startY); last >= 0 {
78		startY = max(0, last-height+1)
79		endY = last + 1
80	}
81	height = endY - startY
82	cells := make([]renderer.Cell, 0, width*height)
83	for y := 0; y < height; y++ {
84		row := source.Row(startY + y)
85		for x := 0; x < width; x++ {
86			cell, _ := cropRemotePreviewCell(row[x], x, width)
87			cells = append(cells, cell)
88		}
89	}
90	preview := protocol.RemotePreview{
91		Version:     protocol.RemotePreviewSchemaVersion,
92		Status:      protocol.RemotePreviewOK,
93		LifecycleID: target.LifecycleID,
94		TabID:       target.LiveTabID,
95		Revision:    revision,
96		Width:       uint16(width),
97		Height:      uint16(height),
98		Cells:       cells,
99	}
100	if err := protocol.ValidateRemotePreview(preview); err != nil {
101		// A crop must not split a wide cell. Return a safe unavailable response
102		// instead of exposing a malformed cell stream.
103		return protocol.RemotePreview{}, err
104	}
105	return preview, nil
106}
107
108// remotePreviewLastContentRow finds the final row with a visible glyph inside
109// the bounded viewport crop. Remote terminal frames retain their full PTY
110// height even when the only output is near the top; compacting the blank tail lets the
111// picker bottom-anchor short output instead of clipping it away.
112func remotePreviewLastContentRow(frame renderer.CellSource, width, start, end int) int {
113	for y := end - 1; y >= start; y-- {
114		for x := range width {
115			cell, splitWideRune := cropRemotePreviewCell(frame.Cell(x, y), x, width)
116			if !splitWideRune && remotePreviewCellVisible(cell) {
117				return y
118			}
119		}
120	}
121	return -1
122}
123
124// cropRemotePreviewCell converts a left wide-rune half at the right crop edge
125// to the styled blank that the picker can safely render. The flag distinguishes
126// that replacement from an actual styled blank in the source frame.
127func cropRemotePreviewCell(cell renderer.Cell, x, width int) (renderer.Cell, bool) {
128	if x == width-1 && !cell.Continuation && renderer.RuneWidth(cell.Rune) == 2 {
129		return renderer.Cell{Rune: ' ', Style: cell.Style}, true
130	}
131	return cell, false
132}
133
134func remotePreviewCellVisible(cell renderer.Cell) bool {
135	return !cell.Continuation &&
136		(cell.Rune != 0 && cell.Rune != ' ' || !cell.Style.Equal(renderer.DefaultStyle()))
137}

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