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