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
 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}