on main help sign in
refactor(ui): dialog: cleanup render logic and use RenderContext (#1871)
1f0605aAyman Bagabas
internal/ui/dialog/commands.go+12−11
@@ -9,7 +9,6 @@ import (
9 "charm.land/bubbles/v2/spinner"
9 "charm.land/bubbles/v2/spinner"
10 "charm.land/bubbles/v2/textinput"
10 "charm.land/bubbles/v2/textinput"
11 tea "charm.land/bubbletea/v2"
11 tea "charm.land/bubbletea/v2"
12− "charm.land/lipgloss/v2"
13 "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/catwalk/pkg/catwalk"
14 "github.com/charmbracelet/crush/internal/commands"
13 "github.com/charmbracelet/crush/internal/commands"
15 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/config"
@@ -17,7 +16,6 @@ import (
17 "github.com/charmbracelet/crush/internal/ui/list"
16 "github.com/charmbracelet/crush/internal/ui/list"
18 "github.com/charmbracelet/crush/internal/ui/styles"
17 "github.com/charmbracelet/crush/internal/ui/styles"
19 uv "github.com/charmbracelet/ultraviolet"
18 uv "github.com/charmbracelet/ultraviolet"
20− "github.com/charmbracelet/x/ansi"
21)
19)
22
20
23// CommandsID is the identifier for the commands dialog.
21// CommandsID is the identifier for the commands dialog.
@@ -247,6 +245,7 @@ func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
247 // we need to reset the command items when width changes
245 // we need to reset the command items when width changes
248 c.setCommandItems(c.selected)
246 c.setCommandItems(c.selected)
249 }
247 }
248+
250 innerWidth := width - c.com.Styles.Dialog.View.GetHorizontalFrameSize()
249 innerWidth := width - c.com.Styles.Dialog.View.GetHorizontalFrameSize()
251 heightOffset := t.Dialog.Title.GetVerticalFrameSize() + titleContentHeight +
250 heightOffset := t.Dialog.Title.GetVerticalFrameSize() + titleContentHeight +
252 t.Dialog.InputPrompt.GetVerticalFrameSize() + inputContentHeight +
251 t.Dialog.InputPrompt.GetVerticalFrameSize() + inputContentHeight +
@@ -257,18 +256,20 @@ func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
257 c.list.SetSize(innerWidth, height-heightOffset)
256 c.list.SetSize(innerWidth, height-heightOffset)
258 c.help.SetWidth(innerWidth)
257 c.help.SetWidth(innerWidth)
259
258
259+ rc := NewRenderContext(t, width)
260+ rc.Title = "Commands"
260− radio := commandsRadioView(t, c.selected, len(c.customCommands) > 0, len(c.mcpPrompts) > 0)
261+ rc.TitleInfo = commandsRadioView(t, c.selected, len(c.customCommands) > 0, len(c.mcpPrompts) > 0)
261− titleStyle := t.Dialog.Title
262+ inputView := t.Dialog.InputPrompt.Render(c.input.View())
262− dialogStyle := t.Dialog.View.Width(width)
263+ rc.AddPart(inputView)
263− headerOffset := lipgloss.Width(radio) + titleStyle.GetHorizontalFrameSize() + dialogStyle.GetHorizontalFrameSize()
264+ listView := t.Dialog.List.Height(c.list.Height()).Render(c.list.Render())
264− helpView := ansi.Truncate(c.help.View(c), innerWidth, "")
265+ rc.AddPart(listView)
265− header := common.DialogTitle(t, "Commands", width-headerOffset) + radio
266+ rc.Help = c.help.View(c)
266
267
267 if c.loading {
268 if c.loading {
268− helpView = t.Dialog.HelpView.Width(width).Render(c.spinner.View() + " Generating Prompt...")
269+ rc.Help = c.spinner.View() + " Generating Prompt..."
269 }
270 }
270− view := HeaderInputListHelpView(t, width, c.list.Height(), header,
271+
271− c.input.View(), c.list.Render(), helpView)
272+ view := rc.Render()
272
273
273 cur := c.Cursor()
274 cur := c.Cursor()
274 DrawCenterCursor(scr, area, view, cur)
275 DrawCenterCursor(scr, area, view, cur)
internal/ui/dialog/common.go+36−36
@@ -4,8 +4,10 @@ import (
4 "strings"
4 "strings"
5
5
6 tea "charm.land/bubbletea/v2"
6 tea "charm.land/bubbletea/v2"
7+ "charm.land/lipgloss/v2"
7 "github.com/charmbracelet/crush/internal/ui/common"
8 "github.com/charmbracelet/crush/internal/ui/common"
8 "github.com/charmbracelet/crush/internal/ui/styles"
9 "github.com/charmbracelet/crush/internal/ui/styles"
10+ "github.com/charmbracelet/x/ansi"
9)
11)
10
12
11// InputCursor adjusts the cursor position for an input field within a dialog.
13// InputCursor adjusts the cursor position for an input field within a dialog.
@@ -43,9 +45,15 @@ type RenderContext struct {
43 // Width is the total width of the dialog including any margins, borders,
45 // Width is the total width of the dialog including any margins, borders,
44 // and paddings.
46 // and paddings.
45 Width int
47 Width int
48+ // Gap is the gap between content parts. Zero means no gap.
49+ Gap int
46 // Title is the title of the dialog. This will be styled using the default
50 // Title is the title of the dialog. This will be styled using the default
47 // dialog title style and prepended to the content parts slice.
51 // dialog title style and prepended to the content parts slice.
48 Title string
52 Title string
53+ // TitleInfo is additional information to display next to the title. This
54+ // part is displayed as is, any styling must be applied before setting this
55+ // field.
56+ TitleInfo string
49 // Parts are the rendered parts of the dialog.
57 // Parts are the rendered parts of the dialog.
50 Parts []string
58 Parts []string
51 // Help is the help view content. This will be appended to the content parts
59 // Help is the help view content. This will be appended to the content parts
@@ -76,55 +84,47 @@ func (rc *RenderContext) Render() string {
76
84
77 parts := []string{}
85 parts := []string{}
78 if len(rc.Title) > 0 {
86 if len(rc.Title) > 0 {
87+ var titleInfoWidth int
88+ if len(rc.TitleInfo) > 0 {
89+ titleInfoWidth = lipgloss.Width(rc.TitleInfo)
90+ }
79 title := common.DialogTitle(rc.Styles, rc.Title,
91 title := common.DialogTitle(rc.Styles, rc.Title,
80 max(0, rc.Width-dialogStyle.GetHorizontalFrameSize()-
92 max(0, rc.Width-dialogStyle.GetHorizontalFrameSize()-
81− titleStyle.GetHorizontalFrameSize()))
93+ titleStyle.GetHorizontalFrameSize()-
94+ titleInfoWidth))
95+ if len(rc.TitleInfo) > 0 {
96+ title += rc.TitleInfo
97+ }
82− parts = append(parts, titleStyle.Render(title), "")
98+ parts = append(parts, titleStyle.Render(title))
99+ if rc.Gap > 0 {
100+ parts = append(parts, make([]string, rc.Gap)...)
101+ }
83 }
102 }
84
103
104+ if rc.Gap <= 0 {
105+ parts = append(parts, rc.Parts...)
106+ } else {
85− for i, p := range rc.Parts {
107+ for i, p := range rc.Parts {
86− if len(p) > 0 {
108+ if len(p) > 0 {
87− parts = append(parts, p)
109+ parts = append(parts, p)
88− }
110+ }
89− if i < len(rc.Parts)-1 {
111+ if i < len(rc.Parts)-1 {
90− parts = append(parts, "")
112+ parts = append(parts, make([]string, rc.Gap)...)
113+ }
91 }
114 }
92 }
115 }
93
116
94 if len(rc.Help) > 0 {
117 if len(rc.Help) > 0 {
118+ if rc.Gap > 0 {
95− parts = append(parts, "")
119+ parts = append(parts, make([]string, rc.Gap)...)
120+ }
96 helpStyle := rc.Styles.Dialog.HelpView
121 helpStyle := rc.Styles.Dialog.HelpView
97 helpStyle = helpStyle.Width(rc.Width - dialogStyle.GetHorizontalFrameSize())
122 helpStyle = helpStyle.Width(rc.Width - dialogStyle.GetHorizontalFrameSize())
98− parts = append(parts, helpStyle.Render(rc.Help))
123+ helpView := ansi.Truncate(helpStyle.Render(rc.Help), rc.Width, "")
124+ parts = append(parts, helpView)
99 }
125 }
100
126
101 content := strings.Join(parts, "\n")
127 content := strings.Join(parts, "\n")
102
128
103 return dialogStyle.Render(content)
129 return dialogStyle.Render(content)
104}
130}
105−
106−// HeaderInputListHelpView generates a view for dialogs with a header, input,
107−// list, and help sections.
108−func HeaderInputListHelpView(t *styles.Styles, width, listHeight int, header, input, list, help string) string {
109− rc := NewRenderContext(t, width)
110−
111− titleStyle := t.Dialog.Title
112− inputStyle := t.Dialog.InputPrompt
113− listStyle := t.Dialog.List.Height(listHeight)
114− listContent := listStyle.Render(list)
115−
116− if len(header) > 0 {
117− rc.AddPart(titleStyle.Render(header))
118− }
119− if len(input) > 0 {
120− rc.AddPart(inputStyle.Render(input))
121− }
122− if len(list) > 0 {
123− rc.AddPart(listContent)
124− }
125− if len(help) > 0 {
126− rc.Help = help
127− }
128−
129− return rc.Render()
130−}
internal/ui/dialog/filepicker.go+1
@@ -234,6 +234,7 @@ func (f *FilePicker) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
234
234
235 t := f.com.Styles
235 t := f.com.Styles
236 rc := NewRenderContext(t, width)
236 rc := NewRenderContext(t, width)
237+ rc.Gap = 1
237 rc.Title = "Add Image"
238 rc.Title = "Add Image"
238 rc.Help = f.help.View(f)
239 rc.Help = f.help.View(f)
239
240
internal/ui/dialog/models.go+9−14
@@ -10,13 +10,11 @@ import (
10 "charm.land/bubbles/v2/key"
10 "charm.land/bubbles/v2/key"
11 "charm.land/bubbles/v2/textinput"
11 "charm.land/bubbles/v2/textinput"
12 tea "charm.land/bubbletea/v2"
12 tea "charm.land/bubbletea/v2"
13− "charm.land/lipgloss/v2"
14 "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/catwalk/pkg/catwalk"
15 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/config"
16 "github.com/charmbracelet/crush/internal/ui/common"
15 "github.com/charmbracelet/crush/internal/ui/common"
17 "github.com/charmbracelet/crush/internal/uiutil"
16 "github.com/charmbracelet/crush/internal/uiutil"
18 uv "github.com/charmbracelet/ultraviolet"
17 uv "github.com/charmbracelet/ultraviolet"
19− "github.com/charmbracelet/x/ansi"
20)
18)
21
19
22// ModelType represents the type of model to select.
20// ModelType represents the type of model to select.
@@ -253,19 +251,16 @@ func (m *Models) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
253 m.list.SetSize(innerWidth, height-heightOffset)
251 m.list.SetSize(innerWidth, height-heightOffset)
254 m.help.SetWidth(innerWidth)
252 m.help.SetWidth(innerWidth)
255
253
256− titleStyle := t.Dialog.Title
254+ rc := NewRenderContext(t, width)
257− dialogStyle := t.Dialog.View
255+ rc.Title = "Switch Model"
256+ rc.TitleInfo = m.modelTypeRadioView()
257+ inputView := t.Dialog.InputPrompt.Render(m.input.View())
258+ rc.AddPart(inputView)
259+ listView := t.Dialog.List.Height(m.list.Height()).Render(m.list.Render())
260+ rc.AddPart(listView)
261+ rc.Help = m.help.View(m)
258
262
259− radios := m.modelTypeRadioView()
263+ view := rc.Render()
260−
261− headerOffset := lipgloss.Width(radios) + titleStyle.GetHorizontalFrameSize() +
262− dialogStyle.GetHorizontalFrameSize()
263−
264− header := common.DialogTitle(t, "Switch Model", width-headerOffset) + radios
265−
266− helpView := ansi.Truncate(m.help.View(m), innerWidth, "")
267− view := HeaderInputListHelpView(t, width, m.list.Height(), header,
268− m.input.View(), m.list.Render(), helpView)
269
264
270 cur := m.Cursor()
265 cur := m.Cursor()
271 DrawCenterCursor(scr, area, view, cur)
266 DrawCenterCursor(scr, area, view, cur)
internal/ui/dialog/sessions.go+8−9
@@ -10,7 +10,6 @@ import (
10 "github.com/charmbracelet/crush/internal/ui/common"
10 "github.com/charmbracelet/crush/internal/ui/common"
11 "github.com/charmbracelet/crush/internal/ui/list"
11 "github.com/charmbracelet/crush/internal/ui/list"
12 uv "github.com/charmbracelet/ultraviolet"
12 uv "github.com/charmbracelet/ultraviolet"
13− "github.com/charmbracelet/x/ansi"
14)
13)
15
14
16// SessionsID is the identifier for the session selector dialog.
15// SessionsID is the identifier for the session selector dialog.
@@ -154,15 +153,15 @@ func (s *Session) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
154 s.list.SetSize(innerWidth, height-heightOffset)
153 s.list.SetSize(innerWidth, height-heightOffset)
155 s.help.SetWidth(innerWidth)
154 s.help.SetWidth(innerWidth)
156
155
157− titleStyle := s.com.Styles.Dialog.Title
156+ rc := NewRenderContext(t, width)
158− dialogStyle := s.com.Styles.Dialog.View.Width(width)
157+ rc.Title = "Switch Session"
159− header := common.DialogTitle(s.com.Styles, "Switch Session",
158+ inputView := t.Dialog.InputPrompt.Render(s.input.View())
160− max(0, width-dialogStyle.GetHorizontalFrameSize()-
159+ rc.AddPart(inputView)
161− titleStyle.GetHorizontalFrameSize()))
160+ listView := t.Dialog.List.Height(s.list.Height()).Render(s.list.Render())
161+ rc.AddPart(listView)
162+ rc.Help = s.help.View(s)
162
163
163− helpView := ansi.Truncate(s.help.View(s), innerWidth, "")
164+ view := rc.Render()
164− view := HeaderInputListHelpView(s.com.Styles, width, s.list.Height(), header,
165− s.input.View(), s.list.Render(), helpView)
166
165
167 cur := s.Cursor()
166 cur := s.Cursor()
168 DrawCenterCursor(scr, area, view, cur)
167 DrawCenterCursor(scr, area, view, cur)