1package model
2
3import (
4 "context"
5 "fmt"
6 "image"
7 "math/rand"
8 "os"
9 "slices"
10 "strings"
11
12 "charm.land/bubbles/v2/help"
13 "charm.land/bubbles/v2/key"
14 "charm.land/bubbles/v2/textarea"
15 tea "charm.land/bubbletea/v2"
16 "charm.land/lipgloss/v2"
17 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
18 "github.com/charmbracelet/crush/internal/app"
19 "github.com/charmbracelet/crush/internal/config"
20 "github.com/charmbracelet/crush/internal/history"
21 "github.com/charmbracelet/crush/internal/pubsub"
22 "github.com/charmbracelet/crush/internal/session"
23 "github.com/charmbracelet/crush/internal/ui/common"
24 "github.com/charmbracelet/crush/internal/ui/dialog"
25 "github.com/charmbracelet/crush/internal/ui/list"
26 "github.com/charmbracelet/crush/internal/ui/logo"
27 "github.com/charmbracelet/crush/internal/ui/styles"
28 "github.com/charmbracelet/crush/internal/version"
29 uv "github.com/charmbracelet/ultraviolet"
30 "github.com/charmbracelet/ultraviolet/screen"
31 "github.com/charmbracelet/x/ansi"
32)
33
34// uiFocusState represents the current focus state of the UI.
35type uiFocusState uint8
36
37// Possible uiFocusState values.
38const (
39 uiFocusNone uiFocusState = iota
40 uiFocusEditor
41 uiFocusMain
42)
43
44type uiState uint8
45
46// Possible uiState values.
47const (
48 uiConfigure uiState = iota
49 uiInitialize
50 uiLanding
51 uiChat
52 uiChatCompact
53)
54
55type sessionLoadedMsg struct {
56 sess session.Session
57}
58
59type sessionFilesLoadedMsg struct {
60 files []SessionFile
61}
62
63// UI represents the main user interface model.
64type UI struct {
65 com *common.Common
66 session *session.Session
67 sessionFiles []SessionFile
68
69 // The width and height of the terminal in cells.
70 width int
71 height int
72 layout layout
73
74 focus uiFocusState
75 state uiState
76
77 keyMap KeyMap
78 keyenh tea.KeyboardEnhancementsMsg
79
80 chat *list.List
81 dialog *dialog.Overlay
82 help help.Model
83
84 // header is the last cached header logo
85 header string
86
87 // sendProgressBar instructs the TUI to send progress bar updates to the
88 // terminal.
89 sendProgressBar bool
90
91 // QueryVersion instructs the TUI to query for the terminal version when it
92 // starts.
93 QueryVersion bool
94
95 // Editor components
96 textarea textarea.Model
97
98 attachments []any // TODO: Implement attachments
99
100 readyPlaceholder string
101 workingPlaceholder string
102
103 // onboarding state
104 onboarding struct {
105 yesInitializeSelected bool
106 }
107
108 // lsp
109 lspStates map[string]app.LSPClientInfo
110
111 // mcp
112 mcpStates map[string]mcp.ClientInfo
113
114 // sidebarLogo keeps a cached version of the sidebar sidebarLogo.
115 sidebarLogo string
116}
117
118// New creates a new instance of the [UI] model.
119func New(com *common.Common) *UI {
120 // Editor components
121 ta := textarea.New()
122 ta.SetStyles(com.Styles.TextArea)
123 ta.ShowLineNumbers = false
124 ta.CharLimit = -1
125 ta.SetVirtualCursor(false)
126 ta.Focus()
127
128 l := list.New()
129
130 ui := &UI{
131 com: com,
132 dialog: dialog.NewOverlay(),
133 keyMap: DefaultKeyMap(),
134 help: help.New(),
135 focus: uiFocusNone,
136 state: uiConfigure,
137 textarea: ta,
138 chat: l,
139 }
140
141 // set onboarding state defaults
142 ui.onboarding.yesInitializeSelected = true
143
144 // If no provider is configured show the user the provider list
145 if !com.Config().IsConfigured() {
146 ui.state = uiConfigure
147 // if the project needs initialization show the user the question
148 } else if n, _ := config.ProjectNeedsInitialization(); n {
149 ui.state = uiInitialize
150 // otherwise go to the landing UI
151 } else {
152 ui.state = uiLanding
153 ui.focus = uiFocusEditor
154 }
155
156 ui.setEditorPrompt()
157 ui.randomizePlaceholders()
158 ui.textarea.Placeholder = ui.readyPlaceholder
159 ui.help.Styles = com.Styles.Help
160
161 return ui
162}
163
164// Init initializes the UI model.
165func (m *UI) Init() tea.Cmd {
166 if m.QueryVersion {
167 return tea.RequestTerminalVersion
168 }
169 return nil
170}
171
172// Update handles updates to the UI model.
173func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
174 var cmds []tea.Cmd
175 switch msg := msg.(type) {
176 case tea.EnvMsg:
177 // Is this Windows Terminal?
178 if !m.sendProgressBar {
179 m.sendProgressBar = slices.Contains(msg, "WT_SESSION")
180 }
181 case sessionLoadedMsg:
182 m.state = uiChat
183 m.session = &msg.sess
184 case sessionFilesLoadedMsg:
185 m.sessionFiles = msg.files
186 case pubsub.Event[history.File]:
187 cmds = append(cmds, m.handleFileEvent(msg.Payload))
188 case pubsub.Event[app.LSPEvent]:
189 m.lspStates = app.GetLSPStates()
190 case pubsub.Event[mcp.Event]:
191 m.mcpStates = mcp.GetStates()
192 case tea.TerminalVersionMsg:
193 termVersion := strings.ToLower(msg.Name)
194 // Only enable progress bar for the following terminals.
195 if !m.sendProgressBar {
196 m.sendProgressBar = strings.Contains(termVersion, "ghostty")
197 }
198 return m, nil
199 case tea.WindowSizeMsg:
200 m.width, m.height = msg.Width, msg.Height
201 m.updateLayoutAndSize()
202 m.chat.ScrollToBottom()
203 case tea.KeyboardEnhancementsMsg:
204 m.keyenh = msg
205 if msg.SupportsKeyDisambiguation() {
206 m.keyMap.Models.SetHelp("ctrl+m", "models")
207 m.keyMap.Editor.Newline.SetHelp("shift+enter", "newline")
208 }
209 case tea.KeyPressMsg:
210 cmds = append(cmds, m.handleKeyPressMsg(msg)...)
211 }
212
213 // This logic gets triggered on any message type, but should it?
214 switch m.focus {
215 case uiFocusMain:
216 case uiFocusEditor:
217 // Textarea placeholder logic
218 if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
219 m.textarea.Placeholder = m.workingPlaceholder
220 } else {
221 m.textarea.Placeholder = m.readyPlaceholder
222 }
223 if m.com.App.Permissions.SkipRequests() {
224 m.textarea.Placeholder = "Yolo mode!"
225 }
226 }
227
228 return m, tea.Batch(cmds...)
229}
230
231func (m *UI) loadSession(sessionID string) tea.Cmd {
232 return func() tea.Msg {
233 // TODO: handle error
234 session, _ := m.com.App.Sessions.Get(context.Background(), sessionID)
235 return sessionLoadedMsg{session}
236 }
237}
238
239func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
240 if m.dialog.HasDialogs() {
241 return m.updateDialogs(msg)
242 }
243
244 switch {
245 case msg.String() == "ctrl+shift+t":
246 m.chat.SelectPrev()
247 case msg.String() == "ctrl+t":
248 m.focus = uiFocusMain
249 m.state = uiChat
250 if m.chat.Len() > 0 {
251 m.chat.AppendItem(list.Gap)
252 }
253 m.chat.AppendItem(
254 list.NewStringItem(
255 fmt.Sprintf("%d", m.chat.Len()),
256 fmt.Sprintf("Welcome to Crush Chat! %d", rand.Intn(1000)),
257 ).WithFocusStyles(&m.com.Styles.BorderFocus, &m.com.Styles.BorderBlur),
258 )
259 m.chat.SetSelectedIndex(m.chat.Len() - 1)
260 m.chat.Focus()
261 m.chat.ScrollToBottom()
262 case key.Matches(msg, m.keyMap.Tab):
263 switch m.state {
264 case uiChat:
265 if m.focus == uiFocusMain {
266 m.focus = uiFocusEditor
267 cmds = append(cmds, m.textarea.Focus())
268 } else {
269 m.focus = uiFocusMain
270 m.textarea.Blur()
271 }
272 }
273 case key.Matches(msg, m.keyMap.Help):
274 m.help.ShowAll = !m.help.ShowAll
275 m.updateLayoutAndSize()
276 return cmds
277 case key.Matches(msg, m.keyMap.Quit):
278 if !m.dialog.ContainsDialog(dialog.QuitDialogID) {
279 m.dialog.AddDialog(dialog.NewQuit(m.com))
280 return
281 }
282 return cmds
283 case key.Matches(msg, m.keyMap.Commands):
284 // TODO: Implement me
285 return cmds
286 case key.Matches(msg, m.keyMap.Models):
287 // TODO: Implement me
288 return cmds
289 case key.Matches(msg, m.keyMap.Sessions):
290 // TODO: Implement me
291 return cmds
292 }
293
294 cmds = append(cmds, m.updateFocused(msg)...)
295 return cmds
296}
297
298// Draw implements [tea.Layer] and draws the UI model.
299func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) {
300 layout := generateLayout(m, area.Dx(), area.Dy())
301
302 // Update cached layout and component sizes if needed.
303 if m.layout != layout {
304 m.layout = layout
305 m.updateSize()
306 }
307
308 // Clear the screen first
309 screen.Clear(scr)
310
311 switch m.state {
312 case uiConfigure:
313 header := uv.NewStyledString(m.header)
314 header.Draw(scr, layout.header)
315
316 mainView := lipgloss.NewStyle().Width(layout.main.Dx()).
317 Height(layout.main.Dy()).
318 Background(lipgloss.ANSIColor(rand.Intn(256))).
319 Render(" Configure ")
320 main := uv.NewStyledString(mainView)
321 main.Draw(scr, layout.main)
322
323 case uiInitialize:
324 header := uv.NewStyledString(m.header)
325 header.Draw(scr, layout.header)
326
327 main := uv.NewStyledString(m.initializeView())
328 main.Draw(scr, layout.main)
329
330 case uiLanding:
331 header := uv.NewStyledString(m.header)
332 header.Draw(scr, layout.header)
333 main := uv.NewStyledString(m.landingView())
334 main.Draw(scr, layout.main)
335
336 editor := uv.NewStyledString(m.textarea.View())
337 editor.Draw(scr, layout.editor)
338
339 case uiChat:
340 header := uv.NewStyledString(m.header)
341 header.Draw(scr, layout.header)
342 m.drawSidebar(scr, layout.sidebar)
343
344 m.chat.Draw(scr, layout.main)
345
346 editor := uv.NewStyledString(m.textarea.View())
347 editor.Draw(scr, layout.editor)
348
349 case uiChatCompact:
350 header := uv.NewStyledString(m.header)
351 header.Draw(scr, layout.header)
352
353 mainView := lipgloss.NewStyle().Width(layout.main.Dx()).
354 Height(layout.main.Dy()).
355 Background(lipgloss.ANSIColor(rand.Intn(256))).
356 Render(" Compact Chat Messages ")
357 main := uv.NewStyledString(mainView)
358 main.Draw(scr, layout.main)
359
360 editor := uv.NewStyledString(m.textarea.View())
361 editor.Draw(scr, layout.editor)
362 }
363
364 // Add help layer
365 help := uv.NewStyledString(m.help.View(m))
366 help.Draw(scr, layout.help)
367
368 // Debugging rendering (visually see when the tui rerenders)
369 if os.Getenv("CRUSH_UI_DEBUG") == "true" {
370 debugView := lipgloss.NewStyle().Background(lipgloss.ANSIColor(rand.Intn(256))).Width(4).Height(2)
371 debug := uv.NewStyledString(debugView.String())
372 debug.Draw(scr, image.Rectangle{
373 Min: image.Pt(4, 1),
374 Max: image.Pt(8, 3),
375 })
376 }
377
378 // This needs to come last to overlay on top of everything
379 if m.dialog.HasDialogs() {
380 if dialogView := m.dialog.View(); dialogView != "" {
381 dialogWidth, dialogHeight := lipgloss.Width(dialogView), lipgloss.Height(dialogView)
382 dialogArea := common.CenterRect(area, dialogWidth, dialogHeight)
383 dialog := uv.NewStyledString(dialogView)
384 dialog.Draw(scr, dialogArea)
385 }
386 }
387}
388
389// View renders the UI model's view.
390func (m *UI) View() tea.View {
391 var v tea.View
392 v.AltScreen = true
393 v.BackgroundColor = m.com.Styles.Background
394
395 layout := generateLayout(m, m.width, m.height)
396 if m.focus == uiFocusEditor && m.textarea.Focused() {
397 cur := m.textarea.Cursor()
398 cur.X++ // Adjust for app margins
399 cur.Y += layout.editor.Min.Y
400 v.Cursor = cur
401 }
402
403 // TODO: Switch to lipgloss.Canvas when available
404 canvas := uv.NewScreenBuffer(m.width, m.height)
405 canvas.Method = ansi.GraphemeWidth
406
407 m.Draw(canvas, canvas.Bounds())
408
409 content := strings.ReplaceAll(canvas.Render(), "\r\n", "\n") // normalize newlines
410 contentLines := strings.Split(content, "\n")
411 for i, line := range contentLines {
412 // Trim trailing spaces for concise rendering
413 contentLines[i] = strings.TrimRight(line, " ")
414 }
415
416 content = strings.Join(contentLines, "\n")
417 v.Content = content
418 if m.sendProgressBar && m.com.App != nil && m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
419 // HACK: use a random percentage to prevent ghostty from hiding it
420 // after a timeout.
421 v.ProgressBar = tea.NewProgressBar(tea.ProgressBarIndeterminate, rand.Intn(100))
422 }
423
424 return v
425}
426
427// ShortHelp implements [help.KeyMap].
428func (m *UI) ShortHelp() []key.Binding {
429 var binds []key.Binding
430 k := &m.keyMap
431
432 switch m.state {
433 case uiInitialize:
434 binds = append(binds, k.Quit)
435 default:
436 // TODO: other states
437 // if m.session == nil {
438 // no session selected
439 binds = append(binds,
440 k.Commands,
441 k.Models,
442 k.Editor.Newline,
443 k.Quit,
444 k.Help,
445 )
446 // }
447 // else {
448 // we have a session
449 // }
450
451 // switch m.state {
452 // case uiChat:
453 // case uiEdit:
454 // binds = append(binds,
455 // k.Editor.AddFile,
456 // k.Editor.SendMessage,
457 // k.Editor.OpenEditor,
458 // k.Editor.Newline,
459 // )
460 //
461 // if len(m.attachments) > 0 {
462 // binds = append(binds,
463 // k.Editor.AttachmentDeleteMode,
464 // k.Editor.DeleteAllAttachments,
465 // k.Editor.Escape,
466 // )
467 // }
468 // }
469 }
470
471 return binds
472}
473
474// FullHelp implements [help.KeyMap].
475func (m *UI) FullHelp() [][]key.Binding {
476 var binds [][]key.Binding
477 k := &m.keyMap
478 help := k.Help
479 help.SetHelp("ctrl+g", "less")
480
481 switch m.state {
482 case uiInitialize:
483 binds = append(binds,
484 []key.Binding{
485 k.Quit,
486 })
487 default:
488 if m.session == nil {
489 // no session selected
490 binds = append(binds,
491 []key.Binding{
492 k.Commands,
493 k.Models,
494 k.Sessions,
495 },
496 []key.Binding{
497 k.Editor.Newline,
498 k.Editor.AddImage,
499 k.Editor.MentionFile,
500 k.Editor.OpenEditor,
501 },
502 []key.Binding{
503 help,
504 },
505 )
506 }
507 // else {
508 // we have a session
509 // }
510 }
511
512 // switch m.state {
513 // case uiChat:
514 // case uiEdit:
515 // binds = append(binds, m.ShortHelp())
516 // }
517
518 return binds
519}
520
521// updateDialogs updates the dialog overlay with the given message and returns cmds
522func (m *UI) updateDialogs(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
523 updatedDialog, cmd := m.dialog.Update(msg)
524 m.dialog = updatedDialog
525 cmds = append(cmds, cmd)
526 return cmds
527}
528
529// updateFocused updates the focused model (chat or editor) with the given message
530// and appends any resulting commands to the cmds slice.
531func (m *UI) updateFocused(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
532 switch m.state {
533 case uiConfigure:
534 return cmds
535 case uiInitialize:
536 return append(cmds, m.updateInitializeView(msg)...)
537 case uiChat, uiLanding, uiChatCompact:
538 switch m.focus {
539 case uiFocusMain:
540 case uiFocusEditor:
541 switch {
542 case key.Matches(msg, m.keyMap.Editor.Newline):
543 m.textarea.InsertRune('\n')
544 }
545
546 ta, cmd := m.textarea.Update(msg)
547 m.textarea = ta
548 cmds = append(cmds, cmd)
549 return cmds
550 }
551 }
552 return cmds
553}
554
555// updateLayoutAndSize updates the layout and sizes of UI components.
556func (m *UI) updateLayoutAndSize() {
557 m.layout = generateLayout(m, m.width, m.height)
558 m.updateSize()
559}
560
561// updateSize updates the sizes of UI components based on the current layout.
562func (m *UI) updateSize() {
563 // Set help width
564 m.help.SetWidth(m.layout.help.Dx())
565
566 // Handle different app states
567 switch m.state {
568 case uiConfigure, uiInitialize:
569 m.renderHeader(false, m.layout.header.Dx())
570
571 case uiLanding:
572 m.renderHeader(false, m.layout.header.Dx())
573 m.textarea.SetWidth(m.layout.editor.Dx())
574 m.textarea.SetHeight(m.layout.editor.Dy())
575
576 case uiChat:
577 m.renderSidebarLogo(m.layout.sidebar.Dx())
578 m.textarea.SetWidth(m.layout.editor.Dx())
579 m.textarea.SetHeight(m.layout.editor.Dy())
580 m.chat.SetSize(m.layout.main.Dx(), m.layout.main.Dy())
581
582 case uiChatCompact:
583 // TODO: set the width and heigh of the chat component
584 m.renderHeader(true, m.layout.header.Dx())
585 m.textarea.SetWidth(m.layout.editor.Dx())
586 m.textarea.SetHeight(m.layout.editor.Dy())
587 }
588}
589
590// generateLayout calculates the layout rectangles for all UI components based
591// on the current UI state and terminal dimensions.
592func generateLayout(m *UI, w, h int) layout {
593 // The screen area we're working with
594 area := image.Rect(0, 0, w, h)
595
596 // The help height
597 helpHeight := 1
598 // The editor height
599 editorHeight := 5
600 // The sidebar width
601 sidebarWidth := 30
602 // The header height
603 // TODO: handle compact
604 headerHeight := 4
605
606 var helpKeyMap help.KeyMap = m
607 if m.help.ShowAll {
608 for _, row := range helpKeyMap.FullHelp() {
609 helpHeight = max(helpHeight, len(row))
610 }
611 }
612
613 // Add app margins
614 appRect := area
615 appRect.Min.X += 1
616 appRect.Min.Y += 1
617 appRect.Max.X -= 1
618 appRect.Max.Y -= 1
619
620 if slices.Contains([]uiState{uiConfigure, uiInitialize, uiLanding}, m.state) {
621 // extra padding on left and right for these states
622 appRect.Min.X += 1
623 appRect.Max.X -= 1
624 }
625
626 appRect, helpRect := uv.SplitVertical(appRect, uv.Fixed(appRect.Dy()-helpHeight))
627
628 layout := layout{
629 area: area,
630 help: helpRect,
631 }
632
633 // Handle different app states
634 switch m.state {
635 case uiConfigure, uiInitialize:
636 // Layout
637 //
638 // header
639 // ------
640 // main
641 // ------
642 // help
643
644 headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(headerHeight))
645 layout.header = headerRect
646 layout.main = mainRect
647
648 case uiLanding:
649 // Layout
650 //
651 // header
652 // ------
653 // main
654 // ------
655 // editor
656 // ------
657 // help
658 headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(headerHeight))
659 mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
660 // Remove extra padding from editor (but keep it for header and main)
661 editorRect.Min.X -= 1
662 editorRect.Max.X += 1
663 layout.header = headerRect
664 layout.main = mainRect
665 layout.editor = editorRect
666
667 case uiChat:
668 // Layout
669 //
670 // ------|---
671 // main |
672 // ------| side
673 // editor|
674 // ----------
675 // help
676
677 mainRect, sideRect := uv.SplitHorizontal(appRect, uv.Fixed(appRect.Dx()-sidebarWidth))
678 // Add padding left
679 sideRect.Min.X += 1
680 mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
681 // Add bottom margin to main
682 mainRect.Max.Y -= 1
683 layout.sidebar = sideRect
684 layout.main = mainRect
685 layout.editor = editorRect
686
687 case uiChatCompact:
688 // Layout
689 //
690 // compact-header
691 // ------
692 // main
693 // ------
694 // editor
695 // ------
696 // help
697 headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(appRect.Dy()-headerHeight))
698 mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
699 layout.header = headerRect
700 layout.main = mainRect
701 layout.editor = editorRect
702 }
703
704 if !layout.editor.Empty() {
705 // Add editor margins 1 top and bottom
706 layout.editor.Min.Y += 1
707 layout.editor.Max.Y -= 1
708 }
709
710 return layout
711}
712
713// layout defines the positioning of UI elements.
714type layout struct {
715 // area is the overall available area.
716 area uv.Rectangle
717
718 // header is the header shown in special cases
719 // e.x when the sidebar is collapsed
720 // or when in the landing page
721 // or in init/config
722 header uv.Rectangle
723
724 // main is the area for the main pane. (e.x chat, configure, landing)
725 main uv.Rectangle
726
727 // editor is the area for the editor pane.
728 editor uv.Rectangle
729
730 // sidebar is the area for the sidebar.
731 sidebar uv.Rectangle
732
733 // help is the area for the help view.
734 help uv.Rectangle
735}
736
737// setEditorPrompt configures the textarea prompt function based on whether
738// yolo mode is enabled.
739func (m *UI) setEditorPrompt() {
740 if m.com.App.Permissions.SkipRequests() {
741 m.textarea.SetPromptFunc(4, m.yoloPromptFunc)
742 return
743 }
744 m.textarea.SetPromptFunc(4, m.normalPromptFunc)
745}
746
747// normalPromptFunc returns the normal editor prompt style (" > " on first
748// line, "::: " on subsequent lines).
749func (m *UI) normalPromptFunc(info textarea.PromptInfo) string {
750 t := m.com.Styles
751 if info.LineNumber == 0 {
752 return " > "
753 }
754 if info.Focused {
755 return t.EditorPromptNormalFocused.Render()
756 }
757 return t.EditorPromptNormalBlurred.Render()
758}
759
760// yoloPromptFunc returns the yolo mode editor prompt style with warning icon
761// and colored dots.
762func (m *UI) yoloPromptFunc(info textarea.PromptInfo) string {
763 t := m.com.Styles
764 if info.LineNumber == 0 {
765 if info.Focused {
766 return t.EditorPromptYoloIconFocused.Render()
767 } else {
768 return t.EditorPromptYoloIconBlurred.Render()
769 }
770 }
771 if info.Focused {
772 return t.EditorPromptYoloDotsFocused.Render()
773 }
774 return t.EditorPromptYoloDotsBlurred.Render()
775}
776
777var readyPlaceholders = [...]string{
778 "Ready!",
779 "Ready...",
780 "Ready?",
781 "Ready for instructions",
782}
783
784var workingPlaceholders = [...]string{
785 "Working!",
786 "Working...",
787 "Brrrrr...",
788 "Prrrrrrrr...",
789 "Processing...",
790 "Thinking...",
791}
792
793// randomizePlaceholders selects random placeholder text for the textarea's
794// ready and working states.
795func (m *UI) randomizePlaceholders() {
796 m.workingPlaceholder = workingPlaceholders[rand.Intn(len(workingPlaceholders))]
797 m.readyPlaceholder = readyPlaceholders[rand.Intn(len(readyPlaceholders))]
798}
799
800// renderHeader renders and caches the header logo at the specified width.
801func (m *UI) renderHeader(compact bool, width int) {
802 // TODO: handle the compact case differently
803 m.header = renderLogo(m.com.Styles, compact, width)
804}
805
806// renderSidebarLogo renders and caches the sidebar logo at the specified
807// width.
808func (m *UI) renderSidebarLogo(width int) {
809 m.sidebarLogo = renderLogo(m.com.Styles, true, width)
810}
811
812// renderLogo renders the Crush logo with the given styles and dimensions.
813func renderLogo(t *styles.Styles, compact bool, width int) string {
814 return logo.Render(version.Version, compact, logo.Opts{
815 FieldColor: t.LogoFieldColor,
816 TitleColorA: t.LogoTitleColorA,
817 TitleColorB: t.LogoTitleColorB,
818 CharmColor: t.LogoCharmColor,
819 VersionColor: t.LogoVersionColor,
820 Width: width,
821 })
822}