session_view.go RawHistoryFolder

1package daemon
2
3import (
4	"time"
5
6	"github.com/bnema/vev/internal/domain"
7)
8
9// viewOptions controls optional (costly) fields captured by snapshotView.
10type viewOptions struct {
11	tabDetails    bool
12	focusedTitles bool
13	terminalTitle bool
14}
15
16// tabView is an immutable, value-only description of one tab.
17type tabView struct {
18	id           domain.TabStableID
19	name         string
20	focusedTitle string
21	attention    bool
22	attentionAt  time.Time
23}
24
25// sessionView is an immutable, value-only description of a session. It
26// deliberately retains no live pointers, mirroring route snapshot entries.
27// Listing paths (picker, palette, and list-sessions) read local session state
28// through snapshotView.
29type sessionView struct {
30	id           domain.SessionID
31	incarnation  domain.IncarnationID
32	name         string
33	ephemeral    bool
34	createdAt    int64
35	defaultTab   int
36	mruAt        uint64
37	attached     bool
38	tabCount     int
39	hasAttention bool
40	tabs         []tabView
41}
42
43// sessionInteractivelyAttachedLocked reports whether sess has registered
44// interactive presence: at least one attachment that is active or has an
45// activation in progress. Suspended attachments keep transport ownership and
46// session membership but hold no interactive authority, so they do not count.
47// Caller holds s.mu; each activity read briefly takes only that attachment's
48// lifecycle.mu and never freezes, drains, or waits on a gate.
49func sessionInteractivelyAttachedLocked(s *session) bool {
50	if s == nil {
51		return false
52	}
53	for ac := range s.attachments {
54		switch ac.attachmentActivity() {
55		case attachmentActive, attachmentActivating:
56			return true
57		}
58	}
59	return false
60}
61
62// snapshotView reads session fields under s.mu and samples the independently
63// atomic mruAt while that lock is held. tabCount and hasAttention are always
64// filled; tabs is allocated only when opts.tabDetails is true (non-nil empty
65// slice when there are no tabs). Never call while holding a pane lock.
66func (s *session) snapshotView(opts viewOptions) sessionView {
67	if opts.focusedTitles && !opts.tabDetails {
68		opts.tabDetails = true
69	}
70	s.mu.Lock()
71	// Session-level listings use the first ordered tab as their deterministic
72	// default. Interactive callers pass an attachment and resolve its stable
73	// view separately.
74	view := sessionView{
75		id:          s.id,
76		incarnation: s.incarnation,
77		name:        s.name,
78		ephemeral:   s.ephemeral,
79		createdAt:   s.createdAt,
80		defaultTab:  0,
81		mruAt:       s.mruAt.Load(),
82		// attached reports interactive presence only; a suspended-only session
83		// remains registered for teardown and possible activation but is not
84		// observably attached.
85		attached: sessionInteractivelyAttachedLocked(s),
86		tabCount: len(s.tabs),
87	}
88	if opts.tabDetails {
89		view.tabs = make([]tabView, 0, len(s.tabs))
90	}
91	for i, tb := range s.tabs {
92		if tb.attention {
93			view.hasAttention = true
94			if !opts.tabDetails {
95				break
96			}
97		}
98		if !opts.tabDetails {
99			continue
100		}
101		entry := tabView{
102			id:          domain.TabStableID(tb.stableID),
103			name:        tabDisplayName(tb, i),
104			attention:   tb.attention,
105			attentionAt: tb.attentionAt,
106		}
107		if opts.focusedTitles {
108			entry.focusedTitle = tb.focusedPaneTitle(opts.terminalTitle)
109		}
110		view.tabs = append(view.tabs, entry)
111	}
112	s.mu.Unlock()
113	return view
114}
115
116// pickerView renders this snapshot as the picker's daemon-private value type.
117func (view sessionView) pickerView() pickerSessionView {
118	out := pickerSessionView{
119		ID:          view.id,
120		Incarnation: view.incarnation,
121		Name:        view.name,
122		TargetName:  view.name,
123		Active:      view.defaultTab,
124		Tabs:        make([]pickerTabEntry, 0, len(view.tabs)),
125	}
126	if !view.ephemeral {
127		createdAt := view.createdAt
128		out.ExpectedCreatedAt = &createdAt
129	}
130	attention := false
131	for _, tb := range view.tabs {
132		out.Tabs = append(out.Tabs, pickerTabEntry{
133			TabID:     tb.id,
134			Name:      tb.name,
135			Detail:    tabTitleDetail(tb.name, tb.focusedTitle),
136			Attention: tb.attention,
137		})
138		if tb.attention {
139			attention = true
140		}
141	}
142	if attention {
143		out.Name = attentionSuffix(out.Name)
144	}
145	return out
146}

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