v0.3.0 99dc266

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
170// RouteAttentionTarget binds one displayed route reference to an exact
171// lifecycle on the daemon currently serving the client. It carries no dialer,
172// credential, or endpoint.
173type RouteAttentionTarget struct {
174	Ref    RouteRef
175	Target ExactSessionTarget
176}
177
178// RouteAttentionSubscription is the bounded, client-owned mapping a daemon
179// uses solely to resolve live attention while rendering its route snapshot.
180type RouteAttentionSubscription struct {
181	Targets []RouteAttentionTarget
182}
183
184func (r RouteRef) empty() bool { return r.Key == 0 && r.Generation == 0 }
185
186func (r RouteRef) Validate() error {
187	if r.empty() {
188		return nil
189	}
190	if r.Key == 0 || r.Generation == 0 {
191		return errors.New("route reference must contain key and generation")
192	}
193	return nil
194}
195
196// RecentRouteEntry is the complete daemon-neutral display view of one route.
197// It contains no dialer, credential, endpoint, live session pointer, or attach
198// target. The client ledger keeps those private beside this value.
199type RecentRouteEntry struct {
200	Key          uint64
201	Generation   uint64
202	Name         string
203	HostLabel    string
204	Kind         RouteKind
205	Ephemeral    bool
206	Attention    bool
207	Reachability RouteReachability
208}
209
210// RecentRouteSnapshot is an immutable, bounded publication of the client
211// route ledger. Entries are ordered from most recent to least recent.
212type RecentRouteSnapshot struct {
213	Generation uint64
214	Active     RouteRef
215	Previous   RouteRef
216	Home       RouteRef
217	Entries    []RecentRouteEntry
218}
219
220// RouteNavigationAction asks the client to resolve one entry from a specific
221// complete snapshot. It carries no session name, endpoint, or credential.
222type RouteNavigationAction struct {
223	SnapshotGeneration uint64
224	Key                uint64
225	Generation         uint64
226}
227
228// RouteNavigationFailure is a bounded taxonomy for a rejected or stale route
229// action. User-facing text remains local to the receiving client.
230type RouteNavigationFailure struct {
231	Key        uint64
232	Generation uint64
233	Code       RouteFailureCode
234}
235
236type RouteFailureCode uint8
237
238const (
239	RouteFailureStaleSelection RouteFailureCode = iota + 1
240	RouteFailureNoSuchRoute
241	RouteFailureUnavailable
242	RouteFailureTargetChanged
243	RouteFailureOriginUnavailable
244)
245
246func (c RouteFailureCode) valid() bool {
247	switch c {
248	case RouteFailureStaleSelection, RouteFailureNoSuchRoute, RouteFailureUnavailable, RouteFailureTargetChanged, RouteFailureOriginUnavailable:
249		return true
250	default:
251		return false
252	}
253}
254
255const (
256	// RouteSnapshotMaxEntries bounds one immutable publication and the amount
257	// of work a receiver performs before returning to its transport loop. The
258	// private client history may use a smaller product cap.
259	RouteSnapshotMaxEntries = 32
260	RouteLabelMaxBytes      = 128
261)

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