on main help sign in
refactor(ui): simplify dialog model and rendering
commit53d38c08edbabad0edd47d38a9b126d271b8301c authorAyman Bagabas <ayman.bagabas@gmail.com> dateMon 15 Dec 2025, 10:19 (1 year ago) parentad4c848
6 files changed+37−72
internal/ui/common/markdown.go+2−2 internal/ui/dialog/dialog.go+16−39 internal/ui/dialog/quit.go+10−10 internal/ui/dialog/sessions.go+7−7 internal/ui/model/ui.go+1−13 internal/ui/styles/styles.go+1−1
internal/ui/common/markdown.go+2−2
@@ -1,9 +1,9 @@
11package common
22
33import (
4+ "charm.land/glamour/v2"
5+ gstyles "charm.land/glamour/v2/styles"
46 "github.com/charmbracelet/crush/internal/ui/styles"
5− "github.com/charmbracelet/glamour/v2"
6− gstyles "github.com/charmbracelet/glamour/v2/styles"
77)
88
99// MarkdownRenderer returns a glamour [glamour.TermRenderer] configured with
internal/ui/dialog/dialog.go+16−39
@@ -4,6 +4,8 @@ import (
44 "charm.land/bubbles/v2/key"
55 tea "charm.land/bubbletea/v2"
66 "charm.land/lipgloss/v2"
7+ "github.com/charmbracelet/crush/internal/ui/common"
8+ uv "github.com/charmbracelet/ultraviolet"
79)
810
911// CloseKey is the default key binding to close dialogs.
@@ -12,35 +14,11 @@ var CloseKey = key.NewBinding(
1214 key.WithHelp("esc", "exit"),
1315)
1416
15−// OverlayKeyMap defines key bindings for dialogs.
16−type OverlayKeyMap struct {
17− Close key.Binding
18−}
19−
20−// ActionType represents the type of action taken by a dialog.
21−type ActionType int
22−
23−const (
24− // ActionNone indicates no action.
25− ActionNone ActionType = iota
26− // ActionClose indicates that the dialog should be closed.
27− ActionClose
28− // ActionSelect indicates that an item has been selected.
29− ActionSelect
30−)
31−
32−// Action represents an action taken by a dialog.
33−// It can be used to signal closing or other operations.
34−type Action struct {
35− Type ActionType
36− Payload any
37−}
38−
3917// Dialog is a component that can be displayed on top of the UI.
4018type Dialog interface {
4119 ID() string
42− Update(msg tea.Msg) (Action, tea.Cmd)
43− Layer() *lipgloss.Layer
20+ Update(msg tea.Msg) tea.Cmd
21+ View() string
4422}
4523
4624// Overlay manages multiple dialogs as an overlay.
@@ -122,27 +100,26 @@ func (d *Overlay) Update(msg tea.Msg) (*Overlay, tea.Cmd) {
122100 }
123101 }
124102
125− action, cmd := dialog.Update(msg)
126− switch action.Type {
127− case ActionClose:
103+ if cmd := dialog.Update(msg); cmd != nil {
128104 // Close the current dialog
129105 d.removeDialog(idx)
130106 return d, cmd
131− case ActionSelect:
132− // Pass the action up (without modifying the dialog stack)
133− return d, cmd
134107 }
135108
136− return d, cmd
109+ return d, nil
137110}
138111
139−// Layers returns the current stack of dialogs as lipgloss layers.
140−func (d *Overlay) Layers() []*lipgloss.Layer {
141− layers := make([]*lipgloss.Layer, len(d.dialogs))
142− for i, dialog := range d.dialogs {
143− layers[i] = dialog.Layer()
112+// Draw renders the overlay and its dialogs.
113+func (d *Overlay) Draw(scr uv.Screen, area uv.Rectangle) {
114+ for _, dialog := range d.dialogs {
115+ view := dialog.View()
116+ viewWidth := lipgloss.Width(view)
117+ viewHeight := lipgloss.Height(view)
118+ center := common.CenterRect(area, viewWidth, viewHeight)
119+ if area.Overlaps(center) {
120+ uv.NewStyledString(view).Draw(scr, center)
121+ }
144122 }
145− return layers
146123}
147124
148125// removeDialog removes a dialog from the stack.
internal/ui/dialog/quit.go+10−10
@@ -73,30 +73,30 @@ func (*Quit) ID() string {
7373}
7474
7575// Update implements [Model].
76−func (q *Quit) Update(msg tea.Msg) (Action, tea.Cmd) {
76+func (q *Quit) Update(msg tea.Msg) tea.Cmd {
7777 switch msg := msg.(type) {
7878 case tea.KeyPressMsg:
7979 switch {
8080 case key.Matches(msg, q.keyMap.LeftRight, q.keyMap.Tab):
8181 q.selectedNo = !q.selectedNo
82− return Action{}, nil
82+ return nil
8383 case key.Matches(msg, q.keyMap.EnterSpace):
8484 if !q.selectedNo {
85− return Action{}, tea.Quit
85+ return tea.Quit
8686 }
87− return Action{}, nil
87+ return nil
8888 case key.Matches(msg, q.keyMap.Yes):
89− return Action{}, tea.Quit
89+ return tea.Quit
9090 case key.Matches(msg, q.keyMap.No, q.keyMap.Close):
91− return Action{}, nil
91+ return nil
9292 }
9393 }
9494
95− return Action{}, nil
95+ return nil
9696}
9797
98−// Layer implements [Model].
99−func (q *Quit) Layer() *lipgloss.Layer {
98+// View implements [Dialog].
99+func (q *Quit) View() string {
100100 const question = "Are you sure you want to quit?"
101101 baseStyle := q.com.Styles.Base
102102 buttonOpts := []common.ButtonOpts{
@@ -113,7 +113,7 @@ func (q *Quit) Layer() *lipgloss.Layer {
113113 ),
114114 )
115115
116− return lipgloss.NewLayer(q.com.Styles.BorderFocus.Render(content))
116+ return q.com.Styles.BorderFocus.Render(content)
117117}
118118
119119// ShortHelp implements [help.KeyMap].
internal/ui/dialog/sessions.go+7−7
@@ -100,7 +100,7 @@ func (s *Session) ID() string {
100100}
101101
102102// Update implements Dialog.
103−func (s *Session) Update(msg tea.Msg) (Action, tea.Cmd) {
103+func (s *Session) Update(msg tea.Msg) tea.Cmd {
104104 switch msg := msg.(type) {
105105 case tea.KeyPressMsg:
106106 switch {
@@ -115,20 +115,20 @@ func (s *Session) Update(msg tea.Msg) (Action, tea.Cmd) {
115115 case key.Matches(msg, s.keyMap.Select):
116116 if item := s.list.SelectedItem(); item != nil {
117117 sessionItem := item.(*SessionItem)
118− return Action{Type: ActionSelect, Payload: sessionItem.Session}, SessionSelectCmd(sessionItem.Session)
118+ return SessionSelectCmd(sessionItem.Session)
119119 }
120120 default:
121121 var cmd tea.Cmd
122122 s.input, cmd = s.input.Update(msg)
123123 s.list.SetFilter(s.input.Value())
124− return Action{}, cmd
124+ return cmd
125125 }
126126 }
127− return Action{}, nil
127+ return nil
128128}
129129
130−// Layer implements Dialog.
131−func (s *Session) Layer() *lipgloss.Layer {
130+// View implements [Dialog].
131+func (s *Session) View() string {
132132 titleStyle := s.com.Styles.Dialog.Title
133133 helpStyle := s.com.Styles.Dialog.HelpView
134134 dialogStyle := s.com.Styles.Dialog.View.Width(s.width)
@@ -156,7 +156,7 @@ func (s *Session) Layer() *lipgloss.Layer {
156156 helpStyle.Render(s.help.View(s)),
157157 }, "\n")
158158
159− return lipgloss.NewLayer(dialogStyle.Render(content))
159+ return dialogStyle.Render(content)
160160}
161161
162162// ShortHelp implements [help.KeyMap].
internal/ui/model/ui.go+1−13
@@ -535,19 +535,7 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) {
535535
536536 // This needs to come last to overlay on top of everything
537537 if m.dialog.HasDialogs() {
538− dialogLayers := m.dialog.Layers()
539− layers := make([]*lipgloss.Layer, 0)
540− for _, layer := range dialogLayers {
541− if layer == nil {
542− continue
543− }
544− layerW, layerH := layer.Width(), layer.Height()
545− layerArea := common.CenterRect(area, layerW, layerH)
546− layers = append(layers, layer.X(layerArea.Min.X).Y(layerArea.Min.Y))
547− }
548−
549− comp := lipgloss.NewCompositor(layers...)
550− comp.Draw(scr, area)
538+ m.dialog.Draw(scr, area)
551539 }
552540}
553541
internal/ui/styles/styles.go+1−1
@@ -8,10 +8,10 @@ import (
88 "charm.land/bubbles/v2/textarea"
99 "charm.land/bubbles/v2/textinput"
1010 tea "charm.land/bubbletea/v2"
11+ "charm.land/glamour/v2/ansi"
1112 "charm.land/lipgloss/v2"
1213 "github.com/alecthomas/chroma/v2"
1314 "github.com/charmbracelet/crush/internal/tui/exp/diffview"
14− "github.com/charmbracelet/glamour/v2/ansi"
1515 "github.com/charmbracelet/x/exp/charmtone"
1616)
1717