v0.2.0 80c61e3

routes.go RawHistoryFolder

1package ports
2
3import (
4	"errors"
5	"fmt"
6	"unicode"
7	"unicode/utf8"
8
9	"github.com/bnema/vev/internal/domain"
10)
11
12// RouteOrigin identifies how a client reached a daemon. The origin is client
13// composition metadata, not an authority granted to the daemon.
14type RouteOrigin uint8
15
16const (
17	RouteOriginLocal RouteOrigin = iota + 1
18	RouteOriginRemote
19	RouteOriginDiscovery
20)
21
22func (o RouteOrigin) valid() bool {
23	switch o {
24	case RouteOriginLocal, RouteOriginRemote, RouteOriginDiscovery:
25		return true
26	default:
27		return false
28	}
29}
30
31func (o RouteOrigin) Validate() error {
32	if !o.valid() {
33		return errors.New("invalid route origin")
34	}
35	return nil
36}
37
38// RouteKind identifies the kind of target represented by one display entry.
39type RouteKind uint8
40
41const (
42	RouteKindLocal RouteKind = iota + 1
43	RouteKindRemote
44)
45
46func (k RouteKind) valid() bool {
47	switch k {
48	case RouteKindLocal, RouteKindRemote:
49		return true
50	default:
51		return false
52	}
53}
54
55func (k RouteKind) Validate() error {
56	if !k.valid() {
57		return errors.New("invalid route kind")
58	}
59	return nil
60}
61
62// RouteReachability is a closed display hint. It is never used as proof that
63// a route can be attached; the exact target and daemon still decide that.
64type RouteReachability uint8
65
66const (
67	RouteReachabilityUnknown RouteReachability = iota
68	RouteReachabilityReachable
69	RouteReachabilityUnavailable
70)
71
72func (r RouteReachability) valid() bool {
73	switch r {
74	case RouteReachabilityUnknown, RouteReachabilityReachable, RouteReachabilityUnavailable:
75		return true
76	default:
77		return false
78	}
79}
80
81func (r RouteReachability) Validate() error {
82	if !r.valid() {
83		return errors.New("invalid route reachability")
84	}
85	return nil
86}
87
88// ValidateRouteLabel rejects malformed or terminal-control text before it can
89// enter a daemon-rendered route snapshot. Empty labels are allowed only when
90// the caller explicitly marks them optional.
91func ValidateRouteLabel(value string, allowEmpty bool) error {
92	if !allowEmpty && value == "" {
93		return errors.New("route label is empty")
94	}
95	if len(value) > RouteLabelMaxBytes {
96		return errors.New("route label is too long")
97	}
98	if !utf8.ValidString(value) {
99		return errors.New("route label is not valid UTF-8")
100	}
101	for _, r := range value {
102		if unicode.IsControl(r) {
103			return errors.New("route label contains a control character")
104		}
105		if unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
106			return errors.New("route label contains a bidirectional or line-separator control character")
107		}
108	}
109	return nil
110}
111
112// ExactSessionTarget is the daemon-neutral identity required to attach to a
113// specific session lifecycle. The transport endpoint is deliberately absent:
114// the selected route's dialer owns that boundary.
115type ExactSessionTarget struct {
116	LifecycleID domain.SessionLifecycleID
117	SessionName string
118}
119
120func (t ExactSessionTarget) Validate() error {
121	if t.LifecycleID == (domain.SessionLifecycleID{}) {
122		return errors.New("missing session lifecycle")
123	}
124	if err := domain.ValidateSessionName(t.SessionName); err != nil {
125		return fmt.Errorf("invalid session name: %w", err)
126	}
127	return nil
128}
129
130// CommittedRouteIdentity is the identity a daemon commits after accepting an
131// attach. It is separate from display data so a stale presentation cannot
132// become an attach authority.
133type CommittedRouteIdentity struct {
134	Target    ExactSessionTarget
135	Ephemeral bool
136}
137
138func (i CommittedRouteIdentity) Validate() error {
139	if err := i.Target.Validate(); err != nil {
140		return err
141	}
142	return nil
143}
144
145// RoutePosition is mutable per-client route state published by the daemon.
146// Target binds the tab cursor to one exact session lifecycle so delayed frames
147// cannot update another route.
148type RoutePosition struct {
149	Target      ExactSessionTarget
150	ActiveTabID domain.TabStableID
151}
152
153func (p RoutePosition) Validate() error {
154	if err := p.Target.Validate(); err != nil {
155		return err
156	}
157	if err := domain.ValidateTabStableID(p.ActiveTabID); err != nil {
158		return fmt.Errorf("invalid active tab ID: %w", err)
159	}
160	return nil
161}
162
163// RouteRef is an opaque client-ledger reference. It is intentionally not a
164// lifecycle identifier and is meaningful only with the matching generation.
165type RouteRef struct {
166	Key        uint64
167	Generation uint64
168}
169
170func (r RouteRef) empty() bool { return r.Key == 0 && r.Generation == 0 }
171
172func (r RouteRef) Validate() error {
173	if r.empty() {
174		return nil
175	}
176	if r.Key == 0 || r.Generation == 0 {
177		return errors.New("route reference must contain key and generation")
178	}
179	return nil
180}
181
182// RecentRouteEntry is the complete daemon-neutral display view of one route.
183// It contains no dialer, credential, endpoint, live session pointer, or attach
184// target. The client ledger keeps those private beside this value.
185type RecentRouteEntry struct {
186	Key          uint64
187	Generation   uint64
188	Name         string
189	HostLabel    string
190	Kind         RouteKind
191	Ephemeral    bool
192	Attention    bool
193	Reachability RouteReachability
194}
195
196// RecentRouteSnapshot is an immutable, bounded publication of the client
197// route ledger. Entries are ordered from most recent to least recent.
198type RecentRouteSnapshot struct {
199	Generation uint64
200	Active     RouteRef
201	Previous   RouteRef
202	Home       RouteRef
203	Entries    []RecentRouteEntry
204}
205
206// RouteNavigationAction asks the client to resolve one entry from a specific
207// complete snapshot. It carries no session name, endpoint, or credential.
208type RouteNavigationAction struct {
209	SnapshotGeneration uint64
210	Key                uint64
211	Generation         uint64
212}
213
214// RouteNavigationFailure is a bounded taxonomy for a rejected or stale route
215// action. User-facing text remains local to the receiving client.
216type RouteNavigationFailure struct {
217	Key        uint64
218	Generation uint64
219	Code       RouteFailureCode
220}
221
222type RouteFailureCode uint8
223
224const (
225	RouteFailureStaleSelection RouteFailureCode = iota + 1
226	RouteFailureNoSuchRoute
227	RouteFailureUnavailable
228	RouteFailureTargetChanged
229	RouteFailureOriginUnavailable
230)
231
232func (c RouteFailureCode) valid() bool {
233	switch c {
234	case RouteFailureStaleSelection, RouteFailureNoSuchRoute, RouteFailureUnavailable, RouteFailureTargetChanged, RouteFailureOriginUnavailable:
235		return true
236	default:
237		return false
238	}
239}
240
241const (
242	// RouteSnapshotMaxEntries bounds one immutable publication and the amount
243	// of work a receiver performs before returning to its transport loop. The
244	// private client history may use a smaller product cap.
245	RouteSnapshotMaxEntries = 32
246	RouteLabelMaxBytes      = 128
247)

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