v0.1.0 7deb448

wire.go RawHistoryFolder

1// Wire codecs for vev's IPC message payloads. They live in ports (alongside
2// Frame and the MsgType constants) because wire encoding is protocol
3// surface, not I/O: everything here is pure, stdlib-only byte marshalling.
4// Adapters keep what actually performs I/O (framing over a connection,
5// listening on a socket) and import these types for the payloads they carry.
6
7package ports
8
9import (
10	"encoding/binary"
11	"errors"
12
13	"github.com/bnema/vev/internal/domain"
14	"github.com/bnema/vev/pkg/renderer"
15)
16
17// errShortPayload is returned when a payload ends before a required field
18// has been fully read.
19var errShortPayload = errors.New("ports: payload too short")
20
21// errTrailingBytes is returned when a fixed-shape payload has bytes left
22// over after every field has been consumed. Protocol strictness here
23// catches version drift early instead of silently ignoring extra data.
24var errTrailingBytes = errors.New("ports: unexpected trailing bytes in payload")
25
26// Intent values carried by Hello, describing what the client wants to do.
27const (
28	IntentEphemeral uint8 = 0
29	IntentNew       uint8 = 1
30	IntentAttach    uint8 = 2
31	IntentResume    uint8 = 3
32)
33
34// Capability bits advertised in Welcome.
35const (
36	CapabilityResume  uint32 = 1 << 0
37	CapabilityUDP     uint32 = 1 << 1
38	CapabilityPredict uint32 = 1 << 2
39)
40
41// ErrorMsg codes.
42const (
43	ErrVersionMismatch    uint16 = 1
44	ErrNoSuchSession      uint16 = 2
45	ErrNameTaken          uint16 = 3
46	ErrServerShutdown     uint16 = 4
47	ErrInvalidSessionName uint16 = 5
48	ErrUnknownCommand     uint16 = 6
49	ErrNotScriptable      uint16 = 7
50	ErrInvalidCommandArgs uint16 = 8
51	ErrNoSuchTarget       uint16 = 9
52	ErrAmbiguousTarget    uint16 = 10
53	ErrInternal           uint16 = 255
54)
55
56// Detached reasons.
57const (
58	ReasonDetach         uint8 = 0
59	ReasonSessionKilled  uint8 = 1
60	ReasonServerShutdown uint8 = 2
61	ReasonReplaced       uint8 = 3
62)
63
64// Hello is sent by the client immediately after connecting.
65type Hello struct {
66	Version     uint16
67	Intent      uint8
68	ClientID    [16]byte
69	ResumeToken uint64
70	Name        string
71	Size        domain.Size
72	TermEnv     string
73	Cwd         string
74	TrueColor   bool
75	// MaxOutputInFlight is the requested maximum number of unacknowledged
76	// state-bearing output frames.
77	MaxOutputInFlight uint8
78	// Env is the complete exported client environment for future PTY children.
79	// Entries are opaque strings so their ordering and contents are preserved.
80	Env []string
81}
82
83// Input carries raw bytes typed/pasted by the client, destined for the PTY.
84type Input struct {
85	InputSeq uint64
86	Data     []byte
87}
88
89// Resize notifies the daemon of a client-side terminal size change.
90type Resize struct {
91	Size domain.Size
92}
93
94// Theme reports the client's terminal foreground/background colors, ANSI
95// palette entries, and whether the client terminal supports truecolor.
96type Theme struct {
97	HasForeground bool
98	Foreground    renderer.RGB
99	HasBackground bool
100	Background    renderer.RGB
101	TrueColor     bool
102	SchemeKnown   bool
103	Light         bool
104	PaletteKnown  uint16
105	Palette       [16]renderer.RGB
106}
107
108// ImagePush carries a clipboard image from a remote client, to be written to
109// a temp file and injected into the focused pane's PTY as a path (see
110// docs/superpowers/specs/2026-07-04-clipboard-image-transfer-design.md).
111type ImagePush struct {
112	InputSeq uint64
113	Mime     string
114	Data     []byte
115}
116
117// ClientNotice reports a fixed client-side event for daemon-rendered user
118// feedback. Action values are deliberately closed so a client cannot inject
119// arbitrary display text into a shared session.
120type ClientNotice struct {
121	Action uint8
122}
123
124const (
125	ClientNoticeClipboardFallback uint8 = 1
126	ClientNoticeClipboardTooLarge uint8 = 2
127	ClientNoticeLinkDegraded      uint8 = 3
128	ClientNoticeLinkConnected     uint8 = 4
129)
130
131func validClientNoticeAction(action uint8) bool {
132	switch action {
133	case ClientNoticeClipboardFallback, ClientNoticeClipboardTooLarge, ClientNoticeLinkDegraded, ClientNoticeLinkConnected:
134		return true
135	default:
136		return false
137	}
138}
139
140// Detach asks the daemon to detach the current client without killing the
141// session.
142type Detach struct{}
143
144// Ping is a keepalive/liveness probe sent by either side.
145type Ping struct{}
146
147// Pong answers a Ping.
148type Pong struct{}
149
150// Ack acknowledges receipt/application of an output state number.
151type Ack struct {
152	AckedStateNum uint64
153}
154
155// Welcome is the daemon's reply to a successful Hello.
156type Welcome struct {
157	SessionID    string
158	SessionName  string
159	Ephemeral    bool
160	ResumeToken  uint64
161	Capabilities uint32
162}
163
164// ErrorMsg reports a protocol- or session-level failure to the client.
165type ErrorMsg struct {
166	Code uint16
167	Text string
168}
169
170// Output carries raw PTY output, destined for the client's terminal.
171type Output struct {
172	BaseStateNum uint64
173	NewStateNum  uint64
174	EchoAck      uint64
175	Data         []byte
176}
177
178// Detached tells a client it has been disconnected from its session and why.
179type Detached struct {
180	Reason uint8
181}
182
183// List asks the daemon to enumerate its live sessions. It carries no
184// fields; the request is fully described by its message type.
185type List struct{}
186
187// Kill asks the daemon to terminate one named session or all sessions.
188type Kill struct {
189	Name string
190	All  bool
191}
192
193// SessionState is the client-visible lifecycle state of a session.
194type SessionState uint8
195
196const (
197	SessionRunning SessionState = iota
198	SessionStopped
199	SessionBroken
200)
201
202// SessionInfo describes one session in a Sessions listing.
203type SessionInfo struct {
204	SessionID string
205	Name      string
206	State     SessionState
207	Ephemeral bool
208	Tabs      uint16
209	Attached  bool
210}
211
212// Sessions is the daemon's reply to a List, enumerating live sessions.
213type Sessions struct {
214	Sessions []SessionInfo
215}
216
217// payloadWriter builds a message payload by appending fields in wire order.
218type payloadWriter struct {
219	b []byte
220}
221
222func (w *payloadWriter) putUint8(v uint8) {
223	w.b = append(w.b, v)
224}
225
226func (w *payloadWriter) putUint16(v uint16) {
227	var tmp [2]byte
228	binary.BigEndian.PutUint16(tmp[:], v)
229	w.b = append(w.b, tmp[:]...)
230}
231
232func (w *payloadWriter) putUint32(v uint32) {
233	var tmp [4]byte
234	binary.BigEndian.PutUint32(tmp[:], v)
235	w.b = append(w.b, tmp[:]...)
236}
237
238func (w *payloadWriter) putUint64(v uint64) {
239	var tmp [8]byte
240	binary.BigEndian.PutUint64(tmp[:], v)
241	w.b = append(w.b, tmp[:]...)
242}
243
244func (w *payloadWriter) putBytes(b []byte) {
245	w.b = append(w.b, b...)
246}
247
248func (w *payloadWriter) putString(s string) {
249	w.putUint16(uint16(len(s)))
250	w.b = append(w.b, s...)
251}
252
253func (w *payloadWriter) putLongString(s string) {
254	w.putUint32(uint32(len(s)))
255	w.b = append(w.b, s...)
256}
257
258// payloadReader consumes a message payload field by field in wire order,
259// erroring instead of panicking on any short read.
260type payloadReader struct {
261	b []byte
262}
263
264func (r *payloadReader) getUint8() (uint8, error) {
265	if len(r.b) < 1 {
266		return 0, errShortPayload
267	}
268	v := r.b[0]
269	r.b = r.b[1:]
270	return v, nil
271}
272
273func (r *payloadReader) getUint16() (uint16, error) {
274	if len(r.b) < 2 {
275		return 0, errShortPayload
276	}
277	v := binary.BigEndian.Uint16(r.b)
278	r.b = r.b[2:]
279	return v, nil
280}
281
282func (r *payloadReader) getUint32() (uint32, error) {
283	if len(r.b) < 4 {
284		return 0, errShortPayload
285	}
286	v := binary.BigEndian.Uint32(r.b)
287	r.b = r.b[4:]
288	return v, nil
289}
290
291func (r *payloadReader) getUint64() (uint64, error) {
292	if len(r.b) < 8 {
293		return 0, errShortPayload
294	}
295	v := binary.BigEndian.Uint64(r.b)
296	r.b = r.b[8:]
297	return v, nil
298}
299
300func (r *payloadReader) getBytes(n int) ([]byte, error) {
301	if len(r.b) < n {
302		return nil, errShortPayload
303	}
304	b := append([]byte(nil), r.b[:n]...)
305	r.b = r.b[n:]
306	return b, nil
307}
308
309func (r *payloadReader) getString() (string, error) {
310	n, err := r.getUint16()
311	if err != nil {
312		return "", err
313	}
314	if len(r.b) < int(n) {
315		return "", errShortPayload
316	}
317	s := string(r.b[:n])
318	r.b = r.b[n:]
319	return s, nil
320}
321
322func (r *payloadReader) getLongString() (string, error) {
323	n, err := r.getUint32()
324	if err != nil {
325		return "", err
326	}
327	if uint64(n) > uint64(len(r.b)) {
328		return "", errShortPayload
329	}
330	s := string(r.b[:int(n)])
331	r.b = r.b[int(n):]
332	return s, nil
333}
334
335// rest consumes and returns all remaining bytes, copied so the result is
336// independent of the reader's backing array.
337func (r *payloadReader) rest() []byte {
338	b := append([]byte(nil), r.b...)
339	r.b = nil
340	return b
341}
342
343// done reports an error if any bytes remain unconsumed.
344func (r *payloadReader) done() error {
345	if len(r.b) != 0 {
346		return errTrailingBytes
347	}
348	return nil
349}
350
351// PeekHelloVersion returns the leading protocol version from a Hello payload.
352func PeekHelloVersion(b []byte) (uint16, bool) {
353	if len(b) < 2 {
354		return 0, false
355	}
356	return binary.BigEndian.Uint16(b), true
357}
358
359// MarshalHello encodes h into a Hello message payload.
360func MarshalHello(h Hello) []byte {
361	w := payloadWriter{}
362	w.putUint16(h.Version)
363	w.putUint8(h.Intent)
364	w.putBytes(h.ClientID[:])
365	w.putUint64(h.ResumeToken)
366	w.putString(h.Name)
367	w.putUint16(uint16(h.Size.Cols))
368	w.putUint16(uint16(h.Size.Rows))
369	w.putString(h.TermEnv)
370	w.putString(h.Cwd)
371	if h.TrueColor {
372		w.putUint8(1)
373	} else {
374		w.putUint8(0)
375	}
376	w.putUint8(h.MaxOutputInFlight)
377	w.putUint32(uint32(len(h.Env)))
378	for _, entry := range h.Env {
379		w.putLongString(entry)
380	}
381	return w.b
382}
383
384// UnmarshalHello decodes a Hello message payload.
385func UnmarshalHello(b []byte) (Hello, error) {
386	r := payloadReader{b: b}
387	var h Hello
388	var err error
389
390	if h.Version, err = r.getUint16(); err != nil {
391		return Hello{}, err
392	}
393	if h.Intent, err = r.getUint8(); err != nil {
394		return Hello{}, err
395	}
396	clientID, err := r.getBytes(len(h.ClientID))
397	if err != nil {
398		return Hello{}, err
399	}
400	copy(h.ClientID[:], clientID)
401	if h.ResumeToken, err = r.getUint64(); err != nil {
402		return Hello{}, err
403	}
404	if h.Name, err = r.getString(); err != nil {
405		return Hello{}, err
406	}
407	cols, err := r.getUint16()
408	if err != nil {
409		return Hello{}, err
410	}
411	rows, err := r.getUint16()
412	if err != nil {
413		return Hello{}, err
414	}
415	h.Size = domain.Size{Cols: int(cols), Rows: int(rows)}
416	if h.TermEnv, err = r.getString(); err != nil {
417		return Hello{}, err
418	}
419	if h.Cwd, err = r.getString(); err != nil {
420		return Hello{}, err
421	}
422	trueColor, err := r.getUint8()
423	if err != nil {
424		return Hello{}, err
425	}
426	h.TrueColor = trueColor != 0
427	if h.MaxOutputInFlight, err = r.getUint8(); err != nil {
428		return Hello{}, err
429	}
430	envCount, err := r.getUint32()
431	if err != nil {
432		return Hello{}, err
433	}
434	// Each entry has at least its uint32 byte length. Check that before
435	// allocating so a malformed count cannot force an excessive allocation.
436	if uint64(envCount) > uint64(len(r.b)/4) {
437		return Hello{}, errShortPayload
438	}
439	if envCount != 0 {
440		h.Env = make([]string, 0, int(envCount))
441		for range int(envCount) {
442			entry, err := r.getLongString()
443			if err != nil {
444				return Hello{}, err
445			}
446			h.Env = append(h.Env, entry)
447		}
448	}
449	if err := r.done(); err != nil {
450		return Hello{}, err
451	}
452	return h, nil
453}
454
455// MarshalInput encodes m into an Input message payload.
456func MarshalInput(m Input) []byte {
457	w := payloadWriter{}
458	w.putUint64(m.InputSeq)
459	w.putBytes(m.Data)
460	return w.b
461}
462
463// UnmarshalInput decodes an Input message payload. After the fixed input
464// sequence, the rest of the payload is data; there is no length prefix.
465func UnmarshalInput(b []byte) (Input, error) {
466	r := payloadReader{b: b}
467	seq, err := r.getUint64()
468	if err != nil {
469		return Input{}, err
470	}
471	return Input{InputSeq: seq, Data: r.rest()}, nil
472}
473
474// MarshalImagePush encodes m into an ImagePush message payload.
475func MarshalImagePush(m ImagePush) []byte {
476	w := payloadWriter{}
477	w.putUint64(m.InputSeq)
478	w.putString(m.Mime)
479	w.putBytes(m.Data)
480	return w.b
481}
482
483// UnmarshalImagePush decodes an ImagePush message payload. After the fixed
484// input sequence and length-prefixed mime string, the rest of the payload is
485// data; there is no length prefix for it.
486func UnmarshalImagePush(b []byte) (ImagePush, error) {
487	r := payloadReader{b: b}
488	seq, err := r.getUint64()
489	if err != nil {
490		return ImagePush{}, err
491	}
492	mime, err := r.getString()
493	if err != nil {
494		return ImagePush{}, err
495	}
496	return ImagePush{InputSeq: seq, Mime: mime, Data: r.rest()}, nil
497}
498
499// MarshalClientNotice encodes a fixed one-byte client notice action.
500func MarshalClientNotice(m ClientNotice) []byte {
501	return []byte{m.Action}
502}
503
504// UnmarshalClientNotice decodes a fixed client notice action and rejects both
505// unknown values and any trailing bytes.
506func UnmarshalClientNotice(b []byte) (ClientNotice, error) {
507	r := payloadReader{b: b}
508	action, err := r.getUint8()
509	if err != nil {
510		return ClientNotice{}, err
511	}
512	if !validClientNoticeAction(action) {
513		return ClientNotice{}, errors.New("ports: unknown client notice action")
514	}
515	if err := r.done(); err != nil {
516		return ClientNotice{}, err
517	}
518	return ClientNotice{Action: action}, nil
519}
520
521// MarshalResize encodes m into a Resize message payload.
522func MarshalResize(m Resize) []byte {
523	w := payloadWriter{}
524	w.putUint16(uint16(m.Size.Cols))
525	w.putUint16(uint16(m.Size.Rows))
526	return w.b
527}
528
529// MarshalTheme encodes m into a 57-byte fixed-width Theme message payload.
530func MarshalTheme(m Theme) []byte {
531	var flags uint8
532	if m.HasForeground {
533		flags |= 0x01
534	}
535	if m.HasBackground {
536		flags |= 0x02
537	}
538	if m.TrueColor {
539		flags |= 0x04
540	}
541	if m.SchemeKnown {
542		flags |= 0x08
543	}
544	if m.Light {
545		flags |= 0x10
546	}
547
548	w := payloadWriter{b: make([]byte, 0, 57)}
549	w.putUint8(flags)
550	w.putUint8(m.Foreground.R)
551	w.putUint8(m.Foreground.G)
552	w.putUint8(m.Foreground.B)
553	w.putUint8(m.Background.R)
554	w.putUint8(m.Background.G)
555	w.putUint8(m.Background.B)
556	w.putUint16(m.PaletteKnown)
557	for _, color := range m.Palette {
558		w.putUint8(color.R)
559		w.putUint8(color.G)
560		w.putUint8(color.B)
561	}
562	return w.b
563}
564
565// UnmarshalTheme decodes a 57-byte fixed-width Theme message payload.
566func UnmarshalTheme(b []byte) (Theme, error) {
567	r := payloadReader{b: b}
568	flags, err := r.getUint8()
569	if err != nil {
570		return Theme{}, err
571	}
572	fgR, err := r.getUint8()
573	if err != nil {
574		return Theme{}, err
575	}
576	fgG, err := r.getUint8()
577	if err != nil {
578		return Theme{}, err
579	}
580	fgB, err := r.getUint8()
581	if err != nil {
582		return Theme{}, err
583	}
584	bgR, err := r.getUint8()
585	if err != nil {
586		return Theme{}, err
587	}
588	bgG, err := r.getUint8()
589	if err != nil {
590		return Theme{}, err
591	}
592	bgB, err := r.getUint8()
593	if err != nil {
594		return Theme{}, err
595	}
596	paletteKnown, err := r.getUint16()
597	if err != nil {
598		return Theme{}, err
599	}
600
601	m := Theme{
602		HasForeground: flags&0x01 != 0,
603		Foreground:    renderer.RGB{R: fgR, G: fgG, B: fgB},
604		HasBackground: flags&0x02 != 0,
605		Background:    renderer.RGB{R: bgR, G: bgG, B: bgB},
606		TrueColor:     flags&0x04 != 0,
607		SchemeKnown:   flags&0x08 != 0,
608		Light:         flags&0x10 != 0,
609		PaletteKnown:  paletteKnown,
610	}
611	for i := range m.Palette {
612		if m.Palette[i].R, err = r.getUint8(); err != nil {
613			return Theme{}, err
614		}
615		if m.Palette[i].G, err = r.getUint8(); err != nil {
616			return Theme{}, err
617		}
618		if m.Palette[i].B, err = r.getUint8(); err != nil {
619			return Theme{}, err
620		}
621	}
622	if err := r.done(); err != nil {
623		return Theme{}, err
624	}
625	return m, nil
626}
627
628// UnmarshalResize decodes a Resize message payload.
629func UnmarshalResize(b []byte) (Resize, error) {
630	r := payloadReader{b: b}
631	cols, err := r.getUint16()
632	if err != nil {
633		return Resize{}, err
634	}
635	rows, err := r.getUint16()
636	if err != nil {
637		return Resize{}, err
638	}
639	if err := r.done(); err != nil {
640		return Resize{}, err
641	}
642	return Resize{Size: domain.Size{Cols: int(cols), Rows: int(rows)}}, nil
643}
644
645// MarshalDetach encodes a Detach message payload (always empty).
646func MarshalDetach(Detach) []byte {
647	return nil
648}
649
650// UnmarshalDetach decodes a Detach message payload.
651func UnmarshalDetach(b []byte) (Detach, error) {
652	r := payloadReader{b: b}
653	if err := r.done(); err != nil {
654		return Detach{}, err
655	}
656	return Detach{}, nil
657}
658
659// MarshalPing encodes a Ping message payload (always empty).
660func MarshalPing(Ping) []byte {
661	return nil
662}
663
664// UnmarshalPing decodes a Ping message payload.
665func UnmarshalPing(b []byte) (Ping, error) {
666	r := payloadReader{b: b}
667	if err := r.done(); err != nil {
668		return Ping{}, err
669	}
670	return Ping{}, nil
671}
672
673// MarshalPong encodes a Pong message payload (always empty).
674func MarshalPong(Pong) []byte {
675	return nil
676}
677
678// UnmarshalPong decodes a Pong message payload.
679func UnmarshalPong(b []byte) (Pong, error) {
680	r := payloadReader{b: b}
681	if err := r.done(); err != nil {
682		return Pong{}, err
683	}
684	return Pong{}, nil
685}
686
687// MarshalAck encodes m into an Ack message payload.
688func MarshalAck(m Ack) []byte {
689	w := payloadWriter{}
690	w.putUint64(m.AckedStateNum)
691	return w.b
692}
693
694// UnmarshalAck decodes an Ack message payload.
695func UnmarshalAck(b []byte) (Ack, error) {
696	r := payloadReader{b: b}
697	acked, err := r.getUint64()
698	if err != nil {
699		return Ack{}, err
700	}
701	if err := r.done(); err != nil {
702		return Ack{}, err
703	}
704	return Ack{AckedStateNum: acked}, nil
705}
706
707// MarshalWelcome encodes m into a Welcome message payload.
708func MarshalWelcome(m Welcome) []byte {
709	w := payloadWriter{}
710	w.putString(m.SessionID)
711	w.putString(m.SessionName)
712	if m.Ephemeral {
713		w.putUint8(1)
714	} else {
715		w.putUint8(0)
716	}
717	w.putUint64(m.ResumeToken)
718	w.putUint32(m.Capabilities)
719	return w.b
720}
721
722// UnmarshalWelcome decodes a Welcome message payload.
723func UnmarshalWelcome(b []byte) (Welcome, error) {
724	r := payloadReader{b: b}
725	var m Welcome
726	var err error
727
728	if m.SessionID, err = r.getString(); err != nil {
729		return Welcome{}, err
730	}
731	if m.SessionName, err = r.getString(); err != nil {
732		return Welcome{}, err
733	}
734	eph, err := r.getUint8()
735	if err != nil {
736		return Welcome{}, err
737	}
738	m.Ephemeral = eph != 0
739	if m.ResumeToken, err = r.getUint64(); err != nil {
740		return Welcome{}, err
741	}
742	if m.Capabilities, err = r.getUint32(); err != nil {
743		return Welcome{}, err
744	}
745	if err := r.done(); err != nil {
746		return Welcome{}, err
747	}
748	return m, nil
749}
750
751// MarshalErrorMsg encodes m into an ErrorMsg message payload.
752func MarshalErrorMsg(m ErrorMsg) []byte {
753	w := payloadWriter{}
754	w.putUint16(m.Code)
755	w.putString(m.Text)
756	return w.b
757}
758
759// UnmarshalErrorMsg decodes an ErrorMsg message payload.
760func UnmarshalErrorMsg(b []byte) (ErrorMsg, error) {
761	r := payloadReader{b: b}
762	var m ErrorMsg
763	var err error
764
765	if m.Code, err = r.getUint16(); err != nil {
766		return ErrorMsg{}, err
767	}
768	if m.Text, err = r.getString(); err != nil {
769		return ErrorMsg{}, err
770	}
771	if err := r.done(); err != nil {
772		return ErrorMsg{}, err
773	}
774	return m, nil
775}
776
777// MarshalOutput encodes m into an Output message payload.
778func MarshalOutput(m Output) []byte {
779	w := payloadWriter{}
780	w.putUint64(m.BaseStateNum)
781	w.putUint64(m.NewStateNum)
782	w.putUint64(m.EchoAck)
783	w.putBytes(m.Data)
784	return w.b
785}
786
787// UnmarshalOutput decodes an Output message payload. After fixed state
788// numbers, the rest of the payload is data; there is no length prefix.
789func UnmarshalOutput(b []byte) (Output, error) {
790	r := payloadReader{b: b}
791	base, err := r.getUint64()
792	if err != nil {
793		return Output{}, err
794	}
795	newState, err := r.getUint64()
796	if err != nil {
797		return Output{}, err
798	}
799	echoAck, err := r.getUint64()
800	if err != nil {
801		return Output{}, err
802	}
803	return Output{BaseStateNum: base, NewStateNum: newState, EchoAck: echoAck, Data: r.rest()}, nil
804}
805
806// MarshalDetached encodes m into a Detached message payload.
807func MarshalDetached(m Detached) []byte {
808	return []byte{m.Reason}
809}
810
811// UnmarshalDetached decodes a Detached message payload.
812func UnmarshalDetached(b []byte) (Detached, error) {
813	r := payloadReader{b: b}
814	reason, err := r.getUint8()
815	if err != nil {
816		return Detached{}, err
817	}
818	if err := r.done(); err != nil {
819		return Detached{}, err
820	}
821	return Detached{Reason: reason}, nil
822}
823
824// MarshalList encodes a List message payload (always empty).
825func MarshalList(List) []byte {
826	return nil
827}
828
829// UnmarshalList decodes a List message payload.
830func UnmarshalList(b []byte) (List, error) {
831	r := payloadReader{b: b}
832	if err := r.done(); err != nil {
833		return List{}, err
834	}
835	return List{}, nil
836}
837
838// MarshalKill encodes m into a Kill message payload.
839func MarshalKill(m Kill) []byte {
840	w := payloadWriter{}
841	w.putString(m.Name)
842	if m.All {
843		w.putUint8(1)
844	}
845	return w.b
846}
847
848// UnmarshalKill decodes a Kill message payload.
849func UnmarshalKill(b []byte) (Kill, error) {
850	r := payloadReader{b: b}
851	name, err := r.getString()
852	if err != nil {
853		return Kill{}, err
854	}
855	var all uint8
856	if len(r.b) > 0 {
857		all, err = r.getUint8()
858		if err != nil {
859			return Kill{}, err
860		}
861	}
862	if err := r.done(); err != nil {
863		return Kill{}, err
864	}
865	return Kill{Name: name, All: all != 0}, nil
866}
867
868// MarshalSessions encodes m into a Sessions message payload: a uint16 count
869// followed by that many session records.
870func MarshalSessions(m Sessions) []byte {
871	w := payloadWriter{}
872	w.putUint16(uint16(len(m.Sessions)))
873	for _, s := range m.Sessions {
874		w.putString(s.SessionID)
875		w.putString(s.Name)
876		if s.Ephemeral {
877			w.putUint8(1)
878		} else {
879			w.putUint8(0)
880		}
881		w.putUint16(s.Tabs)
882		if s.Attached {
883			w.putUint8(1)
884		} else {
885			w.putUint8(0)
886		}
887		w.putUint8(uint8(s.State))
888	}
889	return w.b
890}
891
892// UnmarshalSessions decodes a Sessions message payload.
893func UnmarshalSessions(b []byte) (Sessions, error) {
894	r := payloadReader{b: b}
895	count, err := r.getUint16()
896	if err != nil {
897		return Sessions{}, err
898	}
899	sessions := make([]SessionInfo, 0, count)
900	for range int(count) {
901		var s SessionInfo
902		if s.SessionID, err = r.getString(); err != nil {
903			return Sessions{}, err
904		}
905		if s.Name, err = r.getString(); err != nil {
906			return Sessions{}, err
907		}
908		eph, err := r.getUint8()
909		if err != nil {
910			return Sessions{}, err
911		}
912		s.Ephemeral = eph != 0
913		if s.Tabs, err = r.getUint16(); err != nil {
914			return Sessions{}, err
915		}
916		att, err := r.getUint8()
917		if err != nil {
918			return Sessions{}, err
919		}
920		s.Attached = att != 0
921		state, err := r.getUint8()
922		if err != nil {
923			return Sessions{}, err
924		}
925		s.State = SessionState(state)
926		if s.State > SessionBroken {
927			return Sessions{}, errors.New("ports: invalid session state")
928		}
929		sessions = append(sessions, s)
930	}
931	if err := r.done(); err != nil {
932		return Sessions{}, err
933	}
934	return Sessions{Sessions: sessions}, nil
935}

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