1package model
2
3import (
4 "math/rand"
5
6 "github.com/charmbracelet/bubbles/v2/key"
7 "github.com/charmbracelet/bubbles/v2/textarea"
8 tea "github.com/charmbracelet/bubbletea/v2"
9 "github.com/charmbracelet/crush/internal/app"
10 "github.com/charmbracelet/crush/internal/ui/common"
11)
12
13type EditorKeyMap struct {
14 AddFile key.Binding
15 SendMessage key.Binding
16 OpenEditor key.Binding
17 Newline key.Binding
18
19 // Attachments key maps
20 AttachmentDeleteMode key.Binding
21 Escape key.Binding
22 DeleteAllAttachments key.Binding
23}
24
25func DefaultEditorKeyMap() EditorKeyMap {
26 return EditorKeyMap{
27 AddFile: key.NewBinding(
28 key.WithKeys("/"),
29 key.WithHelp("/", "add file"),
30 ),
31 SendMessage: key.NewBinding(
32 key.WithKeys("enter"),
33 key.WithHelp("enter", "send"),
34 ),
35 OpenEditor: key.NewBinding(
36 key.WithKeys("ctrl+o"),
37 key.WithHelp("ctrl+o", "open editor"),
38 ),
39 Newline: key.NewBinding(
40 key.WithKeys("shift+enter", "ctrl+j"),
41 // "ctrl+j" is a common keybinding for newline in many editors. If
42 // the terminal supports "shift+enter", we substitute the help text
43 // to reflect that.
44 key.WithHelp("ctrl+j", "newline"),
45 ),
46 AttachmentDeleteMode: key.NewBinding(
47 key.WithKeys("ctrl+r"),
48 key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
49 ),
50 Escape: key.NewBinding(
51 key.WithKeys("esc", "alt+esc"),
52 key.WithHelp("esc", "cancel delete mode"),
53 ),
54 DeleteAllAttachments: key.NewBinding(
55 key.WithKeys("r"),
56 key.WithHelp("ctrl+r+r", "delete all attachments"),
57 ),
58 }
59}
60
61// EditorModel represents the editor UI model.
62type EditorModel struct {
63 com *common.Common
64 app *app.App
65
66 keyMap EditorKeyMap
67 textarea *textarea.Model
68
69 attachments []any // TODO: Implement attachments
70
71 readyPlaceholder string
72 workingPlaceholder string
73}
74
75// NewEditorModel creates a new instance of EditorModel.
76func NewEditorModel(com *common.Common, app *app.App) *EditorModel {
77 ta := textarea.New()
78 ta.SetStyles(com.Styles.TextArea)
79 ta.ShowLineNumbers = false
80 ta.CharLimit = -1
81 ta.SetVirtualCursor(false)
82 ta.Focus()
83 e := &EditorModel{
84 com: com,
85 app: app,
86 keyMap: DefaultEditorKeyMap(),
87 textarea: ta,
88 }
89
90 e.setEditorPrompt()
91 e.randomizePlaceholders()
92 e.textarea.Placeholder = e.readyPlaceholder
93
94 return e
95}
96
97// Init initializes the editor model.
98func (m *EditorModel) Init() tea.Cmd {
99 return nil
100}
101
102// Update handles updates to the editor model.
103func (m *EditorModel) Update(msg tea.Msg) (*EditorModel, tea.Cmd) {
104 var cmds []tea.Cmd
105 var cmd tea.Cmd
106
107 m.textarea, cmd = m.textarea.Update(msg)
108 cmds = append(cmds, cmd)
109
110 // Textarea placeholder logic
111 if m.app.AgentCoordinator != nil && m.app.AgentCoordinator.IsBusy() {
112 m.textarea.Placeholder = m.workingPlaceholder
113 } else {
114 m.textarea.Placeholder = m.readyPlaceholder
115 }
116 if m.app.Permissions.SkipRequests() {
117 m.textarea.Placeholder = "Yolo mode!"
118 }
119
120 // TODO: Add attachments
121
122 return m, tea.Batch(cmds...)
123}
124
125// View renders the editor model.
126func (m *EditorModel) View() string {
127 return m.textarea.View()
128}
129
130// ShortHelp returns the short help view for the editor model.
131func (m *EditorModel) ShortHelp() []key.Binding {
132 k := m.keyMap
133 binds := []key.Binding{
134 k.AddFile,
135 k.SendMessage,
136 k.OpenEditor,
137 k.Newline,
138 }
139
140 if len(m.attachments) > 0 {
141 binds = append(binds,
142 k.AttachmentDeleteMode,
143 k.DeleteAllAttachments,
144 k.Escape,
145 )
146 }
147
148 return binds
149}
150
151// FullHelp returns the full help view for the editor model.
152func (m *EditorModel) FullHelp() [][]key.Binding {
153 return [][]key.Binding{
154 m.ShortHelp(),
155 }
156}
157
158// Cursor returns the relative cursor position of the editor.
159func (m *EditorModel) Cursor() *tea.Cursor {
160 return m.textarea.Cursor()
161}
162
163// Blur implements Container.
164func (m *EditorModel) Blur() tea.Cmd {
165 m.textarea.Blur()
166 return nil
167}
168
169// Focus implements Container.
170func (m *EditorModel) Focus() tea.Cmd {
171 return m.textarea.Focus()
172}
173
174// Focused returns whether the editor is focused.
175func (m *EditorModel) Focused() bool {
176 return m.textarea.Focused()
177}
178
179// SetSize sets the size of the editor.
180func (m *EditorModel) SetSize(width, height int) {
181 m.textarea.SetWidth(width)
182 m.textarea.SetHeight(height)
183}
184
185func (m *EditorModel) setEditorPrompt() {
186 if m.app.Permissions.SkipRequests() {
187 m.textarea.SetPromptFunc(4, m.yoloPromptFunc)
188 return
189 }
190 m.textarea.SetPromptFunc(4, m.normalPromptFunc)
191}
192
193func (m *EditorModel) normalPromptFunc(info textarea.PromptInfo) string {
194 t := m.com.Styles
195 if info.LineNumber == 0 {
196 return " > "
197 }
198 if info.Focused {
199 return t.EditorPromptNormalFocused.Render()
200 }
201 return t.EditorPromptNormalBlurred.Render()
202}
203
204func (m *EditorModel) yoloPromptFunc(info textarea.PromptInfo) string {
205 t := m.com.Styles
206 if info.LineNumber == 0 {
207 if info.Focused {
208 return t.EditorPromptYoloIconFocused.Render()
209 } else {
210 return t.EditorPromptYoloIconBlurred.Render()
211 }
212 }
213 if info.Focused {
214 return t.EditorPromptYoloDotsFocused.Render()
215 }
216 return t.EditorPromptYoloDotsBlurred.Render()
217}
218
219var readyPlaceholders = [...]string{
220 "Ready!",
221 "Ready...",
222 "Ready?",
223 "Ready for instructions",
224}
225
226var workingPlaceholders = [...]string{
227 "Working!",
228 "Working...",
229 "Brrrrr...",
230 "Prrrrrrrr...",
231 "Processing...",
232 "Thinking...",
233}
234
235func (m *EditorModel) randomizePlaceholders() {
236 m.workingPlaceholder = workingPlaceholders[rand.Intn(len(workingPlaceholders))]
237 m.readyPlaceholder = readyPlaceholders[rand.Intn(len(readyPlaceholders))]
238}