session.go RawHistoryFolder

1// Package protocol defines vev's typed, transport-neutral client/daemon
2// contract. Binary framing and codecs live outside this package.
3package protocol
4
5import (
6	"errors"
7	"fmt"
8	"math"
9	"time"
10
11	renderer "github.com/bnema/vev-vt"
12	"github.com/bnema/vev/internal/domain"
13)
14
15// Version is the negotiated vev session protocol version. It carries the
16// daemonmux physical preamble conversation (multiplex.proto), whose
17// negotiated ceilings and accepted daemon binding ride the same wire
18// version as the session and broker conversations.
19const Version uint16 = 64
20
21// HandshakeTimeout bounds every transport handshake from connect through the
22// first committed publication. It excludes the preceding client-local
23// warm-reuse probe, which has its own short budget: a cold dial that follows
24// a warm miss still receives the full handshake budget.
25const HandshakeTimeout = 15 * time.Second
26
27// MaxOutputDataLen reserves the type byte, fixed Output fields, and the largest
28// valid ViewContext (publication, lifecycle, session name, ephemeral flag, tab
29// and pane IDs, including string lengths) within the 16 MiB frame limit.
30const MaxOutputDataLen = (16 << 20) - 1 - 55 - (8 + 16 + 2 + 64 + 1 + 2 + 128 + 2 + 128)
31
32// MaxOutputWindow caps output states sent before client acknowledgement.
33const MaxOutputWindow = 8
34
35// ConnectionCapabilities describes transport behavior relevant to session
36// flow without naming a concrete carriage.
37type ConnectionCapabilities struct {
38	OutputDataLimit       int
39	PreferredOutputWindow uint8
40	AsyncSend             bool
41	OwnedSynchronousSend  bool
42	LinkState             bool
43}
44
45var (
46	ErrInvalidSize              = errors.New("ports: invalid terminal size")
47	ErrInvalidHello             = errors.New("ports: invalid hello")
48	ErrInvalidOutput            = errors.New("ports: invalid output")
49	ErrInvalidAck               = errors.New("ports: invalid ack")
50	ErrInvalidAttachTarget      = errors.New("ports: invalid attach target")
51	ErrInvalidNavigation        = errors.New("ports: invalid navigation")
52	ErrInvalidEnvironmentPolicy = errors.New("ports: invalid environment policy")
53	ErrInvalidClientNotice      = errors.New("ports: invalid client notice")
54	ErrInvalidTerminalFocus     = errors.New("ports: invalid terminal focus")
55	ErrInvalidDetached          = errors.New("ports: invalid detached reason")
56)
57
58const (
59	IntentEphemeral uint8 = 0
60	IntentNew       uint8 = 1
61	IntentAttach    uint8 = 2
62	IntentResume    uint8 = 3
63)
64
65type EnvironmentPolicy uint8
66
67const (
68	EnvironmentPolicyClientOwned EnvironmentPolicy = iota
69	EnvironmentPolicyDaemonOwned
70)
71
72const (
73	CapabilityResume  uint32 = 1 << 0
74	CapabilityUDP     uint32 = 1 << 1
75	CapabilityPredict uint32 = 1 << 2
76)
77
78const (
79	ErrVersionMismatch    uint16 = 1
80	ErrNoSuchSession      uint16 = 2
81	ErrNameTaken          uint16 = 3
82	ErrServerShutdown     uint16 = 4
83	ErrInvalidSessionName uint16 = 5
84	ErrUnknownCommand     uint16 = 6
85	ErrNotScriptable      uint16 = 7
86	ErrInvalidCommandArgs uint16 = 8
87	ErrNoSuchTarget       uint16 = 9
88	ErrAmbiguousTarget    uint16 = 10
89	ErrInternal           uint16 = 255
90)
91
92const (
93	ReasonDetach         uint8 = 0
94	ReasonSessionKilled  uint8 = 1
95	ReasonServerShutdown uint8 = 2
96	ReasonReplaced       uint8 = 3
97	// ReasonDetachToPicker transfers terminal ownership to the client picker.
98	ReasonDetachToPicker uint8 = 4
99)
100
101// Hello is sent by the client immediately after connecting.
102type Hello struct {
103	Version     uint16
104	Intent      uint8
105	ClientID    [16]byte
106	ResumeToken uint64
107	Name        string
108	Size        domain.Size
109	PixelWidth  int
110	PixelHeight int
111	TermEnv     string
112	Cwd         string
113	TrueColor   bool
114	// KittyDirectGraphics is an explicit declaration that the client has
115	// probed its direct outer terminal and it accepts Kitty graphics output.
116	KittyDirectGraphics bool
117	// KittyKeyboard declares that the client enabled the kitty keyboard
118	// protocol on its outer terminal, so input carries kitty key sequences.
119	KittyKeyboard          bool
120	MaxOutputInFlight      uint8
121	Env                    []string
122	SessionTarget          *SessionAttachTarget
123	ExactTarget            *ExactSessionTarget
124	PreferredTabID         domain.TabStableID
125	EnvironmentPolicy      EnvironmentPolicy
126	NavigationCapabilities NavigationCapabilities
127	Remote                 bool
128	// TerminalFocus is the client's terminal window focus when it attaches,
129	// so the attachment's first paint already knows whether it may be seen.
130	TerminalFocus domain.TerminalFocus
131	// ClientPID is the local client process, exposed to bar scripts. It is 0
132	// for remote attaches, which run on another machine.
133	ClientPID uint32
134}
135
136func (h Hello) Geometry() domain.Geometry {
137	return domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}.NormalizePixels()
138}
139
140type Input struct {
141	InputSeq uint64
142	ActionID uint64
143	Data     []byte
144}
145
146type Resize struct {
147	Size        domain.Size
148	PixelWidth  int
149	PixelHeight int
150}
151
152func (m Resize) Geometry() domain.Geometry {
153	return domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}.NormalizePixels()
154}
155
156type Theme struct {
157	HasForeground bool
158	Foreground    renderer.RGB
159	HasBackground bool
160	Background    renderer.RGB
161	TrueColor     bool
162	SchemeKnown   bool
163	Light         bool
164	PaletteKnown  uint16
165	Palette       [16]renderer.RGB
166}
167
168type ImagePush struct {
169	InputSeq uint64
170	Mime     string
171	Data     []byte
172}
173
174type ClientNotice struct{ Action uint8 }
175
176const (
177	ClientNoticeClipboardFallback uint8 = 1
178	ClientNoticeClipboardTooLarge uint8 = 2
179	ClientNoticeLinkDegraded      uint8 = 3
180	ClientNoticeLinkConnected     uint8 = 4
181)
182
183// Detach releases the attachment. Closed reports that the client process is
184// ending, so the daemon applies the ephemeral close-on-exit policy.
185type Detach struct {
186	Closed bool
187}
188type Ping struct{}
189type Pong struct{}
190
191type Ack struct {
192	Epoch uint64
193	State uint64
194}
195
196type Welcome struct {
197	SessionID         string
198	SessionName       string
199	Ephemeral         bool
200	ResumeToken       uint64
201	Capabilities      uint32
202	CommittedIdentity *CommittedRouteIdentity
203}
204
205type ErrorMsg struct {
206	Code uint16
207	Text string
208}
209
210type ViewContext struct {
211	Publication   uint64
212	Route         CommittedRouteIdentity
213	TabID         domain.TabStableID
214	FocusedPaneID domain.PaneStableID
215}
216
217func (c ViewContext) Validate() error {
218	if c.Publication == 0 {
219		return errors.New("invalid view publication")
220	}
221	if err := c.Route.Validate(); err != nil {
222		return err
223	}
224	if err := domain.ValidateTabStableID(c.TabID); err != nil {
225		return err
226	}
227	return domain.ValidatePaneStableID(c.FocusedPaneID)
228}
229
230type Output struct {
231	Epoch        uint64
232	Base         uint64
233	New          uint64
234	Echo         uint64
235	ViewRevision uint64
236	Size         domain.Size
237	Full         bool
238	Context      *ViewContext
239	Data         []byte
240}
241
242type UIFence struct{ ActionID uint64 }
243
244type UIReceiptOutcome uint8
245
246const (
247	UIReceiptProcessed UIReceiptOutcome = iota + 1
248	UIReceiptUnavailable
249)
250
251type UIReceipt struct {
252	ActionID        uint64
253	Epoch           uint64
254	State           uint64
255	ViewPublication uint64
256	Outcome         UIReceiptOutcome
257}
258
259// Validate requires a committed publication for success. Unavailable receipts
260// carry no boundary: expiry cannot claim that any captured view was processed.
261func (r UIReceipt) Validate() error {
262	if r.ActionID == 0 {
263		return errors.New("UI receipt requires an action ID")
264	}
265	switch r.Outcome {
266	case UIReceiptProcessed:
267		if r.Epoch != 0 && r.State != 0 && r.ViewPublication != 0 {
268			return nil
269		}
270	case UIReceiptUnavailable:
271		if r.Epoch == 0 && r.State == 0 && r.ViewPublication == 0 {
272			return nil
273		}
274	}
275	return errors.New("invalid UI receipt boundary or outcome")
276}
277
278type UIViewUpdate struct {
279	Epoch   uint64
280	State   uint64
281	Context ViewContext
282}
283
284type AttachTarget struct {
285	CauseActionID     uint64
286	RequestID         uint64
287	Endpoint          string
288	Session           string
289	Intent            uint8
290	SessionTarget     *SessionAttachTarget
291	ExactTarget       *ExactSessionTarget
292	EnvironmentPolicy EnvironmentPolicy
293	SamePeer          bool
294	// PreferredTabID carries an explicit local tab selection across a handoff.
295	// Empty retains the client's route memory; a disappeared tab uses Hello's
296	// existing default-tab fallback.
297	PreferredTabID domain.TabStableID
298}
299
300type Detached struct{ Reason uint8 }
301type List struct{}
302
303type KillScope uint8
304
305const (
306	KillSession KillScope = 0
307	// KillDaemon retains the legacy All=true wire value so an older client can
308	// never turn daemon shutdown into destructive session deletion.
309	KillDaemon KillScope = 1
310	KillAll    KillScope = 2
311)
312
313type Kill struct {
314	RequestID uint64
315	Name      string
316	Scope     KillScope
317}
318
319type KillOutcome uint8
320
321const (
322	KillSucceeded KillOutcome = iota + 1
323	KillFailed
324	KillOutcomeUnknown
325)
326
327type KillFailure struct {
328	Class string
329	Name  string
330	Text  string
331}
332
333type KillResult struct {
334	RequestID uint64
335	Outcome   KillOutcome
336	Code      uint16
337	Text      string
338	Failures  []KillFailure
339}
340
341type SessionState uint8
342
343const (
344	SessionUp SessionState = iota
345	SessionDown
346	SessionBroken
347)
348
349type SessionInfo struct {
350	SessionID string
351	Name      string
352	State     SessionState
353	Ephemeral bool
354	Tabs      uint16
355	Attached  bool
356}
357
358type Sessions struct{ Sessions []SessionInfo }
359type OutputResetRequest struct{}
360
361// SelectTab asks the daemon to show one exact tab of the attached session on
362// this attachment's own view. It is the client picker's in-place tab switch:
363// the attachment and its stream stay live, and a tab that no longer exists is
364// refused without changing the view.
365type SelectTab struct {
366	TabID domain.TabStableID
367}
368
369// Validate requires a well-formed stable tab identity.
370func (m SelectTab) Validate() error {
371	return domain.ValidateTabStableID(m.TabID)
372}
373
374// TerminalFocus reports that the client's terminal window gained or lost
375// focus. A client sends it only after its terminal reports focus, so an
376// attachment that never sends it keeps unknown focus.
377type TerminalFocus struct {
378	Focus domain.TerminalFocus
379}
380
381// Validate accepts only a reported state: unknown is the absence of a report.
382func (m TerminalFocus) Validate() error {
383	if m.Focus != domain.TerminalFocusFocused && m.Focus != domain.TerminalFocusUnfocused {
384		return ErrInvalidTerminalFocus
385	}
386	return nil
387}
388
389func validEnvironmentPolicy(policy EnvironmentPolicy) bool {
390	return policy == EnvironmentPolicyClientOwned || policy == EnvironmentPolicyDaemonOwned
391}
392
393func ValidateEnvironmentPolicy(policy EnvironmentPolicy) error {
394	if !validEnvironmentPolicy(policy) {
395		return ErrInvalidEnvironmentPolicy
396	}
397	return nil
398}
399
400func validClientNoticeAction(action uint8) bool {
401	switch action {
402	case ClientNoticeClipboardFallback, ClientNoticeClipboardTooLarge, ClientNoticeLinkDegraded, ClientNoticeLinkConnected:
403		return true
404	default:
405		return false
406	}
407}
408
409func validDetachedReason(reason uint8) bool {
410	switch reason {
411	case ReasonDetach, ReasonSessionKilled, ReasonServerShutdown, ReasonReplaced, ReasonDetachToPicker:
412		return true
413	default:
414		return false
415	}
416}
417
418func ValidateClientNotice(m ClientNotice) error {
419	if !validClientNoticeAction(m.Action) {
420		return ErrInvalidClientNotice
421	}
422	return nil
423}
424
425func ValidateDetached(m Detached) error {
426	if !validDetachedReason(m.Reason) {
427		return ErrInvalidDetached
428	}
429	return nil
430}
431
432func ValidateHello(h Hello) error {
433	if h.Intent != IntentEphemeral && h.Intent != IntentNew && h.Intent != IntentAttach && h.Intent != IntentResume {
434		return ErrInvalidHello
435	}
436	if err := ValidateGeometry(domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}); err != nil {
437		return fmt.Errorf("%w: geometry", ErrInvalidHello)
438	}
439	if !validEnvironmentPolicy(h.EnvironmentPolicy) || !h.TerminalFocus.Valid() || (h.Remote && h.ClientPID != 0) {
440		return ErrInvalidHello
441	}
442	if err := ValidateNavigation(h.Intent, h.NavigationCapabilities); err != nil {
443		return fmt.Errorf("%w: navigation: %w", ErrInvalidHello, err)
444	}
445	if len(h.Name) > math.MaxUint16 || len(h.TermEnv) > math.MaxUint16 || len(h.Cwd) > math.MaxUint16 || uint64(len(h.Env)) > math.MaxUint32 {
446		return ErrInvalidHello
447	}
448	for _, entry := range h.Env {
449		if uint64(len(entry)) > math.MaxUint32 {
450			return ErrInvalidHello
451		}
452	}
453	if h.PreferredTabID != "" {
454		if h.Intent != IntentAttach && h.Intent != IntentResume {
455			return ErrInvalidHello
456		}
457		if err := domain.ValidateTabStableID(h.PreferredTabID); err != nil {
458			return fmt.Errorf("%w: preferred tab: %v", ErrInvalidHello, err)
459		}
460	}
461	if h.ExactTarget != nil {
462		if h.Intent != IntentAttach && h.Intent != IntentResume {
463			return ErrInvalidHello
464		}
465		if err := h.ExactTarget.Validate(); err != nil {
466			return fmt.Errorf("%w: exact target: %v", ErrInvalidHello, err)
467		}
468		if h.Name != h.ExactTarget.SessionName {
469			return ErrInvalidHello
470		}
471		if h.SessionTarget != nil && (h.SessionTarget.LifecycleID != h.ExactTarget.LifecycleID || h.SessionTarget.SessionName != h.ExactTarget.SessionName) {
472			return ErrInvalidHello
473		}
474	}
475	if h.SessionTarget == nil {
476		return nil
477	}
478	if h.EnvironmentPolicy != EnvironmentPolicyDaemonOwned || (h.Intent != IntentAttach && h.Intent != IntentResume) {
479		return ErrInvalidHello
480	}
481	if err := h.SessionTarget.Validate(); err != nil {
482		return fmt.Errorf("%w: session target: %v", ErrInvalidHello, err)
483	}
484	if h.Name != h.SessionTarget.SessionName {
485		return ErrInvalidHello
486	}
487	return nil
488}
489
490func ValidateOutput(m Output) error {
491	if m.Epoch == 0 {
492		return ErrInvalidOutput
493	}
494	if m.New == 0 {
495		if m.Base != 0 || m.Full || m.Context != nil {
496			return ErrInvalidOutput
497		}
498	} else if m.Context == nil || (m.Base == 0 && !m.Full) || (m.Base != 0 && (m.Full || m.New != m.Base+1)) {
499		return ErrInvalidOutput
500	}
501	if err := ValidateSize(m.Size); err != nil {
502		return fmt.Errorf("%w: size", ErrInvalidOutput)
503	}
504	if m.Context != nil {
505		if err := m.Context.Validate(); err != nil {
506			return fmt.Errorf("%w: context: %w", ErrInvalidOutput, err)
507		}
508	}
509	if uint64(len(m.Data)) > math.MaxUint32 || len(m.Data) > MaxOutputDataLen {
510		return ErrInvalidOutput
511	}
512	return nil
513}
514
515func ValidateAck(m Ack) error {
516	if m.Epoch == 0 {
517		return ErrInvalidAck
518	}
519	return nil
520}
521
522func ValidateAttachTarget(m AttachTarget) error {
523	if m.Session == "" || len(m.Endpoint) > math.MaxUint16 || len(m.Session) > math.MaxUint16 {
524		return ErrInvalidAttachTarget
525	}
526	if err := domain.ValidateSessionName(m.Session); err != nil {
527		return ErrInvalidAttachTarget
528	}
529	if m.Intent != IntentEphemeral && m.Intent != IntentNew && m.Intent != IntentAttach && m.Intent != IntentResume {
530		return ErrInvalidAttachTarget
531	}
532	if m.Intent == IntentResume || !validEnvironmentPolicy(m.EnvironmentPolicy) {
533		return ErrInvalidAttachTarget
534	}
535	if m.Intent == IntentNew && m.Endpoint != "" && m.RequestID == 0 || m.RequestID != 0 && m.Intent != IntentNew {
536		return ErrInvalidAttachTarget
537	}
538	if m.ExactTarget != nil && (m.ExactTarget.SessionName != m.Session || m.ExactTarget.Validate() != nil) {
539		return ErrInvalidAttachTarget
540	}
541	if m.SamePeer && (m.Endpoint != "" || m.SessionTarget != nil || m.ExactTarget == nil) {
542		return ErrInvalidAttachTarget
543	}
544	if m.PreferredTabID != "" && (m.Endpoint != "" || m.SessionTarget != nil || m.ExactTarget == nil || m.Intent != IntentAttach || domain.ValidateTabStableID(m.PreferredTabID) != nil) {
545		return ErrInvalidAttachTarget
546	}
547	if m.SessionTarget == nil {
548		return nil
549	}
550	if m.Endpoint != "" || m.EnvironmentPolicy != EnvironmentPolicyDaemonOwned || m.Intent != IntentAttach {
551		return ErrInvalidAttachTarget
552	}
553	if err := m.SessionTarget.Validate(); err != nil {
554		return fmt.Errorf("%w: session target: %v", ErrInvalidAttachTarget, err)
555	}
556	if m.Session != m.SessionTarget.SessionName {
557		return ErrInvalidAttachTarget
558	}
559	return nil
560}

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