v0.5.0 131f96f

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

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