Detailed changes
@@ -18,6 +18,7 @@ import (
"github.com/charmbracelet/crush/internal/message"
"github.com/charmbracelet/crush/internal/ui/common"
"github.com/charmbracelet/crush/internal/ui/list"
+ "github.com/charmbracelet/crush/internal/ui/styles"
"github.com/charmbracelet/crush/internal/uicmd"
"github.com/charmbracelet/crush/internal/uiutil"
)
@@ -32,24 +33,6 @@ type SendMsg struct {
Attachments []message.Attachment
}
-// Messages for commands
-type (
- SwitchSessionsMsg struct{}
- NewSessionsMsg struct{}
- SwitchModelMsg struct{}
- QuitMsg struct{}
- OpenFilePickerMsg struct{}
- ToggleHelpMsg struct{}
- ToggleCompactModeMsg struct{}
- ToggleThinkingMsg struct{}
- OpenReasoningDialogMsg struct{}
- OpenExternalEditorMsg struct{}
- ToggleYoloModeMsg struct{}
- CompactMsg struct {
- SessionID string
- }
-)
-
// Commands represents a dialog that shows available commands.
type Commands struct {
com *common.Common
@@ -149,10 +132,12 @@ func (c *Commands) ID() string {
}
// Update implements Dialog.
-func (c *Commands) Update(msg tea.Msg) tea.Cmd {
+func (c *Commands) Update(msg tea.Msg) tea.Msg {
switch msg := msg.(type) {
case tea.KeyPressMsg:
switch {
+ case key.Matches(msg, c.keyMap.Close):
+ return CloseMsg{}
case key.Matches(msg, c.keyMap.Previous):
c.list.Focus()
c.list.SelectPrev()
@@ -175,8 +160,10 @@ func (c *Commands) Update(msg tea.Msg) tea.Cmd {
default:
var cmd tea.Cmd
c.input, cmd = c.input.Update(msg)
- // Update the list filter
- c.list.SetFilter(c.input.Value())
+ value := c.input.Value()
+ c.list.SetFilter(value)
+ c.list.ScrollToTop()
+ c.list.SetSelected(0)
return cmd
}
}
@@ -195,14 +182,17 @@ func (c *Commands) ReloadMCPPrompts() tea.Cmd {
// Cursor returns the cursor position relative to the dialog.
func (c *Commands) Cursor() *tea.Cursor {
- return c.input.Cursor()
+ return InputCursor(c.com.Styles, c.input.Cursor())
}
-// View implements [Dialog].
-func (c *Commands) View() string {
- t := c.com.Styles
+// radioView generates the command type selector radio buttons.
+func radioView(t *styles.Styles, selected uicmd.CommandType, hasUserCmds bool, hasMCPPrompts bool) string {
+ if !hasUserCmds && !hasMCPPrompts {
+ return ""
+ }
+
selectedFn := func(t uicmd.CommandType) string {
- if t == c.selected {
+ if t == selected {
return "◉ " + t.String()
}
return "○ " + t.String()
@@ -211,46 +201,27 @@ func (c *Commands) View() string {
parts := []string{
selectedFn(uicmd.SystemCommands),
}
- if len(c.userCmds) > 0 {
+ if hasUserCmds {
parts = append(parts, selectedFn(uicmd.UserCommands))
}
- if c.mcpPrompts.Len() > 0 {
+ if hasMCPPrompts {
parts = append(parts, selectedFn(uicmd.MCPPrompts))
}
radio := strings.Join(parts, " ")
- radio = t.Dialog.Commands.CommandTypeSelector.Render(radio)
- if len(c.userCmds) > 0 || c.mcpPrompts.Len() > 0 {
- radio = " " + radio
- }
+ return t.Dialog.Commands.CommandTypeSelector.Render(radio)
+}
+// View implements [Dialog].
+func (c *Commands) View() string {
+ t := c.com.Styles
+ radio := radioView(t, c.selected, len(c.userCmds) > 0, c.mcpPrompts.Len() > 0)
titleStyle := t.Dialog.Title
- helpStyle := t.Dialog.HelpView
dialogStyle := t.Dialog.View.Width(c.width)
- inputStyle := t.Dialog.InputPrompt
- helpStyle = helpStyle.Width(c.width - dialogStyle.GetHorizontalFrameSize())
-
headerOffset := lipgloss.Width(radio) + titleStyle.GetHorizontalFrameSize() + dialogStyle.GetHorizontalFrameSize()
header := common.DialogTitle(t, "Commands", c.width-headerOffset) + radio
- title := titleStyle.Render(header)
- help := helpStyle.Render(c.help.View(c))
- listContent := c.list.Render()
- if nlines := lipgloss.Height(listContent); nlines < c.list.Height() {
- // pad the list content to avoid jumping when navigating
- listContent += strings.Repeat("\n", max(0, c.list.Height()-nlines))
- }
-
- content := strings.Join([]string{
- title,
- "",
- inputStyle.Render(c.input.View()),
- "",
- c.list.Render(),
- "",
- help,
- }, "\n")
-
- return dialogStyle.Render(content)
+ return HeaderInputListHelpView(t, c.width, c.list.Height(), header,
+ c.input.View(), c.list.Render(), c.help.View(c))
}
// ShortHelp implements [help.KeyMap].
@@ -316,7 +287,9 @@ func (c *Commands) setCommandType(commandType uicmd.CommandType) {
}
c.list.SetItems(commandItems...)
- // Reset selection and filter
+ c.list.SetSelected(0)
+ c.list.SetFilter("")
+ c.list.ScrollToTop()
c.list.SetSelected(0)
c.input.SetValue("")
}
@@ -485,7 +458,7 @@ func (c *Commands) defaultCommands() []uicmd.Command {
Description: "Quit",
Shortcut: "ctrl+c",
Handler: func(cmd uicmd.Command) tea.Cmd {
- return uiutil.CmdHandler(QuitMsg{})
+ return uiutil.CmdHandler(tea.QuitMsg{})
},
},
}...)
@@ -0,0 +1,56 @@
+package dialog
+
+import (
+ "strings"
+
+ tea "charm.land/bubbletea/v2"
+ "github.com/charmbracelet/crush/internal/ui/styles"
+)
+
+// InputCursor adjusts the cursor position for an input field within a dialog.
+func InputCursor(t *styles.Styles, cur *tea.Cursor) *tea.Cursor {
+ if cur != nil {
+ titleStyle := t.Dialog.Title
+ dialogStyle := t.Dialog.View
+ inputStyle := t.Dialog.InputPrompt
+ // Adjust cursor position to account for dialog layout
+ cur.X += inputStyle.GetBorderLeftSize() +
+ inputStyle.GetMarginLeft() +
+ inputStyle.GetPaddingLeft() +
+ dialogStyle.GetBorderLeftSize() +
+ dialogStyle.GetPaddingLeft() +
+ dialogStyle.GetMarginLeft()
+ cur.Y += titleStyle.GetVerticalFrameSize() +
+ inputStyle.GetBorderTopSize() +
+ inputStyle.GetMarginTop() +
+ inputStyle.GetPaddingTop() +
+ inputStyle.GetBorderBottomSize() +
+ inputStyle.GetMarginBottom() +
+ inputStyle.GetPaddingBottom() +
+ dialogStyle.GetPaddingTop() +
+ dialogStyle.GetMarginTop() +
+ dialogStyle.GetBorderTopSize()
+ }
+ return cur
+}
+
+// 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 {
+ titleStyle := t.Dialog.Title
+ helpStyle := t.Dialog.HelpView
+ dialogStyle := t.Dialog.View.Width(width)
+ inputStyle := t.Dialog.InputPrompt
+ helpStyle = helpStyle.Width(width - dialogStyle.GetHorizontalFrameSize())
+ listStyle := t.Dialog.List.Height(listHeight)
+ listContent := listStyle.Render(list)
+
+ content := strings.Join([]string{
+ titleStyle.Render(header),
+ inputStyle.Render(input),
+ listContent,
+ helpStyle.Render(help),
+ }, "\n")
+
+ return dialogStyle.Render(content)
+}
@@ -17,7 +17,7 @@ var CloseKey = key.NewBinding(
// Dialog is a component that can be displayed on top of the UI.
type Dialog interface {
ID() string
- Update(msg tea.Msg) tea.Cmd
+ Update(msg tea.Msg) tea.Msg
View() string
}
@@ -71,6 +71,14 @@ func (d *Overlay) RemoveDialog(dialogID string) {
}
}
+// RemoveFrontDialog removes the front dialog from the stack.
+func (d *Overlay) RemoveFrontDialog() {
+ if len(d.dialogs) == 0 {
+ return
+ }
+ d.removeDialog(len(d.dialogs) - 1)
+}
+
// Dialog returns the dialog with the specified ID, or nil if not found.
func (d *Overlay) Dialog(dialogID string) Dialog {
for _, dialog := range d.dialogs {
@@ -81,6 +89,14 @@ func (d *Overlay) Dialog(dialogID string) Dialog {
return nil
}
+// DialogLast returns the front dialog, or nil if there are no dialogs.
+func (d *Overlay) DialogLast() Dialog {
+ if len(d.dialogs) == 0 {
+ return nil
+ }
+ return d.dialogs[len(d.dialogs)-1]
+}
+
// BringToFront brings the dialog with the specified ID to the front.
func (d *Overlay) BringToFront(dialogID string) {
for i, dialog := range d.dialogs {
@@ -94,38 +110,40 @@ func (d *Overlay) BringToFront(dialogID string) {
}
// Update handles dialog updates.
-func (d *Overlay) Update(msg tea.Msg) (*Overlay, tea.Cmd) {
+func (d *Overlay) Update(msg tea.Msg) tea.Msg {
if len(d.dialogs) == 0 {
- return d, nil
+ return nil
}
idx := len(d.dialogs) - 1 // active dialog is the last one
dialog := d.dialogs[idx]
- switch msg := msg.(type) {
- case tea.KeyPressMsg:
- if key.Matches(msg, CloseKey) {
- // Close the current dialog
- d.removeDialog(idx)
- return d, nil
- }
+ if dialog == nil {
+ return nil
}
- if cmd := dialog.Update(msg); cmd != nil {
- // Close the current dialog
- d.removeDialog(idx)
- return d, cmd
+ return dialog.Update(msg)
+}
+
+// CenterPosition calculates the centered position for the dialog.
+func (d *Overlay) CenterPosition(area uv.Rectangle, dialogID string) uv.Rectangle {
+ dialog := d.Dialog(dialogID)
+ if dialog == nil {
+ return uv.Rectangle{}
}
+ return d.centerPositionView(area, dialog.View())
+}
- return d, nil
+func (d *Overlay) centerPositionView(area uv.Rectangle, view string) uv.Rectangle {
+ viewWidth := lipgloss.Width(view)
+ viewHeight := lipgloss.Height(view)
+ return common.CenterRect(area, viewWidth, viewHeight)
}
// 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)
+ center := d.centerPositionView(area, view)
if area.Overlaps(center) {
uv.NewStyledString(view).Draw(scr, center)
}
@@ -0,0 +1,34 @@
+package dialog
+
+import (
+ tea "charm.land/bubbletea/v2"
+ "github.com/charmbracelet/crush/internal/session"
+)
+
+// CloseMsg is a message to close the current dialog.
+type CloseMsg struct{}
+
+// QuitMsg is a message to quit the application.
+type QuitMsg = tea.QuitMsg
+
+// SessionSelectedMsg is a message indicating a session has been selected.
+type SessionSelectedMsg struct {
+ Session session.Session
+}
+
+// Messages for commands
+type (
+ SwitchSessionsMsg struct{}
+ NewSessionsMsg struct{}
+ SwitchModelMsg struct{}
+ OpenFilePickerMsg struct{}
+ ToggleHelpMsg struct{}
+ ToggleCompactModeMsg struct{}
+ ToggleThinkingMsg struct{}
+ OpenReasoningDialogMsg struct{}
+ OpenExternalEditorMsg struct{}
+ ToggleYoloModeMsg struct{}
+ CompactMsg struct {
+ SessionID string
+ }
+)
@@ -60,22 +60,24 @@ func (*Quit) ID() string {
}
// Update implements [Model].
-func (q *Quit) Update(msg tea.Msg) tea.Cmd {
+func (q *Quit) Update(msg tea.Msg) tea.Msg {
switch msg := msg.(type) {
case tea.KeyPressMsg:
switch {
+ case key.Matches(msg, q.keyMap.Close):
+ return CloseMsg{}
case key.Matches(msg, q.keyMap.LeftRight, q.keyMap.Tab):
q.selectedNo = !q.selectedNo
- return nil
+ return CloseMsg{}
case key.Matches(msg, q.keyMap.EnterSpace):
if !q.selectedNo {
- return tea.Quit
+ return QuitMsg{}
}
- return nil
+ return CloseMsg{}
case key.Matches(msg, q.keyMap.Yes):
- return tea.Quit
+ return QuitMsg{}
case key.Matches(msg, q.keyMap.No, q.keyMap.Close):
- return nil
+ return CloseMsg{}
}
}
@@ -1,13 +1,10 @@
package dialog
import (
- "strings"
-
"charm.land/bubbles/v2/help"
"charm.land/bubbles/v2/key"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
- "charm.land/lipgloss/v2"
"github.com/charmbracelet/crush/internal/session"
"github.com/charmbracelet/crush/internal/ui/common"
"github.com/charmbracelet/crush/internal/ui/list"
@@ -34,11 +31,6 @@ type Session struct {
var _ Dialog = (*Session)(nil)
-// SessionSelectedMsg is a message sent when a session is selected.
-type SessionSelectedMsg struct {
- Session session.Session
-}
-
// NewSessions creates a new Session dialog.
func NewSessions(com *common.Common, sessions ...session.Session) *Session {
s := new(Session)
@@ -73,11 +65,6 @@ func NewSessions(com *common.Common, sessions ...session.Session) *Session {
return s
}
-// Cursor returns the cursor position relative to the dialog.
-func (s *Session) Cursor() *tea.Cursor {
- return s.input.Cursor()
-}
-
// SetSize sets the size of the dialog.
func (s *Session) SetSize(width, height int) {
s.width = width
@@ -94,10 +81,12 @@ func (s *Session) ID() string {
}
// Update implements Dialog.
-func (s *Session) Update(msg tea.Msg) tea.Cmd {
+func (s *Session) Update(msg tea.Msg) tea.Msg {
switch msg := msg.(type) {
case tea.KeyPressMsg:
switch {
+ case key.Matches(msg, s.keyMap.Close):
+ return CloseMsg{}
case key.Matches(msg, s.keyMap.Previous):
s.list.Focus()
s.list.SelectPrev()
@@ -109,48 +98,36 @@ func (s *Session) Update(msg tea.Msg) tea.Cmd {
case key.Matches(msg, s.keyMap.Select):
if item := s.list.SelectedItem(); item != nil {
sessionItem := item.(*SessionItem)
- return SessionSelectCmd(sessionItem.Session)
+ return SessionSelectedMsg{sessionItem.Session}
}
default:
var cmd tea.Cmd
s.input, cmd = s.input.Update(msg)
- s.list.SetFilter(s.input.Value())
+ value := s.input.Value()
+ s.list.SetFilter(value)
+ s.list.ScrollToTop()
+ s.list.SetSelected(0)
return cmd
}
}
return nil
}
+// Cursor returns the cursor position relative to the dialog.
+func (s *Session) Cursor() *tea.Cursor {
+ return InputCursor(s.com.Styles, s.input.Cursor())
+}
+
// 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)
- inputStyle := s.com.Styles.Dialog.InputPrompt
- helpStyle = helpStyle.Width(s.width - dialogStyle.GetHorizontalFrameSize())
- listContent := s.list.Render()
- if nlines := lipgloss.Height(listContent); nlines < s.list.Height() {
- // pad the list content to avoid jumping when navigating
- listContent += strings.Repeat("\n", max(0, s.list.Height()-nlines))
- }
+ header := common.DialogTitle(s.com.Styles, "Switch Session",
+ max(0, s.width-dialogStyle.GetHorizontalFrameSize()-
+ titleStyle.GetHorizontalFrameSize()))
- content := strings.Join([]string{
- titleStyle.Render(
- common.DialogTitle(
- s.com.Styles,
- "Switch Session",
- max(0, s.width-
- dialogStyle.GetHorizontalFrameSize()-
- titleStyle.GetHorizontalFrameSize()))),
- "",
- inputStyle.Render(s.input.View()),
- "",
- listContent,
- "",
- helpStyle.Render(s.help.View(s)),
- }, "\n")
-
- return dialogStyle.Render(content)
+ return HeaderInputListHelpView(s.com.Styles, s.width, s.list.Height(), header,
+ s.input.View(), s.list.Render(), s.help.View(s))
}
// ShortHelp implements [help.KeyMap].
@@ -181,10 +158,3 @@ func (s *Session) FullHelp() [][]key.Binding {
}
return m
}
-
-// SessionSelectCmd creates a command that sends a SessionSelectMsg.
-func SessionSelectCmd(s session.Session) tea.Cmd {
- return func() tea.Msg {
- return SessionSelectedMsg{Session: s}
- }
-}
@@ -20,11 +20,11 @@ import (
"github.com/charmbracelet/crush/internal/message"
"github.com/charmbracelet/crush/internal/pubsub"
"github.com/charmbracelet/crush/internal/session"
- "github.com/charmbracelet/crush/internal/tui/util"
"github.com/charmbracelet/crush/internal/ui/common"
"github.com/charmbracelet/crush/internal/ui/dialog"
"github.com/charmbracelet/crush/internal/ui/logo"
"github.com/charmbracelet/crush/internal/ui/styles"
+ "github.com/charmbracelet/crush/internal/uiutil"
"github.com/charmbracelet/crush/internal/version"
uv "github.com/charmbracelet/ultraviolet"
"github.com/charmbracelet/ultraviolet/screen"
@@ -194,12 +194,6 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// TODO: Get. Rid. Of. Magic numbers!
sessions.SetSize(min(120, m.width-8), 30)
m.dialog.AddDialog(sessions)
- case dialog.SessionSelectedMsg:
- m.dialog.RemoveDialog(dialog.SessionsID)
- cmds = append(cmds,
- m.loadSession(msg.Session.ID),
- m.loadSessionFiles(msg.Session.ID),
- )
case sessionLoadedMsg:
m.state = uiChat
m.session = &msg.sess
@@ -330,15 +324,6 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
case tea.KeyPressMsg:
cmds = append(cmds, m.handleKeyPressMsg(msg)...)
-
- // Command dialog messages
- // TODO: Properly structure and handle these messages
- case dialog.ToggleYoloModeMsg:
- m.com.App.Permissions.SetSkipRequests(!m.com.App.Permissions.SkipRequests())
- m.dialog.RemoveDialog(dialog.CommandsID)
- case dialog.SwitchSessionsMsg:
- cmds = append(cmds, m.loadSessionsCmd)
- m.dialog.RemoveDialog(dialog.CommandsID)
}
// This logic gets triggered on any message type, but should it?
@@ -384,7 +369,6 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
return true
}
switch {
- case key.Matches(msg, m.keyMap.Tab):
case key.Matches(msg, m.keyMap.Help):
m.help.ShowAll = !m.help.ShowAll
m.updateLayoutAndSize()
@@ -400,7 +384,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
}
commands, err := dialog.NewCommands(m.com, sessionID)
if err != nil {
- cmds = append(cmds, util.ReportError(err))
+ cmds = append(cmds, uiutil.ReportError(err))
} else {
// TODO: Get. Rid. Of. Magic numbers!
commands.SetSize(min(120, m.width-8), 30)
@@ -427,64 +411,101 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
return cmds
}
- updatedDialog, cmd := m.dialog.Update(msg)
- m.dialog = updatedDialog
- if cmd != nil {
- cmds = append(cmds, cmd)
+ msg := m.dialog.Update(msg)
+ if msg == nil {
+ return cmds
}
+
+ switch msg := msg.(type) {
+ // Generic dialog messages
+ case dialog.CloseMsg:
+ m.dialog.RemoveFrontDialog()
+ // Session dialog messages
+ case dialog.SessionSelectedMsg:
+ m.dialog.RemoveDialog(dialog.SessionsID)
+ cmds = append(cmds,
+ m.loadSession(msg.Session.ID),
+ m.loadSessionFiles(msg.Session.ID),
+ )
+ // Command dialog messages
+ case dialog.ToggleYoloModeMsg:
+ m.com.App.Permissions.SetSkipRequests(!m.com.App.Permissions.SkipRequests())
+ m.dialog.RemoveDialog(dialog.CommandsID)
+ case dialog.SwitchSessionsMsg:
+ cmds = append(cmds, m.loadSessionsCmd)
+ m.dialog.RemoveDialog(dialog.CommandsID)
+ case dialog.CompactMsg:
+ err := m.com.App.AgentCoordinator.Summarize(context.Background(), msg.SessionID)
+ if err != nil {
+ cmds = append(cmds, uiutil.ReportError(err))
+ }
+ case dialog.ToggleHelpMsg:
+ m.help.ShowAll = !m.help.ShowAll
+ case dialog.QuitMsg:
+ cmds = append(cmds, tea.Quit)
+ }
+
return cmds
}
switch m.state {
case uiChat:
- switch {
- case key.Matches(msg, m.keyMap.Tab):
- if m.focus == uiFocusMain {
- m.focus = uiFocusEditor
- cmds = append(cmds, m.textarea.Focus())
- m.chat.Blur()
- } else {
+ switch m.focus {
+ case uiFocusEditor:
+ switch {
+ case key.Matches(msg, m.keyMap.Tab):
m.focus = uiFocusMain
m.textarea.Blur()
m.chat.Focus()
m.chat.SetSelected(m.chat.Len() - 1)
+ default:
+ handleGlobalKeys(msg)
}
- case key.Matches(msg, m.keyMap.Chat.Up):
- m.chat.ScrollBy(-1)
- if !m.chat.SelectedItemInView() {
+ case uiFocusMain:
+ switch {
+ case key.Matches(msg, m.keyMap.Tab):
+ m.focus = uiFocusEditor
+ cmds = append(cmds, m.textarea.Focus())
+ m.chat.Blur()
+ case key.Matches(msg, m.keyMap.Chat.Up):
+ m.chat.ScrollBy(-1)
+ if !m.chat.SelectedItemInView() {
+ m.chat.SelectPrev()
+ m.chat.ScrollToSelected()
+ }
+ case key.Matches(msg, m.keyMap.Chat.Down):
+ m.chat.ScrollBy(1)
+ if !m.chat.SelectedItemInView() {
+ m.chat.SelectNext()
+ m.chat.ScrollToSelected()
+ }
+ case key.Matches(msg, m.keyMap.Chat.UpOneItem):
m.chat.SelectPrev()
m.chat.ScrollToSelected()
- }
- case key.Matches(msg, m.keyMap.Chat.Down):
- m.chat.ScrollBy(1)
- if !m.chat.SelectedItemInView() {
+ case key.Matches(msg, m.keyMap.Chat.DownOneItem):
m.chat.SelectNext()
m.chat.ScrollToSelected()
+ case key.Matches(msg, m.keyMap.Chat.HalfPageUp):
+ m.chat.ScrollBy(-m.chat.Height() / 2)
+ m.chat.SelectFirstInView()
+ case key.Matches(msg, m.keyMap.Chat.HalfPageDown):
+ m.chat.ScrollBy(m.chat.Height() / 2)
+ m.chat.SelectLastInView()
+ case key.Matches(msg, m.keyMap.Chat.PageUp):
+ m.chat.ScrollBy(-m.chat.Height())
+ m.chat.SelectFirstInView()
+ case key.Matches(msg, m.keyMap.Chat.PageDown):
+ m.chat.ScrollBy(m.chat.Height())
+ m.chat.SelectLastInView()
+ case key.Matches(msg, m.keyMap.Chat.Home):
+ m.chat.ScrollToTop()
+ m.chat.SelectFirst()
+ case key.Matches(msg, m.keyMap.Chat.End):
+ m.chat.ScrollToBottom()
+ m.chat.SelectLast()
+ default:
+ handleGlobalKeys(msg)
}
- case key.Matches(msg, m.keyMap.Chat.UpOneItem):
- m.chat.SelectPrev()
- m.chat.ScrollToSelected()
- case key.Matches(msg, m.keyMap.Chat.DownOneItem):
- m.chat.SelectNext()
- m.chat.ScrollToSelected()
- case key.Matches(msg, m.keyMap.Chat.HalfPageUp):
- m.chat.ScrollBy(-m.chat.Height() / 2)
- m.chat.SelectFirstInView()
- case key.Matches(msg, m.keyMap.Chat.HalfPageDown):
- m.chat.ScrollBy(m.chat.Height() / 2)
- m.chat.SelectLastInView()
- case key.Matches(msg, m.keyMap.Chat.PageUp):
- m.chat.ScrollBy(-m.chat.Height())
- m.chat.SelectFirstInView()
- case key.Matches(msg, m.keyMap.Chat.PageDown):
- m.chat.ScrollBy(m.chat.Height())
- m.chat.SelectLastInView()
- case key.Matches(msg, m.keyMap.Chat.Home):
- m.chat.ScrollToTop()
- m.chat.SelectFirst()
- case key.Matches(msg, m.keyMap.Chat.End):
- m.chat.ScrollToBottom()
- m.chat.SelectLast()
default:
handleGlobalKeys(msg)
}
@@ -588,11 +609,29 @@ func (m *UI) Cursor() *tea.Cursor {
// Don't show cursor if editor is not visible
return nil
}
- if m.focus == uiFocusEditor && m.textarea.Focused() {
- cur := m.textarea.Cursor()
- cur.X++ // Adjust for app margins
- cur.Y += m.layout.editor.Min.Y
- return cur
+ if m.dialog.HasDialogs() {
+ if front := m.dialog.DialogLast(); front != nil {
+ c, ok := front.(uiutil.Cursor)
+ if ok {
+ cur := c.Cursor()
+ if cur != nil {
+ pos := m.dialog.CenterPosition(m.layout.area, front.ID())
+ cur.X += pos.Min.X
+ cur.Y += pos.Min.Y
+ return cur
+ }
+ }
+ }
+ return nil
+ }
+ switch m.focus {
+ case uiFocusEditor:
+ if m.textarea.Focused() {
+ cur := m.textarea.Cursor()
+ cur.X++ // Adjust for app margins
+ cur.Y += m.layout.editor.Min.Y
+ return cur
+ }
}
return nil
}
@@ -277,6 +277,8 @@ type Styles struct {
SelectedItem lipgloss.Style
InputPrompt lipgloss.Style
+ List lipgloss.Style
+
Commands struct {
CommandTypeSelector lipgloss.Style
}
@@ -909,7 +911,9 @@ func DefaultStyles() Styles {
s.Dialog.Help.FullSeparator = base.Foreground(border)
s.Dialog.NormalItem = base.Padding(0, 1).Foreground(fgBase)
s.Dialog.SelectedItem = base.Padding(0, 1).Background(primary).Foreground(fgBase)
- s.Dialog.InputPrompt = base.Padding(0, 1)
+ s.Dialog.InputPrompt = base.Margin(1, 1)
+
+ s.Dialog.List = base.Margin(0, 0, 1, 0)
s.Dialog.Commands.CommandTypeSelector = base.Foreground(fgHalfMuted)