1package daemon
2
3import (
4 "errors"
5 "strings"
6
7 "github.com/bnema/vev/internal/domain"
8 promptui "github.com/bnema/vev/internal/usecase/prompt"
9 "github.com/bnema/vev/internal/usecase/ui"
10)
11
12func promptModalFor(title string) ui.Modal {
13 return ui.Modal{WidthPct: 100, MinWidth: 32, FixedHeight: 4, Title: title, Anchor: domain.AnchorBottom, Margins: ui.Margins{Bottom: 1}}
14}
15
16func (d *Daemon) enterPrompt(sess *session, ac *attachedClient, title, initial string, submit func(string) error) {
17 d.closePrompt(ac)
18 ac.overlays.promptMu.Lock()
19 ac.overlays.prompt = promptui.New(title, initial)
20 ac.overlays.promptSubmit = submit
21 ac.overlays.promptPending = nil
22 ac.overlays.promptMu.Unlock()
23 d.invalidateRender(sess, ac, true, "prompt.go")
24}
25
26func (d *Daemon) enterTransitionPrompt(sess *session, ac *attachedClient, title, initial string, submit func(string, *attachmentEffect) error) {
27 d.closePrompt(ac)
28 ac.overlays.promptMu.Lock()
29 ac.overlays.prompt = promptui.New(title, initial)
30 ac.overlays.promptTransitionSubmit = submit
31 ac.overlays.promptPending = nil
32 ac.overlays.promptMu.Unlock()
33 d.invalidateRender(sess, ac, true, "prompt.go")
34}
35
36func (d *Daemon) closePrompt(ac *attachedClient) {
37 ac.overlays.promptMu.Lock()
38 ac.overlays.prompt = nil
39 ac.overlays.promptSubmit = nil
40 ac.overlays.promptTransitionSubmit = nil
41 ac.overlays.promptPending = nil
42 ac.overlays.promptMu.Unlock()
43}
44
45// promptValidationError identifies expected prompt feedback that should stay
46// inline rather than creating a history entry or toast.
47func promptValidationError(err error) bool {
48 return errors.Is(err, domain.ErrInvalidSessionName) ||
49 errors.Is(err, errSessionNameRequired) ||
50 errors.Is(err, errSessionNameInUse)
51}
52
53func (d *Daemon) handlePromptInput(ac *attachedClient, data []byte, effects ...*attachmentEffect) {
54 sess := ac.currentAttachmentSession()
55 if sess == nil {
56 return
57 }
58
59 ac.overlays.promptMu.Lock()
60 if ac.overlays.prompt == nil {
61 ac.overlays.promptPending = nil
62 ac.overlays.promptMu.Unlock()
63 return
64 }
65
66 changed := false
67 exit := false
68 var submit func(string) error
69 var transitionSubmit func(string, *attachmentEffect) error
70 var submittedPrompt *promptui.Model
71 var value string
72
73 routeOverlayBytes(data, &ac.overlays.promptPending, overlayEvents{
74 rune: func(r rune) {
75 ac.overlays.prompt.Insert(r)
76 changed = true
77 },
78 backspace: func() {
79 ac.overlays.prompt.Backspace()
80 changed = true
81 },
82 enter: func() {
83 submit = ac.overlays.promptSubmit
84 transitionSubmit = ac.overlays.promptTransitionSubmit
85 submittedPrompt = ac.overlays.prompt
86 value = strings.TrimSpace(ac.overlays.prompt.Value())
87 },
88 cancel: func() { exit = true },
89 up: func() {},
90 down: func() {},
91 })
92 ac.overlays.promptMu.Unlock()
93
94 if submit != nil || transitionSubmit != nil {
95 var err error
96 if transitionSubmit != nil {
97 var effect *attachmentEffect
98 if len(effects) != 0 {
99 effect = effects[0]
100 }
101 err = transitionSubmit(value, effect)
102 } else {
103 err = submit(value)
104 }
105 if errors.Is(err, errAttachmentTransition) {
106 return
107 }
108 if err != nil {
109 ac.overlays.promptMu.Lock()
110 if ac.overlays.prompt == submittedPrompt {
111 ac.overlays.prompt.SetError(err.Error())
112 }
113 ac.overlays.promptMu.Unlock()
114 if !promptValidationError(err) {
115 d.reportAttachmentError(sess, err)
116 }
117 d.invalidateRender(sess, ac, true, "prompt.go")
118 return
119 }
120 d.closePrompt(ac)
121 if current := ac.currentAttachmentSession(); current != nil {
122 d.invalidateRender(current, ac, true, "prompt.go")
123 }
124 return
125 }
126 if exit {
127 d.closePrompt(ac)
128 d.invalidateRender(sess, ac, true, "prompt.go")
129 return
130 }
131 if changed {
132 d.invalidateRender(sess, ac, true, "prompt.go")
133 }
134}