editor.go

  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}