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