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