1package daemon
2
3import (
4 "errors"
5 "sort"
6 "time"
7
8 renderer "github.com/bnema/vev-vt"
9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/ports"
11 "github.com/bnema/vev/internal/usecase/command"
12 "github.com/bnema/vev/internal/usecase/layout"
13 "github.com/bnema/vev/internal/usecase/palette"
14 "github.com/bnema/vev/internal/usecase/picker"
15 themeui "github.com/bnema/vev/internal/usecase/theme"
16 "github.com/bnema/vev/internal/usecase/ui"
17)
18
19const (
20 paletteRailBreakpoint = 96
21 paletteRailWidth = 64
22)
23
24var paletteModal = ui.Modal{WidthPct: 100, MinWidth: 32, FixedHeight: 11, Title: " Commands ", Anchor: domain.AnchorBottom, Margins: ui.Margins{Top: 1, Right: 1, Bottom: 1, Left: 1}}
25
26func paletteModalFor(size domain.Size, cfg domain.PaletteConfig) ui.Modal {
27 modal := paletteModal
28 if size.Cols >= paletteRailBreakpoint {
29 modal.FixedWidth = paletteRailWidth
30 }
31 if cfg.AnchorSet {
32 modal.Anchor = cfg.Anchor
33 } else if size.Cols >= paletteRailBreakpoint {
34 modal.Anchor = domain.AnchorBottomRight
35 }
36 return modal
37}
38
39func (d *Daemon) enterPalette(sess *session, ac *attachedClient) {
40 // Capture the client-owned route snapshot before taking paletteMu. The
41 // snapshot is immutable for this palette interaction and the daemon never
42 // consults its own session history for recent-route commands.
43 routeSnapshot := ac.routeSnapshotCopy()
44 commands := d.paletteCommands()
45 results := d.paletteResults(sess, commands, routeSnapshot, ac.remoteOrigin)
46 ac.overlays.paletteMu.Lock()
47 ac.overlays.paletteGeneration++
48 ac.overlays.palette = palette.New(results)
49 ac.overlays.paletteRouteSnapshot = routeSnapshot
50 ac.overlays.paletteHints = palette.ContextualHints{}
51 ac.overlays.paletteFeedback = ""
52 ac.overlays.palettePending = nil
53 ac.overlays.paletteMu.Unlock()
54 d.invalidateRender(sess, ac, true, "palette.go")
55}
56
57// paletteResults captures eligible named sessions before paletteMu so the
58// palette has an immutable lifecycle target without violating lock ordering.
59func (d *Daemon) paletteResults(current *session, commands []command.Command, routeSnapshot ports.RecentRouteSnapshot, currentRemoteOrigin string) []palette.Result {
60 d.mu.Lock()
61 sessions := d.sessionsSnapshotLocked()
62 stopped := make([]stoppedSession, 0, len(d.stopped))
63 for _, entry := range d.stopped {
64 if !entry.purging {
65 stopped = append(stopped, entry)
66 }
67 }
68 d.mu.Unlock()
69
70 results := make([]palette.Result, 0, len(commands)+len(sessions)+len(stopped)+len(routeSnapshot.Entries))
71 for _, cmd := range commands {
72 results = append(results, palette.NewCommandResult(cmd))
73 }
74 active := make([]palette.Result, 0, len(sessions))
75 for _, candidate := range sessions {
76 if candidate == current {
77 continue
78 }
79 snap := candidate.snapshotView(viewOptions{})
80 if snap.name != "" && !snap.ephemeral {
81 active = append(active, palette.NewActiveSessionResult(snap.name, time.Unix(0, snap.createdAt), snap.id))
82 }
83 }
84 sort.Slice(active, func(i, j int) bool {
85 left, _ := active[i].SessionName()
86 right, _ := active[j].SessionName()
87 return left < right
88 })
89 sort.Slice(stopped, func(i, j int) bool { return stopped[i].name < stopped[j].name })
90 results = append(results, active...)
91 localNames := make(map[string]struct{}, len(active)+len(stopped))
92 for _, candidate := range active {
93 name, _ := candidate.SessionName()
94 localNames[name] = struct{}{}
95 }
96 for _, candidate := range stopped {
97 results = append(results, palette.NewStoppedSessionResult(candidate.name, time.Unix(0, candidate.createdAt)))
98 localNames[candidate.name] = struct{}{}
99 }
100 formattedRoutes := formatRecentRouteSnapshot(routeSnapshot)
101 for i, entry := range routeSnapshot.Entries {
102 if i >= len(formattedRoutes) {
103 break
104 }
105 label := formattedRoutes[i].name
106 if label == "" {
107 continue
108 }
109 if _, represented := localNames[entry.Name]; represented {
110 localRoute := entry.Kind == ports.RouteKindLocal
111 currentRemoteRoute := entry.Kind == ports.RouteKindRemote && currentRemoteOrigin != "" && entry.HostLabel == currentRemoteOrigin
112 if localRoute || currentRemoteRoute {
113 continue
114 }
115 }
116 results = append(results, palette.NewRecentRouteResult(label, ports.RouteNavigationAction{
117 SnapshotGeneration: routeSnapshot.Generation,
118 Key: entry.Key,
119 Generation: entry.Generation,
120 }))
121 }
122 return results
123}
124
125func recentRouteHints(snapshot ports.RecentRouteSnapshot, args []string) palette.ContextualHints {
126 formatted := formatRecentRouteSnapshot(snapshot)
127 names := make([]string, len(formatted))
128 for i, entry := range formatted {
129 names[i] = entry.name
130 }
131 hints := palette.BuildRecentSessionHints(names, args)
132 for i, entry := range snapshot.Entries {
133 if i >= len(hints.Recent) {
134 break
135 }
136 hints.Recent[i].SnapshotGeneration = snapshot.Generation
137 hints.Recent[i].Key = entry.Key
138 hints.Recent[i].Generation = entry.Generation
139 }
140 return hints
141}
142
143func (d *Daemon) paletteCommands() []command.Command {
144 commands := command.PaletteRegistry()
145 d.paletteRecentMu.Lock()
146 recent := append([]string(nil), d.paletteRecent...)
147 d.paletteRecentMu.Unlock()
148 overrides := d.codeOverrideSnapshot()
149
150 byCode := make(map[string]command.Command, len(commands))
151 for _, cmd := range commands {
152 cmd = commandWithOverrides(cmd, overrides)
153 byCode[cmd.Code] = cmd
154 }
155 out := make([]command.Command, 0, len(commands))
156 used := make(map[string]bool, len(recent))
157 for _, code := range recent {
158 cmd, ok := byCode[code]
159 if !ok || used[code] {
160 continue
161 }
162 out = append(out, cmd)
163 used[code] = true
164 }
165 for _, cmd := range commands {
166 cmd = commandWithOverrides(cmd, overrides)
167 if !used[cmd.Code] {
168 out = append(out, cmd)
169 }
170 }
171 return out
172}
173
174func (d *Daemon) recordPaletteUse(code string) {
175 d.paletteRecentMu.Lock()
176 defer d.paletteRecentMu.Unlock()
177
178 recent := make([]string, 0, len(d.paletteRecent)+1)
179 recent = append(recent, code)
180 for _, existing := range d.paletteRecent {
181 if existing != code {
182 recent = append(recent, existing)
183 }
184 }
185 d.paletteRecent = recent
186}
187
188func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte, effects ...*attachmentEffectTicket) {
189 entry := ac.currentAttachmentSession()
190 if entry == nil {
191 return
192 }
193 sess := entry
194 var cmd command.Command
195 var sessionTarget palette.Result
196 var hasSessionTarget bool
197 var routeTarget ports.RouteNavigationAction
198 var hasRouteTarget bool
199 var args []string
200 var routeSnapshot ports.RecentRouteSnapshot
201 var generation uint64
202 var rawQuery string
203 changed, cancel, execute := false, false, false
204 var effect *attachmentEffectTicket
205 if len(effects) != 0 {
206 effect = effects[0]
207 }
208
209 ac.overlays.paletteMu.Lock()
210 if ac.overlays.palette == nil {
211 ac.overlays.palettePending = nil
212 ac.overlays.paletteMu.Unlock()
213 return
214 }
215 routeOverlayBytes(data, &ac.overlays.palettePending, overlayEvents{
216 rune: func(r rune) {
217 if !execute {
218 ac.overlays.palette.Insert(r)
219 changed = true
220 }
221 },
222 backspace: func() {
223 if !execute {
224 ac.overlays.palette.Backspace()
225 changed = true
226 }
227 },
228 tab: func() {
229 if !execute {
230 changed = ac.overlays.palette.CompleteSelected() || changed
231 }
232 },
233 up: func() {
234 if !execute {
235 ac.overlays.palette.Up()
236 changed = true
237 }
238 },
239 down: func() {
240 if !execute {
241 ac.overlays.palette.Down()
242 changed = true
243 }
244 },
245 cancel: func() {
246 if !execute {
247 cancel = true
248 }
249 },
250 enter: func() {
251 if execute {
252 return
253 }
254 selected, ok := ac.overlays.palette.Selected()
255 if !ok {
256 changed = true
257 return
258 }
259 rawQuery = ac.overlays.palette.Query()
260 if selectedCommand, ok := selected.Command(); ok {
261 cmd = selectedCommand
262 // JRS is contextual: fuzzy selection cannot turn an unrelated
263 // query into a jump.
264 if cmd.Slug == "jump-recent-session" {
265 action, valid := palette.ParseAction([]palette.Result{selected}, rawQuery)
266 if !valid {
267 changed = true
268 return
269 }
270 args = action.Args
271 routeSnapshot = ac.overlays.paletteRouteSnapshot
272 routeSnapshot.Entries = append([]ports.RecentRouteEntry(nil), routeSnapshot.Entries...)
273 }
274 } else if action, ok := selected.RouteNavigationAction(); ok {
275 routeTarget = action
276 hasRouteTarget = true
277 } else if _, ok := selected.SessionName(); ok {
278 sessionTarget = selected
279 hasSessionTarget = true
280 } else {
281 changed = true
282 return
283 }
284 generation = ac.overlays.paletteGeneration
285 execute = true
286 },
287 })
288 if changed {
289 ac.overlays.paletteFeedback = ""
290 active, ok := ac.overlays.palette.ArgumentCommand()
291 if ok && active.Slug == "jump-recent-session" {
292 args := paletteArgs(ac.overlays.palette.Query(), active)
293 if ac.overlays.paletteRouteSnapshot.Generation != 0 {
294 ac.overlays.paletteHints = recentRouteHints(ac.overlays.paletteRouteSnapshot, args)
295 } else {
296 ac.overlays.paletteHints = palette.ContextualHints{}
297 }
298 } else {
299 ac.overlays.paletteHints = palette.ContextualHints{}
300 }
301 }
302 if cancel {
303 ac.clearPaletteLocked()
304 }
305 ac.overlays.paletteMu.Unlock()
306 if cancel {
307 d.invalidateRender(entry, ac, true, "palette.go")
308 return
309 }
310 if !execute {
311 if changed {
312 d.invalidateRender(entry, ac, true, "palette.go")
313 }
314 return
315 }
316
317 if hasRouteTarget {
318 exec := paletteExec{d: d, sess: sess, attachment: entry, ac: ac, effect: effect}
319 if err := exec.NavigateRecentRoute(routeTarget); err != nil {
320 if errors.Is(err, errAttachmentTransition) {
321 return
322 }
323 ac.paletteFailure(generation, rawQuery, "requested recent session is unavailable")
324 d.invalidateRender(entry, ac, true, "palette.go")
325 return
326 }
327 if ac.closeExecutedPalette(generation, rawQuery) {
328 d.invalidateRender(entry, ac, true, "palette.go")
329 }
330 return
331 }
332
333 if hasSessionTarget {
334 createdAt, _ := sessionTarget.SessionCreatedAt()
335 expectedCreatedAt := createdAt.UnixNano()
336 name, _ := sessionTarget.SessionName()
337 target := picker.Target{Name: name, TabIndex: -1, ExpectedCreatedAt: &expectedCreatedAt}
338 if id, active := sessionTarget.SessionID(); active {
339 target.Session = id
340 } else {
341 target.Stopped = true
342 }
343 var err error
344 if effect != nil {
345 err = d.switchToTargetForAttachment(effect.connectionToken(), target, sessionHandoffGuard{}, "palette-session")
346 } else {
347 err = d.switchToTarget(sess, ac, target)
348 }
349 if errors.Is(err, errAttachmentTransition) {
350 return
351 }
352 if err != nil {
353 ac.paletteFailure(generation, rawQuery, "requested session is unavailable")
354 d.invalidateRender(entry, ac, true, "palette.go")
355 return
356 }
357 if ac.closeExecutedPalette(generation, rawQuery) {
358 if current := ac.currentAttachmentSession(); current != nil {
359 d.invalidateRender(current, ac, true, "palette.go")
360 }
361 }
362 return
363 }
364
365 if cmd.Slug == "jump-recent-session" {
366 rank, err := command.ParsePositiveDecimal(args)
367 if err != nil {
368 ac.paletteFailure(generation, rawQuery, "rank must be one positive decimal")
369 d.invalidateRender(entry, ac, true, "palette.go")
370 return
371 }
372 exec := paletteExec{d: d, sess: sess, attachment: entry, ac: ac, routeSnapshot: routeSnapshot, effect: effect}
373 if err := exec.JumpRecentSession(rank); err != nil {
374 if errors.Is(err, errAttachmentTransition) {
375 return
376 }
377 ac.paletteFailure(generation, rawQuery, "requested recent session is unavailable")
378 d.invalidateRender(entry, ac, true, "palette.go")
379 return
380 }
381 if ac.closeExecutedPalette(generation, rawQuery) {
382 d.recordPaletteUse(cmd.Code)
383 if current := ac.currentAttachmentSession(); current != nil {
384 d.invalidateRender(current, ac, true, "palette.go")
385 }
386 }
387 return
388 }
389 attachmentHandoff := cmd.Slug == "back-session" || cmd.Slug == "detach"
390 if !attachmentHandoff && !ac.closeExecutedPalette(generation, rawQuery) {
391 return
392 }
393 sess.dispatchMu.Lock()
394 err := cmd.Run(paletteExec{d: d, sess: sess, ac: ac, effect: effect, redrawClosedPalette: true}, args)
395 sess.dispatchMu.Unlock()
396 if attachmentHandoff {
397 if current := ac.currentSession(); current != nil {
398 currentToken := current.attachmentToken(ac, ac.transport())
399 fresh, admitted := ac.beginAttachmentEffect(currentToken)
400 if ac.closeExecutedPalette(generation, rawQuery) {
401 d.invalidateRender(current, ac, true, "palette.go")
402 }
403 if admitted {
404 fresh.End()
405 }
406 } else {
407 ac.closeExecutedPalette(generation, rawQuery)
408 }
409 }
410 if errors.Is(err, errAttachmentTransition) {
411 return
412 }
413 if err != nil {
414 d.log.Error("palette command failed", "err", err, "command", cmd.Code)
415 d.reportError(sess, paletteCommandNoticeError(cmd, err))
416 } else {
417 d.recordPaletteUse(cmd.Code)
418 }
419}
420
421func paletteCommandNoticeError(cmd command.Command, err error) error {
422 if cmd.Scope == command.CommandScopeCrossSession {
423 return movePickerUserError(err)
424 }
425 return err
426}
427
428func paletteArgs(query string, cmd command.Command) []string {
429 action, ok := palette.ParseAction(palette.CommandResults([]command.Command{cmd}), query)
430 if !ok {
431 return nil
432 }
433 return action.Args
434}
435
436func (ac *attachedClient) clearPaletteLocked() {
437 ac.overlays.paletteGeneration++
438 ac.overlays.palette = nil
439 ac.overlays.paletteRouteSnapshot = ports.RecentRouteSnapshot{}
440 ac.overlays.paletteHints = palette.ContextualHints{}
441 ac.overlays.paletteFeedback = ""
442 ac.overlays.palettePending = nil
443}
444
445func (ac *attachedClient) paletteFailure(generation uint64, rawQuery, feedback string) {
446 ac.overlays.paletteMu.Lock()
447 defer ac.overlays.paletteMu.Unlock()
448 if ac.overlays.palette == nil || ac.overlays.paletteGeneration != generation || ac.overlays.palette.Query() != rawQuery {
449 return
450 }
451 ac.overlays.paletteFeedback = feedback
452}
453
454func (ac *attachedClient) closeExecutedPalette(generation uint64, rawQuery string) bool {
455 ac.overlays.paletteMu.Lock()
456 defer ac.overlays.paletteMu.Unlock()
457 if ac.overlays.palette == nil || ac.overlays.paletteGeneration != generation || ac.overlays.palette.Query() != rawQuery {
458 return false
459 }
460 ac.clearPaletteLocked()
461 return true
462}
463
464type paletteExec struct {
465 d *Daemon
466 sess *session
467 attachment *session
468 ac *attachedClient
469 routeSnapshot ports.RecentRouteSnapshot
470 actions daemonActionRunner
471 effect *attachmentEffectTicket
472 redrawClosedPalette bool
473}
474
475func (e paletteExec) runAction(request daemonActionRequest) error {
476 request.effect = e.effect
477 if request.target.session == nil {
478 request.target = resolveDaemonActionTargetForAttachment(e.sess, e.ac)
479 }
480 runner := e.actions
481 if runner == nil {
482 runner = daemonActions{d: e.d}
483 }
484 err := runner.Run(request)
485 if errors.Is(err, errDaemonActionNoChange) {
486 if e.redrawClosedPalette {
487 e.d.invalidateRender(e.sess, e.ac, true, "palette.go")
488 }
489 return nil
490 }
491 if err != nil {
492 return err
493 }
494 if e.actions == nil {
495 finishDaemonActionForClient(e.d, request, e.ac, "palette.go")
496 }
497 return nil
498}
499
500func (e paletteExec) CreateTab() error {
501 return e.runAction(daemonActionRequest{kind: daemonActionCreateTab, viewport: e.sess.fullViewportSize()})
502}
503
504func (e paletteExec) CreateSession() error {
505 entry := e.attachment
506 if entry == nil {
507 entry = e.sess
508 }
509 e.d.enterTransitionPrompt(entry, e.ac, " Create session ", "", func(name string, token attachmentConnectionToken) error {
510 if token.ac == nil {
511 if e.sess == nil {
512 return errAttachmentTransition
513 }
514 return e.d.createSessionAndSwitch(e.sess, e.ac, name)
515 }
516 return e.d.createSessionAndSwitchForAttachment(token, name)
517 })
518 return nil
519}
520
521func (e paletteExec) CloseTab() error {
522 return e.runAction(daemonActionRequest{kind: daemonActionCloseTab})
523}
524
525func (e paletteExec) split(direction layout.Direction) error {
526 return e.runAction(daemonActionRequest{kind: daemonActionSplitPane, direction: direction})
527}
528
529func (e paletteExec) SplitRight() error { return e.split(layout.Right) }
530func (e paletteExec) SplitLeft() error { return e.split(layout.Left) }
531func (e paletteExec) SplitUp() error { return e.split(layout.Up) }
532func (e paletteExec) SplitDown() error { return e.split(layout.Down) }
533func (e paletteExec) consumeOrExpelPane(direction layout.Direction) error {
534 return e.runAction(daemonActionRequest{kind: daemonActionConsumeOrExpelPane, direction: direction})
535}
536func (e paletteExec) ConsumeOrExpelPaneLeft() error {
537 return e.consumeOrExpelPane(layout.Left)
538}
539func (e paletteExec) ConsumeOrExpelPaneRight() error {
540 return e.consumeOrExpelPane(layout.Right)
541}
542
543func (e paletteExec) StackPane() error {
544 return e.runAction(daemonActionRequest{kind: daemonActionStackPane})
545}
546
547func (e paletteExec) ToggleStack() error {
548 return e.runAction(daemonActionRequest{kind: daemonActionToggleStack})
549}
550
551func (e paletteExec) ClosePane() error {
552 return e.runAction(daemonActionRequest{kind: daemonActionClosePane})
553}
554
555func (e paletteExec) OpenMovePanePicker() error {
556 target := resolveDaemonActionTargetForAttachment(e.sess, e.ac)
557 if target.tab == nil || target.pane == nil {
558 return errMovePaneInvalid
559 }
560 return e.d.enterPickerForIntent(e.sess, e.ac, pickerMovePane, moveSourceLocator{
561 Session: sessionMoveLocator(e.sess),
562 TabID: domain.TabStableID(target.tab.stableID),
563 PaneID: domain.PaneStableID(target.pane.stableID),
564 Attachment: e.ac,
565 })
566}
567
568func (e paletteExec) OpenMoveTabPicker() error {
569 target := resolveDaemonActionTargetForAttachment(e.sess, e.ac)
570 if target.tab == nil {
571 return errMovePaneInvalid
572 }
573 return e.d.enterPickerForIntent(e.sess, e.ac, pickerMoveTab, moveSourceLocator{
574 Session: sessionMoveLocator(e.sess),
575 TabID: domain.TabStableID(target.tab.stableID),
576 Attachment: e.ac,
577 })
578}
579
580func (e paletteExec) focus(direction layout.Direction) error {
581 return e.runAction(daemonActionRequest{kind: daemonActionFocusPane, direction: direction})
582}
583
584func (e paletteExec) FocusPaneLeft() error { return e.focus(layout.Left) }
585func (e paletteExec) FocusPaneRight() error { return e.focus(layout.Right) }
586func (e paletteExec) FocusPaneUp() error { return e.focus(layout.Up) }
587func (e paletteExec) FocusPaneDown() error { return e.focus(layout.Down) }
588
589func (e paletteExec) EnterResizeMode() error { return e.d.enterResizeMode(e.sess, e.ac) }
590func (e paletteExec) resize(axis layout.Axis, delta int) error {
591 return resizeUserError(e.runAction(daemonActionRequest{kind: daemonActionResizePane, axis: axis, delta: delta}))
592}
593func (e paletteExec) GrowPaneWidth() error { return e.resize(layout.Width, resizeStepCols) }
594func (e paletteExec) ShrinkPaneWidth() error { return e.resize(layout.Width, -resizeStepCols) }
595func (e paletteExec) GrowPaneHeight() error { return e.resize(layout.Height, resizeStepRows) }
596func (e paletteExec) ShrinkPaneHeight() error { return e.resize(layout.Height, -resizeStepRows) }
597func (e paletteExec) EqualizePanes() error {
598 return resizeUserError(e.runAction(daemonActionRequest{kind: daemonActionEqualizePanes}))
599}
600
601func (e paletteExec) NextTab() error {
602 return e.runAction(daemonActionRequest{kind: daemonActionNextTab})
603}
604
605func (e paletteExec) PrevTab() error {
606 return e.runAction(daemonActionRequest{kind: daemonActionPreviousTab})
607}
608
609func (e paletteExec) ToggleFloatingPane() error {
610 return e.d.toggleFloating(e.sess, e.ac)
611}
612
613func (e paletteExec) BackSession() error {
614 if e.effect != nil {
615 return e.d.backSessionForAttachment(e.effect.connectionToken())
616 }
617 e.d.backSession(e.sess, e.ac)
618 return nil
619}
620
621func (e paletteExec) Detach() error {
622 if e.effect != nil {
623 if !e.d.clientGoneForAttachment(e.effect.connectionToken(), true) {
624 return errAttachmentTransition
625 }
626 return nil
627 }
628 e.d.clientGone(e.sess, e.ac, e.ac.transport(), true)
629 return nil
630}
631
632func (e paletteExec) EnterVisualMode() error {
633 e.d.enterCopyMode(e.sess, e.ac)
634 return nil
635}
636
637func (e paletteExec) RenameSession() error {
638 e.sess.mu.Lock()
639 currentName := e.sess.name
640 e.sess.mu.Unlock()
641 e.d.enterPrompt(e.sess, e.ac, " Rename session ", currentName, func(name string) error {
642 return e.runAction(daemonActionRequest{kind: daemonActionRenameSession, name: name})
643 })
644 return nil
645}
646
647func (e paletteExec) RenameTab() error {
648 tb := e.sess.tabForAttachment(e.ac)
649 if e.ac == nil {
650 tb = e.sess.firstTab()
651 }
652 if tb == nil {
653 return nil
654 }
655 e.sess.mu.Lock()
656 index := 0
657 for i, candidate := range e.sess.tabs {
658 if candidate == tb {
659 index = i
660 break
661 }
662 }
663 currentName := tabDisplayName(tb, index)
664 e.sess.mu.Unlock()
665 e.d.enterPrompt(e.sess, e.ac, " Rename tab ", currentName, func(name string) error {
666 return e.runAction(daemonActionRequest{kind: daemonActionRenameTab, target: daemonActionTarget{session: e.sess, tab: tb}, name: name})
667 })
668 return nil
669}
670
671func (e paletteExec) OpenSessionPicker() error {
672 if e.ac != nil && e.ac.navigationCapabilities&ports.NavigationCapabilityHomePicker != 0 && e.effect != nil {
673 return e.d.sendNavigationActionForAttachment(e.effect.connectionToken(), ports.NavigationOpenHomePicker)
674 }
675 e.d.enterPicker(e.sess, e.ac)
676 return nil
677}
678
679func (e paletteExec) OpenNotifications() error {
680 e.d.enterNotices(e.sess, e.ac)
681 return nil
682}
683
684func (e paletteExec) YankLastNotification() error {
685 entry := e.attachment
686 if entry == nil {
687 entry = e.sess
688 }
689 n, ok := e.d.notices.latest()
690 if !ok {
691 // Reported directly rather than returned: the generic cmd.Run error
692 // path only logs, so a returned error here would never reach the
693 // user as the warn toast this no-op is supposed to produce.
694 e.d.reportAttachmentError(entry, domain.UserWarn(domain.NoticeClipboard, "no notifications yet", nil))
695 return nil
696 }
697 e.d.yankNotice(entry, e.ac, n)
698 return nil
699}
700
701func (e paletteExec) JumpRecentSession(rank int) error {
702 if e.routeSnapshot.Generation == 0 || rank < 1 || rank > len(e.routeSnapshot.Entries) {
703 return command.ErrInvalidArguments
704 }
705 entry := e.routeSnapshot.Entries[rank-1]
706 return e.NavigateRecentRoute(ports.RouteNavigationAction{
707 SnapshotGeneration: e.routeSnapshot.Generation,
708 Key: entry.Key,
709 Generation: entry.Generation,
710 })
711}
712
713func (e paletteExec) NavigateRecentRoute(action ports.RouteNavigationAction) error {
714 if e.d.beforeRecentSessionHandoff != nil {
715 e.d.beforeRecentSessionHandoff()
716 }
717 if e.effect != nil {
718 return e.d.sendRecentRouteNavigationActionForAttachment(e.effect.connectionToken(), action)
719 }
720 if e.ac == nil || e.attachment == nil {
721 return errAttachmentTransition
722 }
723 return e.d.sendRecentRouteNavigationActionForAttachment(e.attachment.attachmentToken(e.ac, e.ac.transport()), action)
724}
725
726func composePaletteClientFrame(model *palette.Model, base renderer.Frame, cfg domain.PaletteConfig, guidance string, styles ...themeui.Styles) (renderer.Frame, []renderer.Damage) {
727 styleSet := resolveStyles(styles)
728 modal := paletteModalFor(domain.Size{Cols: base.Width, Rows: base.Height}, cfg)
729 return composeModalClientFrame(base, modal, styleSet, func(size domain.Size) renderer.Frame {
730 return model.Render(size, palette.RenderOptions{Styles: palette.RenderStyles{Base: styleSet.PickerBase, Row: styleSet.PickerBase, Selection: styleSet.PickerSelection, Description: styleSet.PickerDescription, SelectionDescription: styleSet.PickerSelectionMuted}, Guidance: guidance})
731 })
732}