From 53d38c08edbabad0edd47d38a9b126d271b8301c Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 15 Dec 2025 10:19:56 -0500 Subject: [PATCH] refactor(ui): simplify dialog model and rendering --- internal/ui/common/markdown.go | 4 +-- internal/ui/dialog/dialog.go | 55 ++++++++++------------------------ internal/ui/dialog/quit.go | 20 ++++++------- internal/ui/dialog/sessions.go | 14 ++++----- internal/ui/model/ui.go | 14 +-------- internal/ui/styles/styles.go | 2 +- 6 files changed, 37 insertions(+), 72 deletions(-) diff --git a/internal/ui/common/markdown.go b/internal/ui/common/markdown.go index 3c90c2dc1582160c919f4fe432e78642a0a2c97d..361cbba2ff8bab34214f95980bd20b98a6ead62a 100644 --- a/internal/ui/common/markdown.go +++ b/internal/ui/common/markdown.go @@ -1,9 +1,9 @@ package common import ( + "charm.land/glamour/v2" + gstyles "charm.land/glamour/v2/styles" "github.com/charmbracelet/crush/internal/ui/styles" - "github.com/charmbracelet/glamour/v2" - gstyles "github.com/charmbracelet/glamour/v2/styles" ) // MarkdownRenderer returns a glamour [glamour.TermRenderer] configured with diff --git a/internal/ui/dialog/dialog.go b/internal/ui/dialog/dialog.go index 73c25151cf7edab317c3c52dd064bc1399598bd6..29175922b44015a31182c27e7411c91c47a7f31f 100644 --- a/internal/ui/dialog/dialog.go +++ b/internal/ui/dialog/dialog.go @@ -4,6 +4,8 @@ import ( "charm.land/bubbles/v2/key" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" + "github.com/charmbracelet/crush/internal/ui/common" + uv "github.com/charmbracelet/ultraviolet" ) // CloseKey is the default key binding to close dialogs. @@ -12,35 +14,11 @@ var CloseKey = key.NewBinding( key.WithHelp("esc", "exit"), ) -// OverlayKeyMap defines key bindings for dialogs. -type OverlayKeyMap struct { - Close key.Binding -} - -// ActionType represents the type of action taken by a dialog. -type ActionType int - -const ( - // ActionNone indicates no action. - ActionNone ActionType = iota - // ActionClose indicates that the dialog should be closed. - ActionClose - // ActionSelect indicates that an item has been selected. - ActionSelect -) - -// Action represents an action taken by a dialog. -// It can be used to signal closing or other operations. -type Action struct { - Type ActionType - Payload any -} - // Dialog is a component that can be displayed on top of the UI. type Dialog interface { ID() string - Update(msg tea.Msg) (Action, tea.Cmd) - Layer() *lipgloss.Layer + Update(msg tea.Msg) tea.Cmd + View() string } // Overlay manages multiple dialogs as an overlay. @@ -122,27 +100,26 @@ func (d *Overlay) Update(msg tea.Msg) (*Overlay, tea.Cmd) { } } - action, cmd := dialog.Update(msg) - switch action.Type { - case ActionClose: + if cmd := dialog.Update(msg); cmd != nil { // Close the current dialog d.removeDialog(idx) return d, cmd - case ActionSelect: - // Pass the action up (without modifying the dialog stack) - return d, cmd } - return d, cmd + return d, nil } -// Layers returns the current stack of dialogs as lipgloss layers. -func (d *Overlay) Layers() []*lipgloss.Layer { - layers := make([]*lipgloss.Layer, len(d.dialogs)) - for i, dialog := range d.dialogs { - layers[i] = dialog.Layer() +// Draw renders the overlay and its dialogs. +func (d *Overlay) Draw(scr uv.Screen, area uv.Rectangle) { + for _, dialog := range d.dialogs { + view := dialog.View() + viewWidth := lipgloss.Width(view) + viewHeight := lipgloss.Height(view) + center := common.CenterRect(area, viewWidth, viewHeight) + if area.Overlaps(center) { + uv.NewStyledString(view).Draw(scr, center) + } } - return layers } // removeDialog removes a dialog from the stack. diff --git a/internal/ui/dialog/quit.go b/internal/ui/dialog/quit.go index 77bcedaaa473fec6a7f79339ddba6ac92f594d34..142ac5f3974d88bb0ff8aceb9f7241ddde0f8377 100644 --- a/internal/ui/dialog/quit.go +++ b/internal/ui/dialog/quit.go @@ -73,30 +73,30 @@ func (*Quit) ID() string { } // Update implements [Model]. -func (q *Quit) Update(msg tea.Msg) (Action, tea.Cmd) { +func (q *Quit) Update(msg tea.Msg) tea.Cmd { switch msg := msg.(type) { case tea.KeyPressMsg: switch { case key.Matches(msg, q.keyMap.LeftRight, q.keyMap.Tab): q.selectedNo = !q.selectedNo - return Action{}, nil + return nil case key.Matches(msg, q.keyMap.EnterSpace): if !q.selectedNo { - return Action{}, tea.Quit + return tea.Quit } - return Action{}, nil + return nil case key.Matches(msg, q.keyMap.Yes): - return Action{}, tea.Quit + return tea.Quit case key.Matches(msg, q.keyMap.No, q.keyMap.Close): - return Action{}, nil + return nil } } - return Action{}, nil + return nil } -// Layer implements [Model]. -func (q *Quit) Layer() *lipgloss.Layer { +// View implements [Dialog]. +func (q *Quit) View() string { const question = "Are you sure you want to quit?" baseStyle := q.com.Styles.Base buttonOpts := []common.ButtonOpts{ @@ -113,7 +113,7 @@ func (q *Quit) Layer() *lipgloss.Layer { ), ) - return lipgloss.NewLayer(q.com.Styles.BorderFocus.Render(content)) + return q.com.Styles.BorderFocus.Render(content) } // ShortHelp implements [help.KeyMap]. diff --git a/internal/ui/dialog/sessions.go b/internal/ui/dialog/sessions.go index c526268cbb3d09509d3e086fcc73e557d272b3dd..edb0289ca26d1303b8ae14d3f6e9d662b66953b1 100644 --- a/internal/ui/dialog/sessions.go +++ b/internal/ui/dialog/sessions.go @@ -100,7 +100,7 @@ func (s *Session) ID() string { } // Update implements Dialog. -func (s *Session) Update(msg tea.Msg) (Action, tea.Cmd) { +func (s *Session) Update(msg tea.Msg) tea.Cmd { switch msg := msg.(type) { case tea.KeyPressMsg: switch { @@ -115,20 +115,20 @@ func (s *Session) Update(msg tea.Msg) (Action, tea.Cmd) { case key.Matches(msg, s.keyMap.Select): if item := s.list.SelectedItem(); item != nil { sessionItem := item.(*SessionItem) - return Action{Type: ActionSelect, Payload: sessionItem.Session}, SessionSelectCmd(sessionItem.Session) + return SessionSelectCmd(sessionItem.Session) } default: var cmd tea.Cmd s.input, cmd = s.input.Update(msg) s.list.SetFilter(s.input.Value()) - return Action{}, cmd + return cmd } } - return Action{}, nil + return nil } -// Layer implements Dialog. -func (s *Session) Layer() *lipgloss.Layer { +// View implements [Dialog]. +func (s *Session) View() string { titleStyle := s.com.Styles.Dialog.Title helpStyle := s.com.Styles.Dialog.HelpView dialogStyle := s.com.Styles.Dialog.View.Width(s.width) @@ -156,7 +156,7 @@ func (s *Session) Layer() *lipgloss.Layer { helpStyle.Render(s.help.View(s)), }, "\n") - return lipgloss.NewLayer(dialogStyle.Render(content)) + return dialogStyle.Render(content) } // ShortHelp implements [help.KeyMap]. diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index 9fe351283decaeecfedec63ff88c8ddbc5e98b4a..99aba0af48844fa7dc1b27b4a4a9f18b5f52b23d 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -535,19 +535,7 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) { // This needs to come last to overlay on top of everything if m.dialog.HasDialogs() { - dialogLayers := m.dialog.Layers() - layers := make([]*lipgloss.Layer, 0) - for _, layer := range dialogLayers { - if layer == nil { - continue - } - layerW, layerH := layer.Width(), layer.Height() - layerArea := common.CenterRect(area, layerW, layerH) - layers = append(layers, layer.X(layerArea.Min.X).Y(layerArea.Min.Y)) - } - - comp := lipgloss.NewCompositor(layers...) - comp.Draw(scr, area) + m.dialog.Draw(scr, area) } } diff --git a/internal/ui/styles/styles.go b/internal/ui/styles/styles.go index cd9df36c8a20713649bdade5405db8ecc855c221..8eb077db163fd3eea80eec5e6a4a625d3c3116d6 100644 --- a/internal/ui/styles/styles.go +++ b/internal/ui/styles/styles.go @@ -8,10 +8,10 @@ import ( "charm.land/bubbles/v2/textarea" "charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" + "charm.land/glamour/v2/ansi" "charm.land/lipgloss/v2" "github.com/alecthomas/chroma/v2" "github.com/charmbracelet/crush/internal/tui/exp/diffview" - "github.com/charmbracelet/glamour/v2/ansi" "github.com/charmbracelet/x/exp/charmtone" )