v0.7.0 4c16dd8

lifecycle.go RawHistoryFolder

1package domain
2
3import (
4	"errors"
5	"fmt"
6	"io"
7	"strings"
8	"unicode"
9	"unicode/utf8"
10)
11
12// SessionLifecycleID is the opaque identity of one session lifecycle.
13//
14// IncarnationID predates the remote picker and is the durable implementation
15// used by snapshots and the catalogue. This alias deliberately keeps one
16// identity type rather than introducing a second, independently comparable
17// UUID. A lifecycle ID changes only when a session is destroyed and recreated.
18type SessionLifecycleID = IncarnationID
19
20// NewSessionLifecycleID allocates a non-zero lifecycle identity.
21func NewSessionLifecycleID(r io.Reader) (SessionLifecycleID, error) {
22	return NewIncarnationID(r)
23}
24
25// TabSelectorKind identifies the exact selector form used for a stopped tab.
26type TabSelectorKind uint8
27
28const (
29	TabSelectorByStableID TabSelectorKind = iota + 1
30	TabSelectorByOrdinal
31)
32
33// TabSelector is an exact tab identity handoff. Stable IDs are preferred for
34// persisted tabs. The ordinal form is a compatibility selector for catalogue
35// records that predate stable stopped-tab IDs; raw name and expected count are
36// required so a reordered or replaced tab cannot be selected accidentally.
37type TabSelector struct {
38	Kind          TabSelectorKind
39	StableID      TabStableID
40	Ordinal       uint16
41	RawName       string
42	ExpectedCount uint16
43}
44
45// NewStableTabSelector creates an exact stable-ID selector.
46func NewStableTabSelector(id TabStableID) TabSelector {
47	return TabSelector{Kind: TabSelectorByStableID, StableID: id}
48}
49
50// NewOrdinalTabSelector creates an exact legacy-compatible selector.
51func NewOrdinalTabSelector(ordinal uint16, rawName string, expectedCount uint16) TabSelector {
52	return TabSelector{Kind: TabSelectorByOrdinal, Ordinal: ordinal, RawName: rawName, ExpectedCount: expectedCount}
53}
54
55// Validate rejects selectors that could silently resolve a different tab.
56func (s TabSelector) Validate() error {
57	switch s.Kind {
58	case TabSelectorByStableID:
59		if s.Ordinal != 0 || s.RawName != "" || s.ExpectedCount != 0 || ValidateTabStableID(s.StableID) != nil {
60			return errors.New("invalid stable tab selector")
61		}
62	case TabSelectorByOrdinal:
63		if s.ExpectedCount == 0 || s.Ordinal >= s.ExpectedCount || s.StableID != "" {
64			return errors.New("invalid ordinal tab selector")
65		}
66		if err := validateTabSelectorName(s.RawName); err != nil {
67			return err
68		}
69	default:
70		return errors.New("unknown tab selector kind")
71	}
72	return nil
73}
74
75// TabSelectorTab is the immutable metadata needed to resolve a selector.
76type TabSelectorTab struct {
77	ID   TabStableID
78	Name string
79}
80
81// Resolve returns the selected tab index only when the complete selector still
82// describes the supplied ordered tab metadata.
83func (s TabSelector) Resolve(tabs []TabSelectorTab) (int, bool) {
84	if s.Validate() != nil || (len(tabs) != int(s.ExpectedCount) && s.Kind == TabSelectorByOrdinal) {
85		return 0, false
86	}
87	switch s.Kind {
88	case TabSelectorByStableID:
89		if len(tabs) == 0 {
90			return 0, false
91		}
92		found := -1
93		for i, tab := range tabs {
94			if tab.ID != s.StableID {
95				continue
96			}
97			if found != -1 {
98				return 0, false
99			}
100			found = i
101		}
102		return found, found >= 0
103	case TabSelectorByOrdinal:
104		index := int(s.Ordinal)
105		return index, index < len(tabs) && tabs[index].Name == s.RawName
106	default:
107		return 0, false
108	}
109}
110
111// RemoteSessionTarget is the structured identity used by picker-based remote
112// access. Endpoint and DisplayOrigin are intentionally separate values: the
113// latter is presentation-only and is never parsed back into a route. Stopped
114// records the selected down-row resume intent; it remains set while that exact
115// lifecycle is created and attached as a live runtime.
116type RemoteSessionTarget struct {
117	Endpoint      string
118	DisplayOrigin string
119	LifecycleID   SessionLifecycleID
120	SessionName   string
121	LiveTabID     TabStableID
122	StoppedTab    TabSelector
123	Stopped       bool
124}
125
126// Validate validates route, presentation, lifecycle, and selector identity as
127// independent contract fields.
128func (t RemoteSessionTarget) Validate() error {
129	if err := ValidateRemoteHostTarget(t.Endpoint); err != nil {
130		return fmt.Errorf("remote route endpoint: %w", err)
131	}
132	if err := ValidateRemoteDisplayOrigin(t.DisplayOrigin); err != nil {
133		return fmt.Errorf("remote display origin: %w", err)
134	}
135	if t.LifecycleID == (SessionLifecycleID{}) {
136		return errors.New("remote lifecycle ID is zero")
137	}
138	if err := ValidateSessionName(t.SessionName); err != nil {
139		return err
140	}
141	if t.Stopped {
142		if t.LiveTabID != "" {
143			return errors.New("down remote target cannot carry a live tab selector")
144		}
145		if t.StoppedTab != (TabSelector{}) && t.StoppedTab.Validate() != nil {
146			return errors.New("down remote target has an invalid tab selector")
147		}
148		return nil
149	}
150	if err := ValidateTabStableID(t.LiveTabID); err != nil {
151		return errors.New("up remote target requires a stable tab ID")
152	}
153	if t.StoppedTab != (TabSelector{}) {
154		return errors.New("up remote target cannot carry a down selector")
155	}
156	return nil
157}
158
159// ResolveTab resolves the selected tab against authoritative metadata. A down
160// record without retained tab metadata carries no selector and may choose only
161// an unambiguous default tab: the empty durable record before creation or its
162// sole fresh tab immediately afterward. It never guesses among multiple tabs.
163func (t RemoteSessionTarget) ResolveTab(tabs []TabSelectorTab) (int, bool) {
164	if t.Validate() != nil {
165		return 0, false
166	}
167	if t.Stopped {
168		if t.StoppedTab == (TabSelector{}) {
169			return 0, len(tabs) <= 1
170		}
171		return t.StoppedTab.Resolve(tabs)
172	}
173	return NewStableTabSelector(t.LiveTabID).Resolve(tabs)
174}
175
176// ValidateRemoteDisplayOrigin validates presentation data without imposing
177// SSH route grammar. Aliases, users, ports, and bracketed IPv6 remain opaque.
178func ValidateRemoteDisplayOrigin(origin string) error {
179	return validateRemoteText(origin, 256, "remote display origin")
180}
181
182// ValidateTabStableID validates the opaque stable tab identity used by
183// picker and attachment handoffs.
184func ValidateTabStableID(value TabStableID) error {
185	return validateStableID(string(value), "tab")
186}
187
188// ValidatePaneStableID validates the session/tab-qualified focused pane identity.
189func ValidatePaneStableID(value PaneStableID) error {
190	return validateStableID(string(value), "pane")
191}
192
193func validateStableID(value, kind string) error {
194	if value == "" || len(value) > 128 || !utf8.ValidString(value) {
195		return fmt.Errorf("invalid stable %s ID", kind)
196	}
197	for _, r := range value {
198		if unicode.IsControl(r) || unicode.IsSpace(r) {
199			return fmt.Errorf("invalid stable %s ID", kind)
200		}
201	}
202	return nil
203}
204
205func validateTabSelectorName(value string) error {
206	if len(value) > 256 {
207		return errors.New("tab name exceeds 256 bytes")
208	}
209	if !utf8.ValidString(value) {
210		return errors.New("tab name is not valid UTF-8")
211	}
212	for _, r := range value {
213		if unicode.IsControl(r) || r == '\n' || r == '\r' || r == '\x1b' {
214			return errors.New("tab name contains control characters")
215		}
216	}
217	return nil
218}
219
220func validateRemoteText(value string, maxBytes int, label string) error {
221	if value == "" {
222		return fmt.Errorf("%s is empty", label)
223	}
224	if len(value) > maxBytes {
225		return fmt.Errorf("%s exceeds %d bytes", label, maxBytes)
226	}
227	if !utf8.ValidString(value) {
228		return fmt.Errorf("%s is not valid UTF-8", label)
229	}
230	if strings.TrimSpace(value) != value {
231		return fmt.Errorf("%s has surrounding whitespace", label)
232	}
233	for _, r := range value {
234		if unicode.IsControl(r) || r == '\n' || r == '\r' || r == '\x1b' {
235			return fmt.Errorf("%s contains control characters", label)
236		}
237	}
238	return nil
239}

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