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