response.go RawHistoryFolder

1package uidriver
2
3import (
4	"context"
5	"errors"
6	"strings"
7	"unicode/utf8"
8
9	"github.com/bnema/vev/internal/domain"
10	"github.com/bnema/vev/internal/ports"
11)
12
13type envelope struct {
14	Version int            `json:"version"`
15	ID      uint64         `json:"id"`
16	Result  any            `json:"result,omitempty"`
17	Error   *responseError `json:"error,omitempty"`
18}
19
20type responseError struct {
21	Code     ports.UIErrorCode `json:"code"`
22	Message  string            `json:"message"`
23	Accepted bool              `json:"accepted"`
24	ActionID uint64            `json:"action_id,omitempty"`
25}
26
27// Ready is connection discovery, never an attachment registry.
28type Ready struct {
29	Attachment string                     `json:"attachment"`
30	Generation uint64                     `json:"generation"`
31	Control    bool                       `json:"control"`
32	Status     ports.UIPresentationStatus `json:"status"`
33}
34
35type contextResponse struct {
36	Attachment      string                     `json:"attachment"`
37	Generation      uint64                     `json:"generation"`
38	Session         sessionResponse            `json:"session"`
39	Focus           focusResponse              `json:"focus"`
40	Epoch           uint64                     `json:"output_epoch"`
41	State           uint64                     `json:"output_state"`
42	ViewRevision    uint64                     `json:"view_revision"`
43	ViewPublication uint64                     `json:"view_publication"`
44	Status          ports.UIPresentationStatus `json:"status"`
45}
46
47type sessionResponse struct {
48	LifecycleID string `json:"lifecycle_id"`
49	SessionName string `json:"session_name"`
50}
51
52type focusResponse struct {
53	TabID  domain.TabStableID  `json:"tab_id"`
54	PaneID domain.PaneStableID `json:"pane_id"`
55}
56
57type cursorResponse struct {
58	Row      int  `json:"row"`
59	Column   int  `json:"column"`
60	Visible  bool `json:"visible"`
61	Style    int  `json:"style"`
62	StyleSet bool `json:"style_set"`
63}
64
65type colorKind string
66
67const (
68	colorDefault colorKind = "default"
69	colorIndexed colorKind = "indexed"
70	colorRGB     colorKind = "rgb"
71)
72
73type colorResponse struct {
74	Kind  colorKind `json:"kind"`
75	Index uint8     `json:"index"`
76	R     uint8     `json:"r"`
77	G     uint8     `json:"g"`
78	B     uint8     `json:"b"`
79}
80
81type styleResponse struct {
82	Foreground     colorResponse `json:"foreground"`
83	Background     colorResponse `json:"background"`
84	Bold           bool          `json:"bold"`
85	Dim            bool          `json:"dim"`
86	Italic         bool          `json:"italic"`
87	Underline      bool          `json:"underline"`
88	UnderlineStyle uint8         `json:"underline_style"`
89	Blink          bool          `json:"blink"`
90	Inverse        bool          `json:"inverse"`
91	Strikethrough  bool          `json:"strikethrough"`
92}
93
94type cellResponse struct {
95	Text         string        `json:"text"`
96	Width        int           `json:"width"`
97	Continuation bool          `json:"continuation"`
98	Style        styleResponse `json:"style"`
99}
100
101type captureResponse struct {
102	Revision uint64          `json:"revision"`
103	Context  contextResponse `json:"context"`
104	Columns  int             `json:"columns"`
105	Rows     int             `json:"rows"`
106	Cursor   cursorResponse  `json:"cursor"`
107	Text     *string         `json:"text,omitempty"`
108	Cells    []cellResponse  `json:"cells,omitempty"`
109}
110
111type actionResponse struct {
112	ActionID uint64               `json:"action_id"`
113	Accepted bool                 `json:"accepted"`
114	Status   ports.UIActionStatus `json:"status"`
115	Revision uint64               `json:"revision"`
116	Context  contextResponse      `json:"context"`
117}
118
119type waitResponse struct {
120	ActionID     uint64               `json:"action_id,omitempty"`
121	ActionStatus ports.UIActionStatus `json:"action_status,omitempty"`
122	Revision     uint64               `json:"revision"`
123	Context      contextResponse      `json:"context"`
124}
125
126func publicContext(value ports.UIContext) contextResponse {
127	var lifecycleID string
128	if value.Route.Target.LifecycleID != (domain.SessionLifecycleID{}) {
129		lifecycleID = value.Route.Target.LifecycleID.String()
130	}
131	return contextResponse{Attachment: value.AttachmentHandle, Generation: value.Generation, Session: sessionResponse{LifecycleID: lifecycleID, SessionName: value.Route.Target.SessionName}, Focus: focusResponse{TabID: value.TabID, PaneID: value.FocusedPaneID}, Epoch: value.OutputEpoch, State: value.OutputState, ViewRevision: value.ViewRevision, ViewPublication: value.ViewPublication, Status: value.Status}
132}
133
134func publicColor(value ports.UIColor) colorResponse {
135	result := colorResponse{Kind: colorDefault}
136	switch value.Kind {
137	case ports.UIColorIndexed:
138		result.Kind = colorIndexed
139		result.Index = value.Index
140	case ports.UIColorRGB:
141		result.Kind = colorRGB
142		result.R, result.G, result.B = value.R, value.G, value.B
143	}
144	return result
145}
146
147func publicCapture(snapshot ports.UISnapshot, format captureFormat) (captureResponse, error) {
148	result := captureResponse{}
149	if snapshot.Columns < 1 || snapshot.Rows < 1 || snapshot.Columns > maxColumns || snapshot.Rows > maxRows {
150		return result, &ports.UIError{Code: ports.UIErrCaptureTooLarge}
151	}
152	if len(snapshot.Cells) != snapshot.Columns*snapshot.Rows {
153		return result, ports.ErrUIUnavailable
154	}
155	if !captureFitsResponse(snapshot, format) {
156		return result, &ports.UIError{Code: ports.UIErrCaptureTooLarge}
157	}
158	result = captureResponse{Revision: snapshot.Revision, Context: publicContext(snapshot.Context), Columns: snapshot.Columns, Rows: snapshot.Rows, Cursor: cursorResponse{Row: snapshot.Cursor.Row, Column: snapshot.Cursor.Column, Visible: snapshot.Cursor.Visible, Style: snapshot.Cursor.Style, StyleSet: snapshot.Cursor.StyleSet}}
159	if format == formatText || format == formatBoth {
160		var text strings.Builder
161		for row := 0; row < snapshot.Rows; row++ {
162			if row > 0 {
163				text.WriteByte('\n')
164			}
165			for _, cell := range snapshot.Cells[row*snapshot.Columns : (row+1)*snapshot.Columns] {
166				if cell.Continuation {
167					continue
168				}
169				if cell.Text == "" {
170					text.WriteByte(' ')
171				} else {
172					text.WriteString(cell.Text)
173				}
174			}
175		}
176		value := text.String()
177		result.Text = &value
178	}
179	if format == formatCells || format == formatBoth {
180		result.Cells = make([]cellResponse, len(snapshot.Cells))
181		for i, cell := range snapshot.Cells {
182			s := cell.Style
183			result.Cells[i] = cellResponse{Text: cell.Text, Width: cell.Width, Continuation: cell.Continuation, Style: styleResponse{Foreground: publicColor(s.Foreground), Background: publicColor(s.Background), Bold: s.Bold, Dim: s.Dim, Italic: s.Italic, Underline: s.Underline, UnderlineStyle: s.UnderlineStyle, Blink: s.Blink, Inverse: s.Inverse, Strikethrough: s.Strikethrough}}
184		}
185	}
186	return result, nil
187}
188
189// captureFitsResponse rejects a malicious or corrupted snapshot before
190// constructing a potentially large response object. The estimate is
191// intentionally conservative for JSON string escaping and cell style fields.
192func captureFitsResponse(snapshot ports.UISnapshot, format captureFormat) bool {
193	const captureOverhead = 4096
194	const cellOverhead = 320
195	size := captureOverhead
196	for _, cell := range snapshot.Cells {
197		if !utf8.ValidString(cell.Text) {
198			return false
199		}
200		if format == formatCells || format == formatBoth {
201			if len(cell.Text) > maxResponseBytes || size > maxResponseBytes-len(cell.Text)-cellOverhead {
202				return false
203			}
204			size += len(cell.Text) + cellOverhead
205		}
206		if (format == formatText || format == formatBoth) && !cell.Continuation {
207			if size > maxResponseBytes-len(cell.Text)-1 {
208				return false
209			}
210			size += len(cell.Text) + 1
211		}
212	}
213	return size <= maxResponseBytes
214}
215
216// Never serialize arbitrary error text: lower layers may include endpoints,
217// input, screen content, or environment. The public code is the safe message.
218func errorEnvelope(id uint64, err error) envelope {
219	failure := responseError{Code: ports.UIErrUnavailable}
220	var uiErr *ports.UIError
221	if errors.As(err, &uiErr) {
222		failure.Code = uiErr.Code
223		failure.Accepted = uiErr.Accepted
224		failure.ActionID = uiErr.ActionID
225	} else if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
226		failure.Code = ports.UIErrTimeout
227	}
228	failure.Message = string(failure.Code)
229	return envelope{Version: apiVersion, ID: id, Error: &failure}
230}

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