result.go RawHistoryFolder

1package palette
2
3import (
4	"time"
5	"unicode/utf8"
6
7	"github.com/bnema/vev/internal/domain"
8	"github.com/bnema/vev/internal/protocol"
9	"github.com/bnema/vev/internal/usecase/command"
10)
11
12// ResultKind identifies the immutable target represented by a palette row.
13type ResultKind uint8
14
15const (
16	ResultKindCommand ResultKind = iota
17	ResultKindActiveSession
18	ResultKindStoppedSession
19	ResultKindRemoteSession
20	ResultKindRecentRoute
21	ResultKindCreateSessionDestination
22	ResultKindImportedSession
23)
24
25// CreateSessionDestinationKind identifies where a CNS result creates a session.
26type CreateSessionDestinationKind uint8
27
28const (
29	CreateSessionOnServingDaemon CreateSessionDestinationKind = iota + 1
30	CreateSessionOnLocalRoute
31	CreateSessionOnRemoteHost
32)
33
34// Result is an immutable palette target. Its kind is the sole discriminator
35// for its private command, local-session, remote-session, imported-session,
36// or route payload.
37type Result struct {
38	kind              ResultKind
39	command           command.Command
40	session           sessionPayload
41	remoteSession     remoteSessionPayload
42	importedSession   importedSessionPayload
43	route             routePayload
44	createDestination createSessionDestinationPayload
45}
46
47type sessionPayload struct {
48	display   string
49	createdAt time.Time
50	target    protocol.ExactSessionTarget
51}
52
53type remoteSessionPayload struct {
54	key               domain.RemoteSessionKey
55	target            domain.RemoteSessionTarget
56	unavailableReason string
57}
58
59// importedSessionPayload is one sanitized navigation-inventory row relayed
60// from the client's local control source. It carries display facts and the
61// opaque selection keys only: no endpoint, connection parameters, CWD,
62// environment, preview, or pane content. Resolution happens source-side
63// through the keys.
64type importedSessionPayload struct {
65	sourceKey     string
66	entryKey      string
67	name          string
68	displayOrigin string
69	state         string
70	reason        string
71}
72
73type routePayload struct {
74	name   string
75	label  string
76	action protocol.RouteNavigationAction
77}
78
79type createSessionDestinationPayload struct {
80	name          string
81	kind          CreateSessionDestinationKind
82	displayOrigin string
83	endpoint      string
84	route         protocol.RouteRef
85	snapshotGen   uint64
86}
87
88// NewCommandResult creates a static command palette target.
89func NewCommandResult(cmd command.Command) Result {
90	return Result{kind: ResultKindCommand, command: cmd}
91}
92
93// NewActiveSessionResult creates an immutable active named-session target.
94func NewActiveSessionResult(target protocol.ExactSessionTarget, createdAt time.Time) Result {
95	return NewActiveSessionResultWithDisplayOrigin(target, createdAt, "")
96}
97
98// NewActiveSessionResultWithDisplayOrigin creates an active named-session target
99// qualified for presentation through a remote attachment origin.
100func NewActiveSessionResultWithDisplayOrigin(target protocol.ExactSessionTarget, createdAt time.Time, displayOrigin string) Result {
101	return Result{
102		kind:    ResultKindActiveSession,
103		session: newSessionPayload(target, createdAt, displayOrigin),
104	}
105}
106
107// NewStoppedSessionResult creates an immutable stopped named-session target.
108func NewStoppedSessionResult(target protocol.ExactSessionTarget, createdAt time.Time) Result {
109	return NewStoppedSessionResultWithDisplayOrigin(target, createdAt, "")
110}
111
112// NewStoppedSessionResultWithDisplayOrigin creates a stopped named-session target
113// qualified for presentation through a remote attachment origin.
114func NewStoppedSessionResultWithDisplayOrigin(target protocol.ExactSessionTarget, createdAt time.Time, displayOrigin string) Result {
115	return Result{
116		kind:    ResultKindStoppedSession,
117		session: newSessionPayload(target, createdAt, displayOrigin),
118	}
119}
120
121// SessionDisplay qualifies palette destinations relative to the client.
122func SessionDisplay(name, origin string) string {
123	if origin == "" {
124		return name
125	}
126	return name + "@" + domain.RemoteDisplayOrigin(origin)
127}
128
129func remoteSessionDisplay(key domain.RemoteSessionKey) string {
130	origin := key.DisplayOrigin
131	if origin == "" {
132		origin = domain.RemoteDisplayOrigin(key.Host)
133	}
134	return SessionDisplay(key.Name, origin)
135}
136
137func newSessionPayload(target protocol.ExactSessionTarget, createdAt time.Time, displayOrigin string) sessionPayload {
138	return sessionPayload{display: SessionDisplay(target.SessionName, displayOrigin), createdAt: createdAt, target: target}
139}
140
141// NewImportedSessionResult creates an immutable inventory-relayed row.
142// Labels are never parsed back into selection authority: Enter resolves
143// through the opaque source/entry keys.
144func NewImportedSessionResult(sourceKey, entryKey, name, displayOrigin, state, reason string) Result {
145	return Result{
146		kind: ResultKindImportedSession,
147		importedSession: importedSessionPayload{
148			sourceKey: sourceKey, entryKey: entryKey, name: name,
149			displayOrigin: displayOrigin, state: state, reason: reason,
150		},
151	}
152}
153
154// NewRecentRouteResult creates an immutable client-ledger route target.
155func NewRecentRouteResult(name, label string, action protocol.RouteNavigationAction) Result {
156	return Result{kind: ResultKindRecentRoute, route: routePayload{name: name, label: label, action: action}}
157}
158
159// NewCreateSessionDestination creates an immutable destination template. The
160// palette binds the current validated CNS name without parsing rendered text.
161func NewCreateSessionDestination(kind CreateSessionDestinationKind, displayOrigin, endpoint string, route protocol.RouteRef, snapshotGeneration uint64) Result {
162	return Result{kind: ResultKindCreateSessionDestination, createDestination: createSessionDestinationPayload{
163		kind: kind, displayOrigin: displayOrigin, endpoint: endpoint, route: route, snapshotGen: snapshotGeneration,
164	}}
165}
166
167func (r Result) withCreateSessionName(name string) Result {
168	r.createDestination.name = name
169	return r
170}
171
172func (r Result) Kind() ResultKind { return r.kind }
173
174func (r Result) sameTarget(other Result) bool {
175	if r.kind != other.kind {
176		return false
177	}
178	switch r.kind {
179	case ResultKindCommand:
180		return r.command.Slug == other.command.Slug
181	case ResultKindActiveSession, ResultKindStoppedSession:
182		return r.session.target == other.session.target
183	case ResultKindRemoteSession:
184		return r.remoteSession.key == other.remoteSession.key && r.remoteSession.target == other.remoteSession.target
185	case ResultKindImportedSession:
186		return r.importedSession.sourceKey == other.importedSession.sourceKey && r.importedSession.entryKey == other.importedSession.entryKey
187	case ResultKindRecentRoute:
188		return r.route.action == other.route.action
189	case ResultKindCreateSessionDestination:
190		return r.createDestination.kind == other.createDestination.kind &&
191			r.createDestination.endpoint == other.createDestination.endpoint &&
192			r.createDestination.route == other.createDestination.route &&
193			r.createDestination.snapshotGen == other.createDestination.snapshotGen
194	default:
195		return false
196	}
197}
198
199func (r Result) DisplayText() string {
200	if r.kind == ResultKindCommand {
201		return r.command.Code
202	}
203	if r.kind == ResultKindRemoteSession {
204		return activeSessionDisplayPrefix + remoteSessionDisplay(r.remoteSession.key)
205	}
206	if r.kind == ResultKindImportedSession {
207		return activeSessionDisplayPrefix + r.importedSession.display()
208	}
209	if r.kind == ResultKindRecentRoute {
210		return activeSessionDisplayPrefix + r.route.label
211	}
212	if r.kind == ResultKindCreateSessionDestination {
213		return r.createSessionDestinationDisplay()
214	}
215	return r.sessionDisplayPrefix() + r.session.display
216}
217
218func (r Result) createSessionDestinationDisplay() string {
219	destination := r.createDestination
220	if destination.name == "" {
221		if destination.kind == CreateSessionOnLocalRoute {
222			return "Create session locally…"
223		}
224		return "Create session on " + destination.displayOrigin + "…"
225	}
226	text := "Create session “" + destination.name + "”"
227	if destination.kind != CreateSessionOnLocalRoute {
228		text += " on " + destination.displayOrigin
229	}
230	return text
231}
232
233func (r Result) sessionDisplayPrefix() string {
234	if r.kind == ResultKindStoppedSession {
235		return stoppedSessionDisplayPrefix
236	}
237	return activeSessionDisplayPrefix
238}
239
240func (r Result) SearchText() string {
241	return r.DisplayText()
242}
243
244// searchTerms returns the user-facing identity and qualified label without the
245// action prefix. The offset maps term rune positions back into DisplayText.
246func (r Result) searchTerms() (identity, label string, offset int, ok bool) {
247	switch r.kind {
248	case ResultKindCommand:
249		return r.command.Code, r.command.Code, 0, true
250	case ResultKindActiveSession, ResultKindStoppedSession:
251		return r.session.target.SessionName, r.session.display, utf8.RuneCountInString(r.sessionDisplayPrefix()), true
252	case ResultKindRemoteSession:
253		return r.remoteSession.key.Name, remoteSessionDisplay(r.remoteSession.key), utf8.RuneCountInString(activeSessionDisplayPrefix), true
254	case ResultKindImportedSession:
255		return r.importedSession.name, r.importedSession.display(), utf8.RuneCountInString(activeSessionDisplayPrefix), true
256	case ResultKindRecentRoute:
257		return r.route.name, r.route.label, utf8.RuneCountInString(activeSessionDisplayPrefix), true
258	default:
259		return "", "", 0, false
260	}
261}
262
263// Command returns the command payload only for command results.
264func (r Result) Command() (command.Command, bool) {
265	return r.command, r.kind == ResultKindCommand
266}
267
268// SessionName returns the session name only for session results.
269func (r Result) SessionName() (string, bool) {
270	return r.session.target.SessionName, r.kind == ResultKindActiveSession || r.kind == ResultKindStoppedSession
271}
272
273// SessionCreatedAt returns the session creation time only for session results.
274func (r Result) SessionCreatedAt() (time.Time, bool) {
275	return r.session.createdAt, r.kind == ResultKindActiveSession || r.kind == ResultKindStoppedSession
276}
277
278// SessionTarget returns the exact lifecycle UUID and name for local active and
279// stopped session results.
280func (r Result) SessionTarget() (protocol.ExactSessionTarget, bool) {
281	return r.session.target, r.kind == ResultKindActiveSession || r.kind == ResultKindStoppedSession
282}
283
284// RemoteSessionTarget returns the structured catalog target only for a remote
285// session result.
286func (r Result) RemoteSessionTarget() (domain.RemoteSessionTarget, bool) {
287	return r.remoteSession.target, r.kind == ResultKindRemoteSession
288}
289
290// RemoteSessionKey returns the structured catalog identity only for a remote
291// session result.
292func (r Result) RemoteSessionKey() (domain.RemoteSessionKey, bool) {
293	return r.remoteSession.key, r.kind == ResultKindRemoteSession
294}
295
296// RemoteSessionUnavailableReason returns the catalog availability reason only
297// for a remote session result.
298func (r Result) RemoteSessionUnavailableReason() (string, bool) {
299	return r.remoteSession.unavailableReason, r.kind == ResultKindRemoteSession
300}
301
302// importedDisplay qualifies an inventory row with its explicit origin.
303// Unqualified names would collapse homonyms across local and remote
304// sources into the wrong selection.
305func (p importedSessionPayload) qualified() string {
306	return SessionDisplay(p.name, p.displayOrigin)
307}
308
309func (p importedSessionPayload) display() string {
310	base := p.qualified()
311	// Remote vision is read-only availability tagging: availability
312	// reasons surface independently of lifecycle state (a cached-up row
313	// can still be unreachable), and any non-live state tags inline so
314	// stale or broken entries never look attachable. Live rows without a
315	// reason stay untagged.
316	tag := p.state
317	if tag == "" || tag == "up" {
318		tag = ""
319	}
320	if p.reason != "" {
321		if tag == "" {
322			return base + " (" + p.reason + ")"
323		}
324		return base + " (" + tag + ": " + p.reason + ")"
325	}
326	if tag == "" {
327		return base
328	}
329	return base + " (" + tag + ")"
330}
331
332// ImportedSessionKey returns the opaque inventory selection keys only for
333// an imported session result.
334func (r Result) ImportedSessionKey() (sourceKey, entryKey string, ok bool) {
335	return r.importedSession.sourceKey, r.importedSession.entryKey, r.kind == ResultKindImportedSession
336}
337
338// ImportedSessionDisplay returns the qualified display label and reason only
339// for an imported session result.
340func (r Result) ImportedSessionDisplay() (display, reason string, ok bool) {
341	if r.kind != ResultKindImportedSession {
342		return "", "", false
343	}
344	return r.importedSession.qualified(), r.importedSession.reason, true
345}
346
347// RouteNavigationAction returns the exact client-ledger target only for a
348// recent-route result.
349func (r Result) RouteNavigationAction() (protocol.RouteNavigationAction, bool) {
350	return r.route.action, r.kind == ResultKindRecentRoute
351}
352
353// CreateSessionDestination returns structured creation authority only for CNS
354// destination rows. Labels are never parsed back into these values.
355func (r Result) CreateSessionDestination() (name string, kind CreateSessionDestinationKind, displayOrigin, endpoint string, route protocol.RouteRef, ok bool) {
356	d := r.createDestination
357	return d.name, d.kind, d.displayOrigin, d.endpoint, d.route, r.kind == ResultKindCreateSessionDestination
358}
359
360// CreateSessionSnapshotGeneration returns the route snapshot generation bound
361// to a local destination. Other destination kinds carry zero.
362func (r Result) CreateSessionSnapshotGeneration() (uint64, bool) {
363	return r.createDestination.snapshotGen, r.kind == ResultKindCreateSessionDestination
364}
365
366const (
367	activeSessionDisplayPrefix  = "Switch to session "
368	stoppedSessionDisplayPrefix = "Resume session "
369)
370
371// CommandResults converts static commands to immutable palette targets.
372func CommandResults(commands []command.Command) []Result {
373	results := make([]Result, len(commands))
374	for i, cmd := range commands {
375		results[i] = NewCommandResult(cmd)
376	}
377	return results
378}

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