on main help sign in
refactor(ui): reorganize ui components into flat structure
commit706e182730c2ea7542c0da0224c72b44c5d0db5d authorAyman Bagabas <ayman.bagabas@gmail.com> dateFri 24 Oct 2025, 12:00 (1 year ago) parent27fa971
7 files changed+310−107
internal/ui/common.go+17 internal/ui/dialog/dialog.go → internal/ui/dialog.go+24−11 internal/ui/dialog/keymap.go−57 internal/ui/common/common.go → internal/ui/model.go+1−1 internal/ui/dialog/quit.go → internal/ui/quit_dialog.go+60−27 internal/ui/styles.go+194 internal/ui/ui.go+14−11
internal/ui/common.gonew file+17
@@ -0,0 +1,17 @@
1+package ui
2+
3+import "github.com/charmbracelet/crush/internal/config"
4+
5+// Common defines common UI options and configurations.
6+type Common struct {
7+ Config *config.Config
8+ Styles Styles
9+}
10+
11+// DefaultCommon returns the default common UI configurations.
12+func DefaultCommon(cfg *config.Config) *Common {
13+ return &Common{
14+ Config: cfg,
15+ Styles: DefaultStyles(),
16+ }
17+}
internal/ui/dialog/dialog.go → internal/ui/dialog.gorenamed+24−11
@@ -1,29 +1,42 @@
1−package dialog
1+package ui
22
33import (
44 "github.com/charmbracelet/bubbles/v2/key"
55 tea "github.com/charmbracelet/bubbletea/v2"
6− "github.com/charmbracelet/crush/internal/ui/common"
76 "github.com/charmbracelet/lipgloss/v2"
87)
98
10−// Model is a component that can be displayed on top of the UI.
11−type Model interface {
12− common.Model[Model]
9+// DialogOverlayKeyMap defines key bindings for dialogs.
10+type DialogOverlayKeyMap struct {
11+ Close key.Binding
12+}
13+
14+// DefaultDialogOverlayKeyMap returns the default key bindings for dialogs.
15+func DefaultDialogOverlayKeyMap() DialogOverlayKeyMap {
16+ return DialogOverlayKeyMap{
17+ Close: key.NewBinding(
18+ key.WithKeys("esc", "alt+esc"),
19+ ),
20+ }
21+}
22+
23+// Dialog is a component that can be displayed on top of the UI.
24+type Dialog interface {
25+ Model[Dialog]
1326 ID() string
1427}
1528
1629// Overlay manages multiple dialogs as an overlay.
1730type Overlay struct {
18− dialogs []Model
19− keyMap KeyMap
31+ dialogs []Dialog
32+ keyMap DialogOverlayKeyMap
2033}
2134
22−// NewOverlay creates a new [Overlay] instance.
23−func NewOverlay(dialogs ...Model) *Overlay {
35+// NewDialogOverlay creates a new [Overlay] instance.
36+func NewDialogOverlay(dialogs ...Dialog) *Overlay {
2437 return &Overlay{
2538 dialogs: dialogs,
26− keyMap: DefaultKeyMap(),
39+ keyMap: DefaultDialogOverlayKeyMap(),
2740 }
2841}
2942
@@ -38,7 +51,7 @@ func (d *Overlay) ContainsDialog(dialogID string) bool {
3851}
3952
4053// AddDialog adds a new dialog to the stack.
41−func (d *Overlay) AddDialog(dialog Model) {
54+func (d *Overlay) AddDialog(dialog Dialog) {
4255 d.dialogs = append(d.dialogs, dialog)
4356}
4457
internal/ui/dialog/keymap.godeleted−57
@@ -1,57 +0,0 @@
1−package dialog
2−
3−import "github.com/charmbracelet/bubbles/v2/key"
4−
5−// KeyMap defines key bindings for dialogs.
6−type KeyMap struct {
7− Close key.Binding
8−}
9−
10−// DefaultKeyMap returns the default key bindings for dialogs.
11−func DefaultKeyMap() KeyMap {
12− return KeyMap{
13− Close: key.NewBinding(
14− key.WithKeys("esc", "alt+esc"),
15− ),
16− }
17−}
18−
19−// QuitKeyMap represents key bindings for the quit dialog.
20−type QuitKeyMap struct {
21− LeftRight,
22− EnterSpace,
23− Yes,
24− No,
25− Tab,
26− Close key.Binding
27−}
28−
29−// DefaultQuitKeyMap returns the default key bindings for the quit dialog.
30−func DefaultQuitKeyMap() QuitKeyMap {
31− return QuitKeyMap{
32− LeftRight: key.NewBinding(
33− key.WithKeys("left", "right"),
34− key.WithHelp("←/→", "switch options"),
35− ),
36− EnterSpace: key.NewBinding(
37− key.WithKeys("enter", " "),
38− key.WithHelp("enter/space", "confirm"),
39− ),
40− Yes: key.NewBinding(
41− key.WithKeys("y", "Y", "ctrl+c"),
42− key.WithHelp("y/Y/ctrl+c", "yes"),
43− ),
44− No: key.NewBinding(
45− key.WithKeys("n", "N"),
46− key.WithHelp("n/N", "no"),
47− ),
48− Tab: key.NewBinding(
49− key.WithKeys("tab"),
50− key.WithHelp("tab", "switch options"),
51− ),
52− Close: key.NewBinding(
53− key.WithKeys("esc", "alt+esc"),
54− key.WithHelp("esc", "cancel"),
55− ),
56− }
57−}
internal/ui/common/common.go → internal/ui/model.gorenamed+1−1
@@ -1,4 +1,4 @@
1−package common
1+package ui
22
33import tea "github.com/charmbracelet/bubbletea/v2"
44
internal/ui/dialog/quit.go → internal/ui/quit_dialog.gorenamed+60−27
@@ -1,4 +1,4 @@
1−package dialog
1+package ui
22
33import (
44 "github.com/charmbracelet/bubbles/v2/key"
@@ -6,27 +6,69 @@ import (
66 "github.com/charmbracelet/lipgloss/v2"
77)
88
9−// Quit represents a confirmation dialog for quitting the application.
10−type Quit struct {
11− keyMap QuitKeyMap
9+// QuitDialogKeyMap represents key bindings for the quit dialog.
10+type QuitDialogKeyMap struct {
11+ LeftRight,
12+ EnterSpace,
13+ Yes,
14+ No,
15+ Tab,
16+ Close key.Binding
17+}
18+
19+// DefaultQuitKeyMap returns the default key bindings for the quit dialog.
20+func DefaultQuitKeyMap() QuitDialogKeyMap {
21+ return QuitDialogKeyMap{
22+ LeftRight: key.NewBinding(
23+ key.WithKeys("left", "right"),
24+ key.WithHelp("←/→", "switch options"),
25+ ),
26+ EnterSpace: key.NewBinding(
27+ key.WithKeys("enter", " "),
28+ key.WithHelp("enter/space", "confirm"),
29+ ),
30+ Yes: key.NewBinding(
31+ key.WithKeys("y", "Y", "ctrl+c"),
32+ key.WithHelp("y/Y/ctrl+c", "yes"),
33+ ),
34+ No: key.NewBinding(
35+ key.WithKeys("n", "N"),
36+ key.WithHelp("n/N", "no"),
37+ ),
38+ Tab: key.NewBinding(
39+ key.WithKeys("tab"),
40+ key.WithHelp("tab", "switch options"),
41+ ),
42+ Close: key.NewBinding(
43+ key.WithKeys("esc", "alt+esc"),
44+ key.WithHelp("esc", "cancel"),
45+ ),
46+ }
47+}
48+
49+// QuitDialog represents a confirmation dialog for quitting the application.
50+type QuitDialog struct {
51+ com *Common
52+ keyMap QuitDialogKeyMap
1253 selectedNo bool // true if "No" button is selected
1354}
1455
15−// NewQuit creates a new quit confirmation dialog.
16−func NewQuit() *Quit {
17− q := &Quit{
56+// NewQuitDialog creates a new quit confirmation dialog.
57+func NewQuitDialog(com *Common) *QuitDialog {
58+ q := &QuitDialog{
59+ com: com,
1860 keyMap: DefaultQuitKeyMap(),
1961 }
2062 return q
2163}
2264
2365// ID implements [Model].
24−func (*Quit) ID() string {
66+func (*QuitDialog) ID() string {
2567 return "quit"
2668}
2769
2870// Update implements [Model].
29−func (q *Quit) Update(msg tea.Msg) (Model, tea.Cmd) {
71+func (q *QuitDialog) Update(msg tea.Msg) (Dialog, tea.Cmd) {
3072 switch msg := msg.(type) {
3173 case tea.KeyPressMsg:
3274 switch {
@@ -49,19 +91,15 @@ func (q *Quit) Update(msg tea.Msg) (Model, tea.Cmd) {
4991}
5092
5193// View implements [Model].
52−func (q *Quit) View() string {
94+func (q *QuitDialog) View() string {
5395 const question = "Are you sure you want to quit?"
54−
55− baseStyle := lipgloss.NewStyle()
56− yesStyle := lipgloss.NewStyle()
57− noStyle := yesStyle
96+ baseStyle := q.com.Styles.Base
97+ yesStyle := q.com.Styles.ButtonSelected
98+ noStyle := q.com.Styles.ButtonUnselected
5899
59100 if q.selectedNo {
60− noStyle = noStyle.Foreground(lipgloss.Color("15")).Background(lipgloss.Color("15"))
61− yesStyle = yesStyle.Background(lipgloss.Color("15"))
62− } else {
63− yesStyle = yesStyle.Foreground(lipgloss.Color("15")).Background(lipgloss.Color("15"))
64− noStyle = noStyle.Background(lipgloss.Color("15"))
101+ noStyle = q.com.Styles.ButtonSelected
102+ yesStyle = q.com.Styles.ButtonUnselected
65103 }
66104
67105 const horizontalPadding = 3
@@ -83,16 +121,11 @@ func (q *Quit) View() string {
83121 ),
84122 )
85123
86− quitDialogStyle := baseStyle.
87− Padding(1, 2).
88− Border(lipgloss.RoundedBorder()).
89− BorderForeground(lipgloss.Color("15"))
90−
91− return quitDialogStyle.Render(content)
124+ return q.com.Styles.BorderFocus.Render(content)
92125}
93126
94127// ShortHelp implements [help.KeyMap].
95−func (q *Quit) ShortHelp() []key.Binding {
128+func (q *QuitDialog) ShortHelp() []key.Binding {
96129 return []key.Binding{
97130 q.keyMap.LeftRight,
98131 q.keyMap.EnterSpace,
@@ -100,7 +133,7 @@ func (q *Quit) ShortHelp() []key.Binding {
100133}
101134
102135// FullHelp implements [help.KeyMap].
103−func (q *Quit) FullHelp() [][]key.Binding {
136+func (q *QuitDialog) FullHelp() [][]key.Binding {
104137 return [][]key.Binding{
105138 {q.keyMap.LeftRight, q.keyMap.EnterSpace, q.keyMap.Yes, q.keyMap.No},
106139 {q.keyMap.Tab, q.keyMap.Close},
internal/ui/styles.gonew file+194
@@ -0,0 +1,194 @@
1+package ui
2+
3+import (
4+ "github.com/charmbracelet/bubbles/v2/filepicker"
5+ "github.com/charmbracelet/bubbles/v2/help"
6+ "github.com/charmbracelet/bubbles/v2/textarea"
7+ "github.com/charmbracelet/bubbles/v2/textinput"
8+ "github.com/charmbracelet/crush/internal/tui/exp/diffview"
9+ "github.com/charmbracelet/glamour/v2/ansi"
10+ "github.com/charmbracelet/lipgloss/v2"
11+ "github.com/charmbracelet/x/exp/charmtone"
12+)
13+
14+const (
15+ CheckIcon string = "✓"
16+ ErrorIcon string = "×"
17+ WarningIcon string = "⚠"
18+ InfoIcon string = "ⓘ"
19+ HintIcon string = "∵"
20+ SpinnerIcon string = "..."
21+ LoadingIcon string = "⟳"
22+ DocumentIcon string = "🖼"
23+ ModelIcon string = "◇"
24+
25+ ToolPending string = "●"
26+ ToolSuccess string = "✓"
27+ ToolError string = "×"
28+
29+ BorderThin string = "│"
30+ BorderThick string = "▌"
31+)
32+
33+type Styles struct {
34+ WindowTooSmall lipgloss.Style
35+
36+ // Reusable text styles
37+ Base lipgloss.Style
38+ Muted lipgloss.Style
39+ Subtle lipgloss.Style
40+
41+ // Tags
42+ TagBase lipgloss.Style
43+ TagError lipgloss.Style
44+ TagInfo lipgloss.Style
45+
46+ // Headers
47+ HeaderTool lipgloss.Style
48+ HeaderToolNested lipgloss.Style
49+
50+ // Panels
51+ PanelMuted lipgloss.Style
52+ PanelBase lipgloss.Style
53+
54+ // Line numbers for code blocks
55+ LineNumber lipgloss.Style
56+
57+ // Message borders
58+ FocusedMessageBorder lipgloss.Border
59+
60+ // Tool calls
61+ ToolCallPending lipgloss.Style
62+ ToolCallError lipgloss.Style
63+ ToolCallSuccess lipgloss.Style
64+ ToolCallCancelled lipgloss.Style
65+ EarlyStateMessage lipgloss.Style
66+
67+ // Text selection
68+ TextSelection lipgloss.Style
69+
70+ // LSP and MCP status indicators
71+ ItemOfflineIcon lipgloss.Style
72+ ItemBusyIcon lipgloss.Style
73+ ItemErrorIcon lipgloss.Style
74+ ItemOnlineIcon lipgloss.Style
75+
76+ // Markdown & Chroma
77+ Markdown ansi.StyleConfig
78+
79+ // Inputs
80+ TextInput textinput.Styles
81+ TextArea textarea.Styles
82+
83+ // Help
84+ Help help.Styles
85+
86+ // Diff
87+ Diff diffview.Style
88+
89+ // FilePicker
90+ FilePicker filepicker.Styles
91+
92+ // Buttons
93+ ButtonSelected lipgloss.Style
94+ ButtonUnselected lipgloss.Style
95+
96+ // Borders
97+ BorderFocus lipgloss.Style
98+ BorderBlur lipgloss.Style
99+}
100+
101+func DefaultStyles() Styles {
102+ var (
103+ // primary = charmtone.Charple
104+ secondary = charmtone.Dolly
105+ // tertiary = charmtone.Bok
106+ // accent = charmtone.Zest
107+
108+ // Backgrounds
109+ bgBase = charmtone.Pepper
110+ bgBaseLighter = charmtone.BBQ
111+ bgSubtle = charmtone.Charcoal
112+ // bgOverlay = charmtone.Iron
113+
114+ // Foregrounds
115+ fgBase = charmtone.Ash
116+ fgMuted = charmtone.Squid
117+ fgHalfMuted = charmtone.Smoke
118+ fgSubtle = charmtone.Oyster
119+ // fgSelected = charmtone.Salt
120+
121+ // Borders
122+ // border = charmtone.Charcoal
123+ borderFocus = charmtone.Charple
124+
125+ // Status
126+ // success = charmtone.Guac
127+ // error = charmtone.Sriracha
128+ // warning = charmtone.Zest
129+ // info = charmtone.Malibu
130+
131+ // Colors
132+ white = charmtone.Butter
133+
134+ blueLight = charmtone.Sardine
135+ blue = charmtone.Malibu
136+
137+ // yellow = charmtone.Mustard
138+ // citron = charmtone.Citron
139+
140+ green = charmtone.Julep
141+ greenDark = charmtone.Guac
142+ // greenLight = charmtone.Bok
143+
144+ // red = charmtone.Coral
145+ redDark = charmtone.Sriracha
146+ // redLight = charmtone.Salmon
147+ // cherry = charmtone.Cherry
148+ )
149+
150+ s := Styles{}
151+
152+ // borders
153+ s.FocusedMessageBorder = lipgloss.Border{Left: BorderThick}
154+
155+ // text presets
156+ s.Base = lipgloss.NewStyle().Foreground(fgBase)
157+ s.Muted = lipgloss.NewStyle().Foreground(fgMuted)
158+ s.Subtle = lipgloss.NewStyle().Foreground(fgSubtle)
159+
160+ s.WindowTooSmall = s.Muted
161+
162+ // tag presets
163+ s.TagBase = lipgloss.NewStyle().Padding(0, 1).Foreground(white)
164+ s.TagError = s.TagBase.Background(redDark)
165+ s.TagInfo = s.TagBase.Background(blueLight)
166+
167+ // headers
168+ s.HeaderTool = lipgloss.NewStyle().Foreground(blue)
169+ s.HeaderToolNested = lipgloss.NewStyle().Foreground(fgHalfMuted)
170+
171+ // panels
172+ s.PanelMuted = s.Muted.Background(bgBaseLighter)
173+ s.PanelBase = lipgloss.NewStyle().Background(bgBase)
174+
175+ // code line number
176+ s.LineNumber = lipgloss.NewStyle().Foreground(fgMuted).Background(bgBase).PaddingRight(1).PaddingLeft(1)
177+
178+ // Tool calls
179+ s.ToolCallPending = lipgloss.NewStyle().Foreground(greenDark).SetString(ToolPending)
180+ s.ToolCallError = lipgloss.NewStyle().Foreground(redDark).SetString(ToolError)
181+ s.ToolCallSuccess = lipgloss.NewStyle().Foreground(green).SetString(ToolSuccess)
182+ // Cancelled uses muted tone but same glyph as pending
183+ s.ToolCallCancelled = s.Muted.SetString(ToolPending)
184+ s.EarlyStateMessage = s.Subtle.PaddingLeft(2)
185+
186+ // Buttons
187+ s.ButtonSelected = lipgloss.NewStyle().Foreground(white).Background(secondary)
188+ s.ButtonUnselected = s.Base.Background(bgSubtle)
189+
190+ // Borders
191+ s.BorderFocus = lipgloss.NewStyle().BorderForeground(borderFocus).Border(lipgloss.RoundedBorder()).Padding(1, 2)
192+
193+ return s
194+}
internal/ui/ui.go+14−11
@@ -6,7 +6,6 @@ import (
66 "github.com/charmbracelet/bubbles/v2/key"
77 tea "github.com/charmbracelet/bubbletea/v2"
88 "github.com/charmbracelet/crush/internal/app"
9− "github.com/charmbracelet/crush/internal/ui/dialog"
109 "github.com/charmbracelet/lipgloss/v2"
1110 uv "github.com/charmbracelet/ultraviolet"
1211)
@@ -17,29 +16,33 @@ const (
1716 uiStateMain uiState = iota
1817)
1918
20−type Model struct {
21− app *app.App
19+type UI struct {
20+ app *app.App
21+ com *Common
22+
2223 width, height int
2324 state uiState
2425
2526 keyMap KeyMap
27+ styles Styles
2628
27− dialog *dialog.Overlay
29+ dialog *Overlay
2830}
2931
30−func New(app *app.App) *Model {
31− return &Model{
32+func New(com *Common, app *app.App) *UI {
33+ return &UI{
3234 app: app,
33− dialog: dialog.NewOverlay(),
35+ com: com,
36+ dialog: NewDialogOverlay(),
3437 keyMap: DefaultKeyMap(),
3538 }
3639}
3740
38−func (m *Model) Init() tea.Cmd {
41+func (m *UI) Init() tea.Cmd {
3942 return nil
4043}
4144
42−func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
45+func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
4346 var cmds []tea.Cmd
4447 switch msg := msg.(type) {
4548 case tea.WindowSizeMsg:
@@ -50,7 +53,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5053 case uiStateMain:
5154 switch {
5255 case key.Matches(msg, m.keyMap.Quit):
53− quitDialog := dialog.NewQuit()
56+ quitDialog := NewQuitDialog(m.com)
5457 if !m.dialog.ContainsDialog(quitDialog.ID()) {
5558 m.dialog.AddDialog(quitDialog)
5659 return m, nil
@@ -68,7 +71,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6871 return m, tea.Batch(cmds...)
6972}
7073
71−func (m *Model) View() tea.View {
74+func (m *UI) View() tea.View {
7275 var v tea.View
7376
7477 // The screen area we're working with