preview.go RawHistoryFolder

1package protocol
2
3import (
4	"errors"
5	"math"
6	"time"
7	"unicode"
8	"unicode/utf8"
9
10	renderer "github.com/bnema/vev-vt"
11	"github.com/bnema/vev/internal/domain"
12)
13
14// RemotePreviewSchemaVersion is independent from the attachment protocol version.
15const RemotePreviewSchemaVersion uint16 = 1
16
17type RemotePreviewStatus uint8
18
19const (
20	RemotePreviewOK RemotePreviewStatus = iota
21	RemotePreviewUnavailable
22	RemotePreviewNoSuchTarget
23	RemotePreviewStale
24	RemotePreviewMalformed
25	RemotePreviewTooLarge
26)
27
28// RemotePreviewRequest is the bounded exact-target viewport a preview watch
29// observes. It is never a session message on its own.
30type RemotePreviewRequest struct {
31	Version uint16
32	Target  domain.RemoteSessionTarget
33	Width   uint16
34	Height  uint16
35}
36
37// RemotePreviewWatch is a client first message: the daemon answers with a
38// stream of RemotePreview frames for Request, at most one per MinInterval,
39// until the client closes the connection. A dead target ends the stream with
40// one RemotePreviewNoSuchTarget frame.
41type RemotePreviewWatch struct {
42	Request     RemotePreviewRequest
43	MinInterval time.Duration
44}
45
46// Watch interval bounds. The wire carries whole milliseconds.
47const (
48	RemotePreviewWatchMinInterval = 16 * time.Millisecond
49	RemotePreviewWatchMaxInterval = time.Second
50)
51
52var ErrInvalidRemotePreviewWatch = errors.New("invalid remote preview watch")
53
54// ValidateRemotePreviewWatch accepts only a valid request and a whole
55// millisecond interval inside the watch bounds.
56func ValidateRemotePreviewWatch(watch RemotePreviewWatch) error {
57	if ValidateRemotePreviewRequest(watch.Request) != nil ||
58		watch.MinInterval < RemotePreviewWatchMinInterval || watch.MinInterval > RemotePreviewWatchMaxInterval ||
59		watch.MinInterval%time.Millisecond != 0 {
60		return ErrInvalidRemotePreviewWatch
61	}
62	return nil
63}
64
65// RemotePreview is a bounded row-major styled-cell viewport. It is never
66// persisted, logged, or traced.
67type RemotePreview struct {
68	Version     uint16
69	Status      RemotePreviewStatus
70	LifecycleID domain.SessionLifecycleID
71	TabID       domain.TabStableID
72	Revision    uint64
73	Width       uint16
74	Height      uint16
75	Cells       []renderer.Cell
76}
77
78const (
79	RemotePreviewMaxWidth  = 256
80	RemotePreviewMaxHeight = 128
81	RemotePreviewMaxCells  = RemotePreviewMaxWidth * RemotePreviewMaxHeight
82	RemotePreviewMaxBytes  = 1 << 20
83)
84
85var (
86	ErrInvalidRemotePreviewRequest   = errors.New("ports: invalid remote preview request")
87	ErrInvalidRemotePreview          = errors.New("ports: invalid remote preview")
88	ErrRemotePreviewTooLarge         = errors.New("ports: remote preview exceeds size limit")
89	ErrRemotePreviewTimeout          = errors.New("ports: remote preview command timed out")
90	ErrRemotePreviewUnsupportedStyle = errors.New("ports: remote preview has unsupported style")
91)
92
93func ValidateRemotePreviewRequest(request RemotePreviewRequest) error {
94	if request.Version != RemotePreviewSchemaVersion || request.Target.Stopped || request.Target.Validate() != nil {
95		return ErrInvalidRemotePreviewRequest
96	}
97	// These fields are encoded with uint16 byte lengths. Reject an oversized
98	// nested route before putString truncates it on the wire.
99	if len(request.Target.Endpoint) > math.MaxUint16 ||
100		len(request.Target.DisplayOrigin) > math.MaxUint16 ||
101		len(request.Target.SessionName) > math.MaxUint16 ||
102		len(request.Target.LiveTabID) > math.MaxUint16 {
103		return ErrInvalidRemotePreviewRequest
104	}
105	if request.Width == 0 || request.Height == 0 || int(request.Width) > RemotePreviewMaxWidth || int(request.Height) > RemotePreviewMaxHeight {
106		return ErrInvalidRemotePreviewRequest
107	}
108	return nil
109}
110
111func ValidateRemotePreview(preview RemotePreview) error {
112	if preview.Version != RemotePreviewSchemaVersion || preview.Status > RemotePreviewTooLarge {
113		return ErrInvalidRemotePreview
114	}
115	if preview.Status != RemotePreviewOK {
116		if preview.Width != 0 || preview.Height != 0 || len(preview.Cells) != 0 {
117			return ErrInvalidRemotePreview
118		}
119		return nil
120	}
121	if preview.LifecycleID == (domain.SessionLifecycleID{}) || domain.ValidateTabStableID(preview.TabID) != nil || preview.Revision == 0 {
122		return ErrInvalidRemotePreview
123	}
124	if preview.Width == 0 || preview.Height == 0 || int(preview.Width) > RemotePreviewMaxWidth || int(preview.Height) > RemotePreviewMaxHeight {
125		return ErrInvalidRemotePreview
126	}
127	want := int(preview.Width) * int(preview.Height)
128	if want > RemotePreviewMaxCells || len(preview.Cells) != want {
129		return ErrRemotePreviewTooLarge
130	}
131	if !previewCellRunValid(int(preview.Width), preview.Cells) {
132		return ErrInvalidRemotePreview
133	}
134	return nil
135}
136
137func validPreviewCellStyle(style renderer.Style) bool {
138	return style.Attrs&^(renderer.AttrDim|renderer.AttrUnderline|renderer.AttrBlink|renderer.AttrStrikethrough) == 0 &&
139		style.UnderlineStyle <= renderer.UnderlineDashed &&
140		style.Foreground >= math.MinInt16 && style.Foreground <= math.MaxInt16 &&
141		style.Background >= math.MinInt16 && style.Background <= math.MaxInt16 &&
142		style.UnderlineColor >= math.MinInt16 && style.UnderlineColor <= math.MaxInt16
143}
144
145func (p RemotePreview) FrameRows() [][]renderer.Cell {
146	if p.Width == 0 || p.Height == 0 || len(p.Cells) != int(p.Width)*int(p.Height) {
147		return nil
148	}
149	rows := make([][]renderer.Cell, p.Height)
150	width := int(p.Width)
151	for y := range rows {
152		rows[y] = append([]renderer.Cell(nil), p.Cells[y*width:(y+1)*width]...)
153	}
154	return rows
155}
156
157// previewCellRunValid reports whether cells form the row-major run of a
158// width-wide viewport: every rune and style is displayable, and each
159// continuation column follows a double-width cell on the same row.
160func previewCellRunValid(width int, cells []renderer.Cell) bool {
161	if width <= 0 {
162		return false
163	}
164	for i, cell := range cells {
165		if !utf8.ValidRune(cell.Rune) || (cell.Rune != 0 && unicode.IsControl(cell.Rune)) || !validPreviewCellStyle(cell.Style) {
166			return false
167		}
168		rowStart := (i / width) * width
169		rowEnd := rowStart + width
170		if cell.Continuation {
171			if cell.Rune != 0 || i == rowStart || cells[i-1].Continuation || renderer.RuneWidth(cells[i-1].Rune) != 2 {
172				return false
173			}
174		} else if renderer.RuneWidth(cell.Rune) == 2 && (i+1 >= rowEnd || !cells[i+1].Continuation) {
175			return false
176		}
177	}
178	return true
179}

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