on main help sign in
refactor(ui): simplify dialog model and rendering
53d38c0Ayman Bagabas
internal/ui/common/markdown.go+2−2
@@ -1,9 +1,9 @@
1package common
1package common
2
2
3import (
3import (
4+ "charm.land/glamour/v2"
5+ gstyles "charm.land/glamour/v2/styles"
4 "github.com/charmbracelet/crush/internal/ui/styles"
6 "github.com/charmbracelet/crush/internal/ui/styles"
5− "github.com/charmbracelet/glamour/v2"
6− gstyles "github.com/charmbracelet/glamour/v2/styles"
7)
7)
8
8
9// MarkdownRenderer returns a glamour [glamour.TermRenderer] configured with
9// MarkdownRenderer returns a glamour [glamour.TermRenderer] configured with
internal/ui/dialog/dialog.go+16−39
@@ -4,6 +4,8 @@ import (
4 "charm.land/bubbles/v2/key"
4 "charm.land/bubbles/v2/key"
5 tea "charm.land/bubbletea/v2"
5 tea "charm.land/bubbletea/v2"
6 "charm.land/lipgloss/v2"
6 "charm.land/lipgloss/v2"
7+ "github.com/charmbracelet/crush/internal/ui/common"
8+ uv "github.com/charmbracelet/ultraviolet"
7)
9)
8
10
9// CloseKey is the default key binding to close dialogs.
11// CloseKey is the default key binding to close dialogs.
@@ -12,35 +14,11 @@ var CloseKey = key.NewBinding(
12 key.WithHelp("esc", "exit"),
14 key.WithHelp("esc", "exit"),
13)
15)
14
16
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−
39// Dialog is a component that can be displayed on top of the UI.
17// Dialog is a component that can be displayed on top of the UI.
40type Dialog interface {
18type Dialog interface {
41 ID() string
19 ID() string
42− Update(msg tea.Msg) (Action, tea.Cmd)
20+ Update(msg tea.Msg) tea.Cmd
43− Layer() *lipgloss.Layer
21+ View() string
44}
22}
45
23
46// Overlay manages multiple dialogs as an overlay.
24// Overlay manages multiple dialogs as an overlay.
@@ -122,27 +100,26 @@ func (d *Overlay) Update(msg tea.Msg) (*Overlay, tea.Cmd) {
122 }
100 }
123 }
101 }
124
102
125− action, cmd := dialog.Update(msg)
103+ if cmd := dialog.Update(msg); cmd != nil {
126− switch action.Type {
127− case ActionClose:
128 // Close the current dialog
104 // Close the current dialog
129 d.removeDialog(idx)
105 d.removeDialog(idx)
130 return d, cmd
106 return d, cmd
131− case ActionSelect:
132− // Pass the action up (without modifying the dialog stack)
133− return d, cmd
134 }
107 }
135
108
136− return d, cmd
109+ return d, nil
137}
110}
138
111
139−// Layers returns the current stack of dialogs as lipgloss layers.
112+// Draw renders the overlay and its dialogs.
140−func (d *Overlay) Layers() []*lipgloss.Layer {
113+func (d *Overlay) Draw(scr uv.Screen, area uv.Rectangle) {
141− layers := make([]*lipgloss.Layer, len(d.dialogs))
142− for i, dialog := range d.dialogs {
114+ for _, dialog := range d.dialogs {
143− layers[i] = dialog.Layer()
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+ }
144 }
122 }
145− return layers
146}
123}
147
124
148// removeDialog removes a dialog from the stack.
125// removeDialog removes a dialog from the stack.
internal/ui/dialog/quit.go+10−10
@@ -73,30 +73,30 @@ func (*Quit) ID() string {
73}
73}
74
74
75// Update implements [Model].
75// Update implements [Model].
76−func (q *Quit) Update(msg tea.Msg) (Action, tea.Cmd) {
76+func (q *Quit) Update(msg tea.Msg) tea.Cmd {
77 switch msg := msg.(type) {
77 switch msg := msg.(type) {
78 case tea.KeyPressMsg:
78 case tea.KeyPressMsg:
79 switch {
79 switch {
80 case key.Matches(msg, q.keyMap.LeftRight, q.keyMap.Tab):
80 case key.Matches(msg, q.keyMap.LeftRight, q.keyMap.Tab):
81 q.selectedNo = !q.selectedNo
81 q.selectedNo = !q.selectedNo
82− return Action{}, nil
82+ return nil
83 case key.Matches(msg, q.keyMap.EnterSpace):
83 case key.Matches(msg, q.keyMap.EnterSpace):
84 if !q.selectedNo {
84 if !q.selectedNo {
85− return Action{}, tea.Quit
85+ return tea.Quit
86 }
86 }
87− return Action{}, nil
87+ return nil
88 case key.Matches(msg, q.keyMap.Yes):
88 case key.Matches(msg, q.keyMap.Yes):
89− return Action{}, tea.Quit
89+ return tea.Quit
90 case key.Matches(msg, q.keyMap.No, q.keyMap.Close):
90 case key.Matches(msg, q.keyMap.No, q.keyMap.Close):
91− return Action{}, nil
91+ return nil
92 }
92 }
93 }
93 }
94
94
95− return Action{}, nil
95+ return nil
96}
96}
97
97
98−// Layer implements [Model].
98+// View implements [Dialog].
99−func (q *Quit) Layer() *lipgloss.Layer {
99+func (q *Quit) View() string {
100 const question = "Are you sure you want to quit?"
100 const question = "Are you sure you want to quit?"
101 baseStyle := q.com.Styles.Base
101 baseStyle := q.com.Styles.Base
102 buttonOpts := []common.ButtonOpts{
102 buttonOpts := []common.ButtonOpts{
@@ -113,7 +113,7 @@ func (q *Quit) Layer() *lipgloss.Layer {
113 ),
113 ),
114 )
114 )
115
115
116− return lipgloss.NewLayer(q.com.Styles.BorderFocus.Render(content))
116+ return q.com.Styles.BorderFocus.Render(content)
117}
117}
118
118
119// ShortHelp implements [help.KeyMap].
119// ShortHelp implements [help.KeyMap].
internal/ui/dialog/sessions.go+7−7
@@ -100,7 +100,7 @@ func (s *Session) ID() string {
100}
100}
101
101
102// Update implements Dialog.
102// Update implements Dialog.
103−func (s *Session) Update(msg tea.Msg) (Action, tea.Cmd) {
103+func (s *Session) Update(msg tea.Msg) tea.Cmd {
104 switch msg := msg.(type) {
104 switch msg := msg.(type) {
105 case tea.KeyPressMsg:
105 case tea.KeyPressMsg:
106 switch {
106 switch {
@@ -115,20 +115,20 @@ func (s *Session) Update(msg tea.Msg) (Action, tea.Cmd) {
115 case key.Matches(msg, s.keyMap.Select):
115 case key.Matches(msg, s.keyMap.Select):
116 if item := s.list.SelectedItem(); item != nil {
116 if item := s.list.SelectedItem(); item != nil {
117 sessionItem := item.(*SessionItem)
117 sessionItem := item.(*SessionItem)
118− return Action{Type: ActionSelect, Payload: sessionItem.Session}, SessionSelectCmd(sessionItem.Session)
118+ return SessionSelectCmd(sessionItem.Session)
119 }
119 }
120 default:
120 default:
121 var cmd tea.Cmd
121 var cmd tea.Cmd
122 s.input, cmd = s.input.Update(msg)
122 s.input, cmd = s.input.Update(msg)
123 s.list.SetFilter(s.input.Value())
123 s.list.SetFilter(s.input.Value())
124− return Action{}, cmd
124+ return cmd
125 }
125 }
126 }
126 }
127− return Action{}, nil
127+ return nil
128}
128}
129
129
130−// Layer implements Dialog.
130+// View implements [Dialog].
131−func (s *Session) Layer() *lipgloss.Layer {
131+func (s *Session) View() string {
132 titleStyle := s.com.Styles.Dialog.Title
132 titleStyle := s.com.Styles.Dialog.Title
133 helpStyle := s.com.Styles.Dialog.HelpView
133 helpStyle := s.com.Styles.Dialog.HelpView
134 dialogStyle := s.com.Styles.Dialog.View.Width(s.width)
134 dialogStyle := s.com.Styles.Dialog.View.Width(s.width)
@@ -156,7 +156,7 @@ func (s *Session) Layer() *lipgloss.Layer {
156 helpStyle.Render(s.help.View(s)),
156 helpStyle.Render(s.help.View(s)),
157 }, "\n")
157 }, "\n")
158
158
159− return lipgloss.NewLayer(dialogStyle.Render(content))
159+ return dialogStyle.Render(content)
160}
160}
161
161
162// ShortHelp implements [help.KeyMap].
162// 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) {
535
535
536 // This needs to come last to overlay on top of everything
536 // This needs to come last to overlay on top of everything
537 if m.dialog.HasDialogs() {
537 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)
551 }
539 }
552}
540}
553
541
internal/ui/styles/styles.go+1−1
@@ -8,10 +8,10 @@ import (
8 "charm.land/bubbles/v2/textarea"
8 "charm.land/bubbles/v2/textarea"
9 "charm.land/bubbles/v2/textinput"
9 "charm.land/bubbles/v2/textinput"
10 tea "charm.land/bubbletea/v2"
10 tea "charm.land/bubbletea/v2"
11+ "charm.land/glamour/v2/ansi"
11 "charm.land/lipgloss/v2"
12 "charm.land/lipgloss/v2"
12 "github.com/alecthomas/chroma/v2"
13 "github.com/alecthomas/chroma/v2"
13 "github.com/charmbracelet/crush/internal/tui/exp/diffview"
14 "github.com/charmbracelet/crush/internal/tui/exp/diffview"
14− "github.com/charmbracelet/glamour/v2/ansi"
15 "github.com/charmbracelet/x/exp/charmtone"
15 "github.com/charmbracelet/x/exp/charmtone"
16)
16)
17
17