1package daemon
2
3import (
4 "time"
5
6 "github.com/bnema/vev/internal/domain"
7 "github.com/bnema/vev/internal/usecase/picker"
8)
9
10// viewOptions controls optional (costly) fields captured by snapshotView.
11type viewOptions struct {
12 tabDetails bool
13 focusedTitles bool
14 terminalTitle bool
15}
16
17// tabView is an immutable, value-only description of one tab.
18type tabView struct {
19 id domain.TabStableID
20 name string
21 focusedTitle string
22 attention bool
23 attentionAt time.Time
24}
25
26// sessionView is an immutable, value-only description of a session. It
27// deliberately retains no live pointers, mirroring route snapshot entries.
28// Listing paths (picker, palette, and list-sessions) read local session state
29// through snapshotView.
30type sessionView struct {
31 id domain.SessionID
32 incarnation domain.IncarnationID
33 name string
34 ephemeral bool
35 createdAt int64
36 defaultTab int
37 mruAt uint64
38 attached bool
39 tabCount int
40 hasAttention bool
41 tabs []tabView
42}
43
44// snapshotView reads session fields under s.mu and samples the independently
45// atomic mruAt while that lock is held. tabCount and hasAttention are always
46// filled; tabs is allocated only when opts.tabDetails is true (non-nil empty
47// slice when there are no tabs). Never call while holding a pane lock.
48func (s *session) snapshotView(opts viewOptions) sessionView {
49 if opts.focusedTitles && !opts.tabDetails {
50 opts.tabDetails = true
51 }
52 s.mu.Lock()
53 // Session-level listings use the first ordered tab as their deterministic
54 // default. Interactive callers pass an attachment and resolve its stable
55 // view separately.
56 view := sessionView{
57 id: s.id,
58 incarnation: s.incarnation,
59 name: s.name,
60 ephemeral: s.ephemeral,
61 createdAt: s.createdAt,
62 defaultTab: 0,
63 mruAt: s.mruAt.Load(),
64 attached: len(s.attachments) != 0,
65 tabCount: len(s.tabs),
66 }
67 if opts.tabDetails {
68 view.tabs = make([]tabView, 0, len(s.tabs))
69 }
70 for i, tb := range s.tabs {
71 if tb.attention {
72 view.hasAttention = true
73 if !opts.tabDetails {
74 break
75 }
76 }
77 if !opts.tabDetails {
78 continue
79 }
80 entry := tabView{
81 id: domain.TabStableID(tb.stableID),
82 name: tabDisplayName(tb, i),
83 attention: tb.attention,
84 attentionAt: tb.attentionAt,
85 }
86 if opts.focusedTitles {
87 entry.focusedTitle = tb.focusedPaneTitle(opts.terminalTitle)
88 }
89 view.tabs = append(view.tabs, entry)
90 }
91 s.mu.Unlock()
92 return view
93}
94
95// pickerView renders this snapshot as the picker's value type. Field-for-field
96// equivalent to the inline construction previously in pickerViews.
97func (view sessionView) pickerView() picker.SessionView {
98 out := picker.SessionView{
99 ID: view.id,
100 Incarnation: view.incarnation,
101 Name: view.name,
102 TargetName: view.name,
103 Active: view.defaultTab,
104 Tabs: make([]picker.TabEntry, 0, len(view.tabs)),
105 }
106 if !view.ephemeral {
107 createdAt := view.createdAt
108 out.ExpectedCreatedAt = &createdAt
109 }
110 attention := false
111 for _, tb := range view.tabs {
112 out.Tabs = append(out.Tabs, picker.TabEntry{
113 TabID: tb.id,
114 Name: tb.name,
115 Detail: tabTitleDetail(tb.name, tb.focusedTitle),
116 Attention: tb.attention,
117 })
118 if tb.attention {
119 attention = true
120 }
121 }
122 if attention {
123 out.Name = attentionSuffix(out.Name)
124 }
125 return out
126}