1package client
2
3import (
4 "time"
5
6 "github.com/bnema/vev/internal/domain"
7 "github.com/bnema/vev/internal/ports"
8 "github.com/bnema/vev/internal/protocol"
9 "github.com/bnema/vev/internal/usecase/keys/kittykey"
10)
11
12// Picker is the composition seam over the client-owned picker.
13//
14// A composition must construct one picker with NewPicker, assign it once to
15// SupervisorConfig.Picker, and paint the bytes returned by Render and
16// RenderNotice. Its zero value is inert so optional composition remains safe,
17// but it does not provide a working picker.
18//
19// The remaining exported methods are the supervisor contract: ApplySnapshot,
20// TakeOp, ResolveKey, SetOwnsInput, OpsReady, and ConsumeTerminalRead. A
21// composition must never drive those methods itself.
22// Doing so can violate the supervisor's single-reader and single-input-owner
23// invariants or consume a pending user decision before the supervisor handles
24// it.
25//
26// Painting is allowed only while PickerPresentation reports true: the plain
27// picker, or the picker overlay over a live attachment whose output the
28// attachment foreground suppresses while the overlay is open. In
29// particular, PresentConnecting is not safe: once the attachment foreground is
30// admitted it owns the same terminal writer before MarkAttached or the initial
31// publication. Nothing serializes picker painting with that foreground.
32// The picker deliberately neither owns a terminal writer nor consumes the
33// terminal resize channel; the composition supplies its current geometry when
34// rendering.
35type Picker struct {
36 controller *pickerController
37}
38
39// Picker is the supervisor's picker seam: the value assigned to
40// SupervisorConfig.Picker.
41var _ pickerHost = (*Picker)(nil)
42
43// PickerPresentation reports whether composition-owned picker bytes may be
44// written for state. This is intentionally narrower than "not attached": an
45// admitted foreground can write while the public presentation is still
46// PresentConnecting.
47func PickerPresentation(state State) bool {
48 return state.Presentation == PresentPicker || state.Presentation == PresentAttachedPicker
49}
50
51// NewPicker composes one client-owned picker. A nil clock uses the system
52// clock, and a zero freshness uses the catalogue default. trueColor is the
53// terminal's detected color capability (AttachmentEnvironment.TrueColor); the
54// picker renders indexed colors without it.
55func NewPicker(clock ports.Clock, freshness time.Duration, trueColor bool) *Picker {
56 return &Picker{controller: newPickerController(clock, freshness, trueColor)}
57}
58
59// ApplySnapshot forwards a broker publication to the supervisor-owned picker.
60func (p *Picker) ApplySnapshot(snapshot ports.BrokerSnapshot) {
61 if p == nil || p.controller == nil {
62 return
63 }
64 p.controller.ApplySnapshot(snapshot)
65}
66
67// TakeOp returns and clears the pending picker operation for the supervisor.
68func (p *Picker) TakeOp() (pickerOp, string) {
69 if p == nil || p.controller == nil {
70 return pickerOp{}, ""
71 }
72 return p.controller.TakeOp()
73}
74
75// ResolveCommit resolves a committed key plus the exact tab it names.
76func (p *Picker) ResolveCommit(key string, base pickerResolveBase) (ports.BrokerOpenStreamRequest, attachmentTab, error) {
77 if p == nil || p.controller == nil {
78 return (*pickerController)(nil).ResolveCommit(key, base)
79 }
80 return p.controller.ResolveCommit(key, base)
81}
82
83// ResolveKill revalidates one kill key for the supervisor.
84func (p *Picker) ResolveKill(key string) (pickerKillTarget, error) {
85 if p == nil || p.controller == nil {
86 return (*pickerController)(nil).ResolveKill(key)
87 }
88 return p.controller.ResolveKill(key)
89}
90
91// SetCurrent names the attachment the picker is presented over.
92func (p *Picker) SetCurrent(current pickerCurrent) {
93 if p != nil && p.controller != nil {
94 p.controller.SetCurrent(current)
95 }
96}
97
98// PreviewRequest names the selected row's preview route and viewport.
99func (p *Picker) PreviewRequest(size domain.Size) (ports.BrokerPreviewRoute, protocol.RemotePreviewRequest, bool) {
100 if p == nil || p.controller == nil {
101 return ports.BrokerPreviewRoute{}, protocol.RemotePreviewRequest{}, false
102 }
103 return p.controller.PreviewRequest(size)
104}
105
106func (p *Picker) SetPreview(preview protocol.RemotePreview, state previewState) {
107 if p != nil && p.controller != nil {
108 p.controller.SetPreview(preview, state)
109 }
110}
111
112func (p *Picker) ClearPreview() {
113 if p != nil && p.controller != nil {
114 p.controller.ClearPreview()
115 }
116}
117
118// SetOwnsInput updates picker input ownership at an attachment boundary.
119func (p *Picker) SetOwnsInput(owns bool) {
120 if p == nil || p.controller == nil {
121 return
122 }
123 p.controller.SetOwnsInput(owns)
124}
125
126// Notify shows one bounded client-local notice on the picker.
127func (p *Picker) Notify(n domain.Notification) {
128 if p != nil && p.controller != nil {
129 p.controller.Notify(n)
130 }
131}
132
133func (p *Picker) noticeDeadline() (time.Time, bool) {
134 if p == nil || p.controller == nil {
135 return time.Time{}, false
136 }
137 return p.controller.noticeDeadline()
138}
139
140// invalidatePresentation forces the next frame to redraw the whole box.
141func (p *Picker) invalidatePresentation() {
142 if p != nil && p.controller != nil {
143 p.controller.invalidatePresentation()
144 }
145}
146
147// OpsReady returns the supervisor's coalescing operation wake channel.
148func (p *Picker) OpsReady() <-chan struct{} {
149 if p == nil || p.controller == nil {
150 return nil
151 }
152 return p.controller.OpsReady()
153}
154
155// ConsumeTerminalRead lets the supervisor offer one read to the picker.
156func (p *Picker) ConsumeTerminalRead(data []byte) bool {
157 if p == nil || p.controller == nil {
158 return false
159 }
160 // The picker decodes legacy keys; kitty keyboard sequences are
161 // translated back before it sees them.
162 return p.controller.ConsumeTerminalRead(kittykey.Translate(data, 0))
163}
164
165// Render returns the current picker frame for size, or nil when no catalogue
166// has been applied yet. A composition paints this payload when Render is
167// invoked and the reported presentation is PresentPicker.
168func (p *Picker) Render(size domain.Size) []byte {
169 if p == nil || p.controller == nil {
170 return nil
171 }
172 return p.controller.Render(size)
173}
174
175// RenderNotice returns the notice stack for size, including blanks for boxes
176// that went away, or nil when nothing changes on screen. A composition paints
177// it after the picker frame only while the supervisor reports PresentPicker.
178func (p *Picker) RenderNotice(size domain.Size) []byte {
179 if p == nil || p.controller == nil {
180 return nil
181 }
182 return p.controller.RenderNotice(size)
183}