diff --git a/internal/ui/dialog/commands.go b/internal/ui/dialog/commands.go index a6861a5c87707d7c0717ec4d3c50c1d995a528af..97addbba1036781840951f2503b8b7813a16d9eb 100644 --- a/internal/ui/dialog/commands.go +++ b/internal/ui/dialog/commands.go @@ -9,7 +9,6 @@ import ( "charm.land/bubbles/v2/spinner" "charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" - "charm.land/lipgloss/v2" "github.com/charmbracelet/catwalk/pkg/catwalk" "github.com/charmbracelet/crush/internal/commands" "github.com/charmbracelet/crush/internal/config" @@ -17,7 +16,6 @@ import ( "github.com/charmbracelet/crush/internal/ui/list" "github.com/charmbracelet/crush/internal/ui/styles" uv "github.com/charmbracelet/ultraviolet" - "github.com/charmbracelet/x/ansi" ) // CommandsID is the identifier for the commands dialog. @@ -247,6 +245,7 @@ func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { // we need to reset the command items when width changes c.setCommandItems(c.selected) } + innerWidth := width - c.com.Styles.Dialog.View.GetHorizontalFrameSize() heightOffset := t.Dialog.Title.GetVerticalFrameSize() + titleContentHeight + t.Dialog.InputPrompt.GetVerticalFrameSize() + inputContentHeight + @@ -257,18 +256,20 @@ func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { c.list.SetSize(innerWidth, height-heightOffset) c.help.SetWidth(innerWidth) - radio := commandsRadioView(t, c.selected, len(c.customCommands) > 0, len(c.mcpPrompts) > 0) - titleStyle := t.Dialog.Title - dialogStyle := t.Dialog.View.Width(width) - headerOffset := lipgloss.Width(radio) + titleStyle.GetHorizontalFrameSize() + dialogStyle.GetHorizontalFrameSize() - helpView := ansi.Truncate(c.help.View(c), innerWidth, "") - header := common.DialogTitle(t, "Commands", width-headerOffset) + radio + rc := NewRenderContext(t, width) + rc.Title = "Commands" + rc.TitleInfo = commandsRadioView(t, c.selected, len(c.customCommands) > 0, len(c.mcpPrompts) > 0) + inputView := t.Dialog.InputPrompt.Render(c.input.View()) + rc.AddPart(inputView) + listView := t.Dialog.List.Height(c.list.Height()).Render(c.list.Render()) + rc.AddPart(listView) + rc.Help = c.help.View(c) if c.loading { - helpView = t.Dialog.HelpView.Width(width).Render(c.spinner.View() + " Generating Prompt...") + rc.Help = c.spinner.View() + " Generating Prompt..." } - view := HeaderInputListHelpView(t, width, c.list.Height(), header, - c.input.View(), c.list.Render(), helpView) + + view := rc.Render() cur := c.Cursor() DrawCenterCursor(scr, area, view, cur) diff --git a/internal/ui/dialog/common.go b/internal/ui/dialog/common.go index 7c812e4223fab44b38a9b4a41099055d737ec4c2..76b75064670935715f03e0d732b9df5070b9e9da 100644 --- a/internal/ui/dialog/common.go +++ b/internal/ui/dialog/common.go @@ -4,8 +4,10 @@ import ( "strings" tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/charmbracelet/crush/internal/ui/common" "github.com/charmbracelet/crush/internal/ui/styles" + "github.com/charmbracelet/x/ansi" ) // InputCursor adjusts the cursor position for an input field within a dialog. @@ -43,9 +45,15 @@ type RenderContext struct { // Width is the total width of the dialog including any margins, borders, // and paddings. Width int + // Gap is the gap between content parts. Zero means no gap. + Gap int // Title is the title of the dialog. This will be styled using the default // dialog title style and prepended to the content parts slice. Title string + // TitleInfo is additional information to display next to the title. This + // part is displayed as is, any styling must be applied before setting this + // field. + TitleInfo string // Parts are the rendered parts of the dialog. Parts []string // Help is the help view content. This will be appended to the content parts @@ -76,55 +84,47 @@ func (rc *RenderContext) Render() string { parts := []string{} if len(rc.Title) > 0 { + var titleInfoWidth int + if len(rc.TitleInfo) > 0 { + titleInfoWidth = lipgloss.Width(rc.TitleInfo) + } title := common.DialogTitle(rc.Styles, rc.Title, max(0, rc.Width-dialogStyle.GetHorizontalFrameSize()- - titleStyle.GetHorizontalFrameSize())) - parts = append(parts, titleStyle.Render(title), "") + titleStyle.GetHorizontalFrameSize()- + titleInfoWidth)) + if len(rc.TitleInfo) > 0 { + title += rc.TitleInfo + } + parts = append(parts, titleStyle.Render(title)) + if rc.Gap > 0 { + parts = append(parts, make([]string, rc.Gap)...) + } } - for i, p := range rc.Parts { - if len(p) > 0 { - parts = append(parts, p) - } - if i < len(rc.Parts)-1 { - parts = append(parts, "") + if rc.Gap <= 0 { + parts = append(parts, rc.Parts...) + } else { + for i, p := range rc.Parts { + if len(p) > 0 { + parts = append(parts, p) + } + if i < len(rc.Parts)-1 { + parts = append(parts, make([]string, rc.Gap)...) + } } } if len(rc.Help) > 0 { - parts = append(parts, "") + if rc.Gap > 0 { + parts = append(parts, make([]string, rc.Gap)...) + } helpStyle := rc.Styles.Dialog.HelpView helpStyle = helpStyle.Width(rc.Width - dialogStyle.GetHorizontalFrameSize()) - parts = append(parts, helpStyle.Render(rc.Help)) + helpView := ansi.Truncate(helpStyle.Render(rc.Help), rc.Width, "") + parts = append(parts, helpView) } content := strings.Join(parts, "\n") return dialogStyle.Render(content) } - -// HeaderInputListHelpView generates a view for dialogs with a header, input, -// list, and help sections. -func HeaderInputListHelpView(t *styles.Styles, width, listHeight int, header, input, list, help string) string { - rc := NewRenderContext(t, width) - - titleStyle := t.Dialog.Title - inputStyle := t.Dialog.InputPrompt - listStyle := t.Dialog.List.Height(listHeight) - listContent := listStyle.Render(list) - - if len(header) > 0 { - rc.AddPart(titleStyle.Render(header)) - } - if len(input) > 0 { - rc.AddPart(inputStyle.Render(input)) - } - if len(list) > 0 { - rc.AddPart(listContent) - } - if len(help) > 0 { - rc.Help = help - } - - return rc.Render() -} diff --git a/internal/ui/dialog/filepicker.go b/internal/ui/dialog/filepicker.go index 8bfdbabd12f54911a7da85842152a17f0ade275a..a099cd2ff9fe6f707ac1e18c8470dba55794889c 100644 --- a/internal/ui/dialog/filepicker.go +++ b/internal/ui/dialog/filepicker.go @@ -234,6 +234,7 @@ func (f *FilePicker) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { t := f.com.Styles rc := NewRenderContext(t, width) + rc.Gap = 1 rc.Title = "Add Image" rc.Help = f.help.View(f) diff --git a/internal/ui/dialog/models.go b/internal/ui/dialog/models.go index c12c78e1f4753c01f80653bb6ee5e5013fc9ea09..543e610d013e58a71447814aedd22841aaa6bf2a 100644 --- a/internal/ui/dialog/models.go +++ b/internal/ui/dialog/models.go @@ -10,13 +10,11 @@ import ( "charm.land/bubbles/v2/key" "charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" - "charm.land/lipgloss/v2" "github.com/charmbracelet/catwalk/pkg/catwalk" "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/ui/common" "github.com/charmbracelet/crush/internal/uiutil" uv "github.com/charmbracelet/ultraviolet" - "github.com/charmbracelet/x/ansi" ) // ModelType represents the type of model to select. @@ -253,19 +251,16 @@ func (m *Models) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { m.list.SetSize(innerWidth, height-heightOffset) m.help.SetWidth(innerWidth) - titleStyle := t.Dialog.Title - dialogStyle := t.Dialog.View + rc := NewRenderContext(t, width) + rc.Title = "Switch Model" + rc.TitleInfo = m.modelTypeRadioView() + inputView := t.Dialog.InputPrompt.Render(m.input.View()) + rc.AddPart(inputView) + listView := t.Dialog.List.Height(m.list.Height()).Render(m.list.Render()) + rc.AddPart(listView) + rc.Help = m.help.View(m) - radios := m.modelTypeRadioView() - - headerOffset := lipgloss.Width(radios) + titleStyle.GetHorizontalFrameSize() + - dialogStyle.GetHorizontalFrameSize() - - header := common.DialogTitle(t, "Switch Model", width-headerOffset) + radios - - helpView := ansi.Truncate(m.help.View(m), innerWidth, "") - view := HeaderInputListHelpView(t, width, m.list.Height(), header, - m.input.View(), m.list.Render(), helpView) + view := rc.Render() cur := m.Cursor() DrawCenterCursor(scr, area, view, cur) diff --git a/internal/ui/dialog/sessions.go b/internal/ui/dialog/sessions.go index 7a4725fcb9fac33d349dd5d6d7812e8f70c00eaa..a70d13ce58fed2ddf1b292d30e405362cf093569 100644 --- a/internal/ui/dialog/sessions.go +++ b/internal/ui/dialog/sessions.go @@ -10,7 +10,6 @@ import ( "github.com/charmbracelet/crush/internal/ui/common" "github.com/charmbracelet/crush/internal/ui/list" uv "github.com/charmbracelet/ultraviolet" - "github.com/charmbracelet/x/ansi" ) // 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 { s.list.SetSize(innerWidth, height-heightOffset) s.help.SetWidth(innerWidth) - titleStyle := s.com.Styles.Dialog.Title - dialogStyle := s.com.Styles.Dialog.View.Width(width) - header := common.DialogTitle(s.com.Styles, "Switch Session", - max(0, width-dialogStyle.GetHorizontalFrameSize()- - titleStyle.GetHorizontalFrameSize())) + rc := NewRenderContext(t, width) + rc.Title = "Switch Session" + inputView := t.Dialog.InputPrompt.Render(s.input.View()) + rc.AddPart(inputView) + listView := t.Dialog.List.Height(s.list.Height()).Render(s.list.Render()) + rc.AddPart(listView) + rc.Help = s.help.View(s) - helpView := ansi.Truncate(s.help.View(s), innerWidth, "") - view := HeaderInputListHelpView(s.com.Styles, width, s.list.Height(), header, - s.input.View(), s.list.Render(), helpView) + view := rc.Render() cur := s.Cursor() DrawCenterCursor(scr, area, view, cur)