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 // Clear the screen first
301 screen.Clear(scr)
302
303 switch m.state {
304 case uiConfigure:
305 header := uv.NewStyledString(m.header)
306 header.Draw(scr, m.layout.header)
307
308 mainView := lipgloss.NewStyle().Width(m.layout.main.Dx()).
309 Height(m.layout.main.Dy()).
310 Background(lipgloss.ANSIColor(rand.Intn(256))).
311 Render(" Configure ")
312 main := uv.NewStyledString(mainView)
313 main.Draw(scr, m.layout.main)
314
315 case uiInitialize:
316 header := uv.NewStyledString(m.header)
317 header.Draw(scr, m.layout.header)
318
319 main := uv.NewStyledString(m.initializeView())
320 main.Draw(scr, m.layout.main)
321
322 case uiLanding:
323 header := uv.NewStyledString(m.header)
324 header.Draw(scr, m.layout.header)
325 main := uv.NewStyledString(m.landingView())
326 main.Draw(scr, m.layout.main)
327
328 editor := uv.NewStyledString(m.textarea.View())
329 editor.Draw(scr, m.layout.editor)
330
331 case uiChat:
332 header := uv.NewStyledString(m.header)
333 header.Draw(scr, m.layout.header)
334 m.drawSidebar(scr, m.layout.sidebar)
335
336 m.chat.Draw(scr, m.layout.main)
337
338 editor := uv.NewStyledString(m.textarea.View())
339 editor.Draw(scr, m.layout.editor)
340
341 case uiChatCompact:
342 header := uv.NewStyledString(m.header)
343 header.Draw(scr, m.layout.header)
344
345 mainView := lipgloss.NewStyle().Width(m.layout.main.Dx()).
346 Height(m.layout.main.Dy()).
347 Background(lipgloss.ANSIColor(rand.Intn(256))).
348 Render(" Compact Chat Messages ")
349 main := uv.NewStyledString(mainView)
350 main.Draw(scr, m.layout.main)
351
352 editor := uv.NewStyledString(m.textarea.View())
353 editor.Draw(scr, m.layout.editor)
354 }
355
356 // Add help layer
357 help := uv.NewStyledString(m.help.View(m))
358 help.Draw(scr, m.layout.help)
359
360 // Debugging rendering (visually see when the tui rerenders)
361 if os.Getenv("CRUSH_UI_DEBUG") == "true" {
362 debugView := lipgloss.NewStyle().Background(lipgloss.ANSIColor(rand.Intn(256))).Width(4).Height(2)
363 debug := uv.NewStyledString(debugView.String())
364 debug.Draw(scr, image.Rectangle{
365 Min: image.Pt(4, 1),
366 Max: image.Pt(8, 3),
367 })
368 }
369
370 // This needs to come last to overlay on top of everything
371 if m.dialog.HasDialogs() {
372 if dialogView := m.dialog.View(); dialogView != "" {
373 dialogWidth, dialogHeight := lipgloss.Width(dialogView), lipgloss.Height(dialogView)
374 dialogArea := common.CenterRect(area, dialogWidth, dialogHeight)
375 dialog := uv.NewStyledString(dialogView)
376 dialog.Draw(scr, dialogArea)
377 }
378 }
379}
380
381// Cursor returns the cursor position and properties for the UI model. It
382// returns nil if the cursor should not be shown.
383func (m *UI) Cursor() *tea.Cursor {
384 if m.layout.editor.Dy() <= 0 {
385 // Don't show cursor if editor is not visible
386 return nil
387 }
388 if m.focus == uiFocusEditor && m.textarea.Focused() {
389 cur := m.textarea.Cursor()
390 cur.X++ // Adjust for app margins
391 cur.Y += m.layout.editor.Min.Y
392 return cur
393 }
394 return nil
395}
396
397// View renders the UI model's view.
398func (m UI) View() tea.View {
399 var v tea.View
400 v.AltScreen = true
401 v.BackgroundColor = m.com.Styles.Background
402 v.Cursor = m.Cursor()
403
404 // TODO: Switch to lipgloss.Canvas when available
405 canvas := uv.NewScreenBuffer(m.width, m.height)
406 canvas.Method = ansi.GraphemeWidth
407
408 m.Draw(canvas, canvas.Bounds())
409
410 content := strings.ReplaceAll(canvas.Render(), "\r\n", "\n") // normalize newlines
411 contentLines := strings.Split(content, "\n")
412 for i, line := range contentLines {
413 // Trim trailing spaces for concise rendering
414 contentLines[i] = strings.TrimRight(line, " ")
415 }
416
417 content = strings.Join(contentLines, "\n")
418 v.Content = content
419 if m.sendProgressBar && m.com.App != nil && m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
420 // HACK: use a random percentage to prevent ghostty from hiding it
421 // after a timeout.
422 v.ProgressBar = tea.NewProgressBar(tea.ProgressBarIndeterminate, rand.Intn(100))
423 }
424
425 return v
426}
427
428// ShortHelp implements [help.KeyMap].
429func (m UI) ShortHelp() []key.Binding {
430 var binds []key.Binding
431 k := &m.keyMap
432
433 switch m.state {
434 case uiInitialize:
435 binds = append(binds, k.Quit)
436 default:
437 // TODO: other states
438 // if m.session == nil {
439 // no session selected
440 binds = append(binds,
441 k.Commands,
442 k.Models,
443 k.Editor.Newline,
444 k.Quit,
445 k.Help,
446 )
447 // }
448 // else {
449 // we have a session
450 // }
451
452 // switch m.state {
453 // case uiChat:
454 // case uiEdit:
455 // binds = append(binds,
456 // k.Editor.AddFile,
457 // k.Editor.SendMessage,
458 // k.Editor.OpenEditor,
459 // k.Editor.Newline,
460 // )
461 //
462 // if len(m.attachments) > 0 {
463 // binds = append(binds,
464 // k.Editor.AttachmentDeleteMode,
465 // k.Editor.DeleteAllAttachments,
466 // k.Editor.Escape,
467 // )
468 // }
469 // }
470 }
471
472 return binds
473}
474
475// FullHelp implements [help.KeyMap].
476func (m UI) FullHelp() [][]key.Binding {
477 var binds [][]key.Binding
478 k := &m.keyMap
479 help := k.Help
480 help.SetHelp("ctrl+g", "less")
481
482 switch m.state {
483 case uiInitialize:
484 binds = append(binds,
485 []key.Binding{
486 k.Quit,
487 })
488 default:
489 if m.session == nil {
490 // no session selected
491 binds = append(binds,
492 []key.Binding{
493 k.Commands,
494 k.Models,
495 k.Sessions,
496 },
497 []key.Binding{
498 k.Editor.Newline,
499 k.Editor.AddImage,
500 k.Editor.MentionFile,
501 k.Editor.OpenEditor,
502 },
503 []key.Binding{
504 help,
505 },
506 )
507 }
508 // else {
509 // we have a session
510 // }
511 }
512
513 // switch m.state {
514 // case uiChat:
515 // case uiEdit:
516 // binds = append(binds, m.ShortHelp())
517 // }
518
519 return binds
520}
521
522// updateDialogs updates the dialog overlay with the given message and returns cmds
523func (m *UI) updateDialogs(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
524 updatedDialog, cmd := m.dialog.Update(msg)
525 m.dialog = updatedDialog
526 cmds = append(cmds, cmd)
527 return cmds
528}
529
530// updateFocused updates the focused model (chat or editor) with the given message
531// and appends any resulting commands to the cmds slice.
532func (m *UI) updateFocused(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
533 switch m.state {
534 case uiConfigure:
535 return cmds
536 case uiInitialize:
537 return append(cmds, m.updateInitializeView(msg)...)
538 case uiChat, uiLanding, uiChatCompact:
539 switch m.focus {
540 case uiFocusMain:
541 case uiFocusEditor:
542 switch {
543 case key.Matches(msg, m.keyMap.Editor.Newline):
544 m.textarea.InsertRune('\n')
545 }
546
547 ta, cmd := m.textarea.Update(msg)
548 m.textarea = ta
549 cmds = append(cmds, cmd)
550 return cmds
551 }
552 }
553 return cmds
554}
555
556// updateLayoutAndSize updates the layout and sizes of UI components.
557func (m *UI) updateLayoutAndSize() {
558 m.layout = generateLayout(m, m.width, m.height)
559 m.updateSize()
560}
561
562// updateSize updates the sizes of UI components based on the current layout.
563func (m *UI) updateSize() {
564 // Set help width
565 m.help.SetWidth(m.layout.help.Dx())
566
567 // Handle different app states
568 switch m.state {
569 case uiConfigure, uiInitialize:
570 m.renderHeader(false, m.layout.header.Dx())
571
572 case uiLanding:
573 m.renderHeader(false, m.layout.header.Dx())
574 m.textarea.SetWidth(m.layout.editor.Dx())
575 m.textarea.SetHeight(m.layout.editor.Dy())
576
577 case uiChat:
578 m.renderSidebarLogo(m.layout.sidebar.Dx())
579 m.textarea.SetWidth(m.layout.editor.Dx())
580 m.textarea.SetHeight(m.layout.editor.Dy())
581 m.chat.SetSize(m.layout.main.Dx(), m.layout.main.Dy())
582
583 case uiChatCompact:
584 // TODO: set the width and heigh of the chat component
585 m.renderHeader(true, m.layout.header.Dx())
586 m.textarea.SetWidth(m.layout.editor.Dx())
587 m.textarea.SetHeight(m.layout.editor.Dy())
588 }
589}
590
591// generateLayout calculates the layout rectangles for all UI components based
592// on the current UI state and terminal dimensions.
593func generateLayout(m *UI, w, h int) layout {
594 // The screen area we're working with
595 area := image.Rect(0, 0, w, h)
596
597 // The help height
598 helpHeight := 1
599 // The editor height
600 editorHeight := 5
601 // The sidebar width
602 sidebarWidth := 30
603 // The header height
604 // TODO: handle compact
605 headerHeight := 4
606
607 var helpKeyMap help.KeyMap = m
608 if m.help.ShowAll {
609 for _, row := range helpKeyMap.FullHelp() {
610 helpHeight = max(helpHeight, len(row))
611 }
612 }
613
614 // Add app margins
615 appRect := area
616 appRect.Min.X += 1
617 appRect.Min.Y += 1
618 appRect.Max.X -= 1
619 appRect.Max.Y -= 1
620
621 if slices.Contains([]uiState{uiConfigure, uiInitialize, uiLanding}, m.state) {
622 // extra padding on left and right for these states
623 appRect.Min.X += 1
624 appRect.Max.X -= 1
625 }
626
627 appRect, helpRect := uv.SplitVertical(appRect, uv.Fixed(appRect.Dy()-helpHeight))
628
629 layout := layout{
630 area: area,
631 help: helpRect,
632 }
633
634 // Handle different app states
635 switch m.state {
636 case uiConfigure, uiInitialize:
637 // Layout
638 //
639 // header
640 // ------
641 // main
642 // ------
643 // help
644
645 headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(headerHeight))
646 layout.header = headerRect
647 layout.main = mainRect
648
649 case uiLanding:
650 // Layout
651 //
652 // header
653 // ------
654 // main
655 // ------
656 // editor
657 // ------
658 // help
659 headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(headerHeight))
660 mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
661 // Remove extra padding from editor (but keep it for header and main)
662 editorRect.Min.X -= 1
663 editorRect.Max.X += 1
664 layout.header = headerRect
665 layout.main = mainRect
666 layout.editor = editorRect
667
668 case uiChat:
669 // Layout
670 //
671 // ------|---
672 // main |
673 // ------| side
674 // editor|
675 // ----------
676 // help
677
678 mainRect, sideRect := uv.SplitHorizontal(appRect, uv.Fixed(appRect.Dx()-sidebarWidth))
679 // Add padding left
680 sideRect.Min.X += 1
681 mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
682 // Add bottom margin to main
683 mainRect.Max.Y -= 1
684 layout.sidebar = sideRect
685 layout.main = mainRect
686 layout.editor = editorRect
687
688 case uiChatCompact:
689 // Layout
690 //
691 // compact-header
692 // ------
693 // main
694 // ------
695 // editor
696 // ------
697 // help
698 headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(appRect.Dy()-headerHeight))
699 mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
700 layout.header = headerRect
701 layout.main = mainRect
702 layout.editor = editorRect
703 }
704
705 if !layout.editor.Empty() {
706 // Add editor margins 1 top and bottom
707 layout.editor.Min.Y += 1
708 layout.editor.Max.Y -= 1
709 }
710
711 return layout
712}
713
714// layout defines the positioning of UI elements.
715type layout struct {
716 // area is the overall available area.
717 area uv.Rectangle
718
719 // header is the header shown in special cases
720 // e.x when the sidebar is collapsed
721 // or when in the landing page
722 // or in init/config
723 header uv.Rectangle
724
725 // main is the area for the main pane. (e.x chat, configure, landing)
726 main uv.Rectangle
727
728 // editor is the area for the editor pane.
729 editor uv.Rectangle
730
731 // sidebar is the area for the sidebar.
732 sidebar uv.Rectangle
733
734 // help is the area for the help view.
735 help uv.Rectangle
736}
737
738// setEditorPrompt configures the textarea prompt function based on whether
739// yolo mode is enabled.
740func (m *UI) setEditorPrompt() {
741 if m.com.App.Permissions.SkipRequests() {
742 m.textarea.SetPromptFunc(4, m.yoloPromptFunc)
743 return
744 }
745 m.textarea.SetPromptFunc(4, m.normalPromptFunc)
746}
747
748// normalPromptFunc returns the normal editor prompt style (" > " on first
749// line, "::: " on subsequent lines).
750func (m *UI) normalPromptFunc(info textarea.PromptInfo) string {
751 t := m.com.Styles
752 if info.LineNumber == 0 {
753 return " > "
754 }
755 if info.Focused {
756 return t.EditorPromptNormalFocused.Render()
757 }
758 return t.EditorPromptNormalBlurred.Render()
759}
760
761// yoloPromptFunc returns the yolo mode editor prompt style with warning icon
762// and colored dots.
763func (m *UI) yoloPromptFunc(info textarea.PromptInfo) string {
764 t := m.com.Styles
765 if info.LineNumber == 0 {
766 if info.Focused {
767 return t.EditorPromptYoloIconFocused.Render()
768 } else {
769 return t.EditorPromptYoloIconBlurred.Render()
770 }
771 }
772 if info.Focused {
773 return t.EditorPromptYoloDotsFocused.Render()
774 }
775 return t.EditorPromptYoloDotsBlurred.Render()
776}
777
778var readyPlaceholders = [...]string{
779 "Ready!",
780 "Ready...",
781 "Ready?",
782 "Ready for instructions",
783}
784
785var workingPlaceholders = [...]string{
786 "Working!",
787 "Working...",
788 "Brrrrr...",
789 "Prrrrrrrr...",
790 "Processing...",
791 "Thinking...",
792}
793
794// randomizePlaceholders selects random placeholder text for the textarea's
795// ready and working states.
796func (m *UI) randomizePlaceholders() {
797 m.workingPlaceholder = workingPlaceholders[rand.Intn(len(workingPlaceholders))]
798 m.readyPlaceholder = readyPlaceholders[rand.Intn(len(readyPlaceholders))]
799}
800
801// renderHeader renders and caches the header logo at the specified width.
802func (m *UI) renderHeader(compact bool, width int) {
803 // TODO: handle the compact case differently
804 m.header = renderLogo(m.com.Styles, compact, width)
805}
806
807// renderSidebarLogo renders and caches the sidebar logo at the specified
808// width.
809func (m *UI) renderSidebarLogo(width int) {
810 m.sidebarLogo = renderLogo(m.com.Styles, true, width)
811}
812
813// renderLogo renders the Crush logo with the given styles and dimensions.
814func renderLogo(t *styles.Styles, compact bool, width int) string {
815 return logo.Render(version.Version, compact, logo.Opts{
816 FieldColor: t.LogoFieldColor,
817 TitleColorA: t.LogoTitleColorA,
818 TitleColorB: t.LogoTitleColorB,
819 CharmColor: t.LogoCharmColor,
820 VersionColor: t.LogoVersionColor,
821 Width: width,
822 })
823}