v0.3.0 0a8834d

wire.go RawHistoryFolder

1// Package visualproc launches the separate visual process and speaks the tiny
2// private daemon<->visual pipe protocol. It never renders and never owns or
3// releases the session lock; the visual does, and reports the outcome.
4//
5// Wire format, both directions (fd 3: daemon->visual, fd 4: visual->daemon):
6//
7//	"NAV1" | kind u8 | length u32 BE | payload
8//
9// Commands: 1 Fade {black u8, duration i64 ns BE}; 2 Lock {generation u64,
10// then three u32-length-prefixed byte strings: PINSource, PINReference,
11// EnvPIN}. Events: 3 LockConfirmed {generation u64}; 4 LockReleased
12// {generation u64}. Process exit is observed by the launcher, not sent.
13package visualproc
14
15import (
16	"encoding/binary"
17	"errors"
18	"io"
19	"time"
20
21	"github.com/bnema/neferafk/internal/ports"
22)
23
24var ErrProtocol = errors.New("visualproc: invalid frame")
25
26const (
27	kindFade byte = iota + 1
28	kindLock
29	kindConfirmed
30	kindReleased
31	maxPayload = 8 + 3*4 + 2*ports.AuthMaxMetadata + ports.AuthMaxSecret
32)
33
34func writeFrame(w io.Writer, kind byte, payload []byte) error {
35	buf := make([]byte, 9+len(payload))
36	defer clear(buf)
37	copy(buf, "NAV1")
38	buf[4] = kind
39	binary.BigEndian.PutUint32(buf[5:], uint32(len(payload)))
40	copy(buf[9:], payload)
41	for b := buf; len(b) > 0; {
42		n, err := w.Write(b)
43		if err != nil {
44			return err
45		}
46		b = b[n:]
47	}
48	return nil
49}
50
51func readFrame(r io.Reader) (byte, []byte, error) {
52	var h [9]byte
53	if _, err := io.ReadFull(r, h[:]); err != nil {
54		return 0, nil, err
55	}
56	n := binary.BigEndian.Uint32(h[5:])
57	if string(h[:4]) != "NAV1" || n > maxPayload {
58		return 0, nil, ErrProtocol
59	}
60	p := make([]byte, n)
61	if _, err := io.ReadFull(r, p); err != nil {
62		clear(p)
63		return 0, nil, err
64	}
65	return h[4], p, nil
66}
67
68// WriteCommand encodes a daemon->visual command. The caller wipes any secret
69// it owns; the encoded copy is wiped here.
70func WriteCommand(w io.Writer, cmd ports.VisualCommand) error {
71	switch c := cmd.(type) {
72	case ports.VisualFade:
73		p := make([]byte, 9)
74		if c.Black {
75			p[0] = 1
76		}
77		binary.BigEndian.PutUint64(p[1:], uint64(c.Duration))
78		return writeFrame(w, kindFade, p)
79	case ports.VisualLock:
80		a := c.Auth
81		fields := [][]byte{[]byte(a.PINSource), []byte(a.PINReference), a.EnvPIN}
82		if c.Generation == 0 || uint64(c.Generation) != a.Generation || len(fields[1]) > ports.AuthMaxMetadata || len(fields[2]) > ports.AuthMaxSecret || len(fields[0]) > 16 {
83			return ErrProtocol
84		}
85		p := binary.BigEndian.AppendUint64(nil, uint64(c.Generation))
86		for _, f := range fields {
87			p = binary.BigEndian.AppendUint32(p, uint32(len(f)))
88			p = append(p, f...)
89		}
90		defer clear(p)
91		return writeFrame(w, kindLock, p)
92	}
93	return ErrProtocol
94}
95
96// ReadCommand decodes a daemon->visual command (used by the visual process).
97// The returned VisualLock owns EnvPIN and its receiver must wipe it.
98func ReadCommand(r io.Reader) (ports.VisualCommand, error) {
99	kind, p, err := readFrame(r)
100	if err != nil {
101		return nil, err
102	}
103	defer clear(p)
104	switch kind {
105	case kindFade:
106		if len(p) != 9 || p[0] > 1 {
107			return nil, ErrProtocol
108		}
109		d := time.Duration(binary.BigEndian.Uint64(p[1:]))
110		if d < 0 || d > ports.MaxDuration {
111			return nil, ErrProtocol
112		}
113		return ports.VisualFade{Black: p[0] == 1, Duration: d}, nil
114	case kindLock:
115		if len(p) < 8 {
116			return nil, ErrProtocol
117		}
118		gen := binary.BigEndian.Uint64(p)
119		rest := p[8:]
120		var f [3][]byte
121		for i := range f {
122			if len(rest) < 4 {
123				return nil, ErrProtocol
124			}
125			n := binary.BigEndian.Uint32(rest)
126			if uint64(n) > uint64(len(rest)-4) {
127				return nil, ErrProtocol
128			}
129			f[i] = rest[4 : 4+n]
130			rest = rest[4+n:]
131		}
132		if gen == 0 || len(rest) != 0 || len(f[0]) > 16 || len(f[1]) > ports.AuthMaxMetadata || len(f[2]) > ports.AuthMaxSecret {
133			return nil, ErrProtocol
134		}
135		return ports.VisualLock{Generation: ports.Generation(gen), Auth: ports.AuthBootstrap{Generation: gen, PINSource: ports.PINSource(f[0]), PINReference: string(f[1]), EnvPIN: append([]byte(nil), f[2]...)}}, nil
136	}
137	return nil, ErrProtocol
138}
139
140// WriteEvent encodes a visual->daemon lifecycle event (used by the visual).
141func WriteEvent(w io.Writer, ev ports.VisualEvent) error {
142	switch e := ev.(type) {
143	case ports.LockConfirmed:
144		return writeFrame(w, kindConfirmed, binary.BigEndian.AppendUint64(nil, uint64(e.Generation)))
145	case ports.LockReleased:
146		return writeFrame(w, kindReleased, binary.BigEndian.AppendUint64(nil, uint64(e.Generation)))
147	}
148	return ErrProtocol
149}
150
151// ReadEvent decodes a visual->daemon event.
152func ReadEvent(r io.Reader) (ports.VisualEvent, error) {
153	kind, p, err := readFrame(r)
154	if err != nil {
155		return nil, err
156	}
157	if len(p) != 8 || binary.BigEndian.Uint64(p) == 0 {
158		return nil, ErrProtocol
159	}
160	gen := ports.Generation(binary.BigEndian.Uint64(p))
161	switch kind {
162	case kindConfirmed:
163		return ports.LockConfirmed{Generation: gen}, nil
164	case kindReleased:
165		return ports.LockReleased{Generation: gen}, nil
166	}
167	return nil, ErrProtocol
168}

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 2

Tags 9

↑↓ moveenter openesc close