@@ -10,10 +10,9 @@ import (
10 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/config"
11 "github.com/charmbracelet/crush/internal/llm/prompt"
11 "github.com/charmbracelet/crush/internal/llm/prompt"
12 "github.com/charmbracelet/crush/internal/tui/components/chat"
12 "github.com/charmbracelet/crush/internal/tui/components/chat"
13− "github.com/charmbracelet/crush/internal/tui/components/completions"
14 "github.com/charmbracelet/crush/internal/tui/components/core"
13 "github.com/charmbracelet/crush/internal/tui/components/core"
15− "github.com/charmbracelet/crush/internal/tui/components/core/list"
16 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
14 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
15+ "github.com/charmbracelet/crush/internal/tui/exp/list"
17 "github.com/charmbracelet/crush/internal/tui/styles"
16 "github.com/charmbracelet/crush/internal/tui/styles"
18 "github.com/charmbracelet/crush/internal/tui/util"
17 "github.com/charmbracelet/crush/internal/tui/util"
@@ -29,6 +28,8 @@ const (
29 UserCommands
28 UserCommands
31+type listModel = list.FilterableList[list.CompletionItem[Command]]
32// Command represents a command that can be executed
33// Command represents a command that can be executed
33type Command struct {
34type Command struct {
@@ -48,7 +49,7 @@ type commandDialogCmp struct {
48 wWidth int // Width of the terminal window
49 wWidth int // Width of the terminal window
49 wHeight int // Height of the terminal window
50 wHeight int // Height of the terminal window
51− commandList list.ListModel
52+ commandList listModel
52 keyMap CommandsDialogKeyMap
53 keyMap CommandsDialogKeyMap
53 help help.Model
54 help help.Model
54 commandType int // SystemCommands or UserCommands
55 commandType int // SystemCommands or UserCommands
@@ -67,24 +68,23 @@ type (
69func NewCommandDialog(sessionID string) CommandsDialog {
70func NewCommandDialog(sessionID string) CommandsDialog {
70− listKeyMap := list.DefaultKeyMap()
71 keyMap := DefaultCommandsDialogKeyMap()
71 keyMap := DefaultCommandsDialogKeyMap()
72−
72+ listKeyMap := list.DefaultKeyMap()
73 listKeyMap.Down.SetEnabled(false)
73 listKeyMap.Down.SetEnabled(false)
74 listKeyMap.Up.SetEnabled(false)
74 listKeyMap.Up.SetEnabled(false)
75− listKeyMap.HalfPageDown.SetEnabled(false)
76− listKeyMap.HalfPageUp.SetEnabled(false)
77− listKeyMap.Home.SetEnabled(false)
78− listKeyMap.End.SetEnabled(false)
80 listKeyMap.DownOneItem = keyMap.Next
75 listKeyMap.DownOneItem = keyMap.Next
81 listKeyMap.UpOneItem = keyMap.Previous
76 listKeyMap.UpOneItem = keyMap.Previous
83 t := styles.CurrentTheme()
78 t := styles.CurrentTheme()
79+ inputStyle := t.S().Base.PaddingLeft(1).PaddingBottom(1)
84− commandList := list.New(
80+ commandList := list.NewFilterableList(
85− list.WithFilterable(true),
81+ []list.CompletionItem[Command]{},
82+ list.WithFilterInputStyle(inputStyle),
83+ list.WithFilterListOptions(
86− list.WithKeyMap(listKeyMap),
84+ list.WithKeyMap(listKeyMap),
87− list.WithWrapNavigation(true),
85+ list.WithWrapNavigation(),
86+ list.WithResizeByList(),
89 help := help.New()
89 help := help.New()
90 help.Styles = t.S().Help
90 help.Styles = t.S().Help
@@ -103,10 +103,8 @@ func (c *commandDialogCmp) Init() tea.Cmd {
103 if err != nil {
103 if err != nil {
104 return util.ReportError(err)
104 return util.ReportError(err)
107 c.userCommands = commands
106 c.userCommands = commands
108− c.SetCommandType(c.commandType)
107+ return c.SetCommandType(c.commandType)
109− return c.commandList.Init()
112func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
110func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -114,22 +112,23 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
114 case tea.WindowSizeMsg:
112 case tea.WindowSizeMsg:
115 c.wWidth = msg.Width
113 c.wWidth = msg.Width
116 c.wHeight = msg.Height
114 c.wHeight = msg.Height
117− c.SetCommandType(c.commandType)
118 return c, c.commandList.SetSize(c.listWidth(), c.listHeight())
115 return c, c.commandList.SetSize(c.listWidth(), c.listHeight())
119 case tea.KeyPressMsg:
116 case tea.KeyPressMsg:
121 case key.Matches(msg, c.keyMap.Select):
118 case key.Matches(msg, c.keyMap.Select):
122− selectedItemInx := c.commandList.SelectedIndex()
119+ selectedItem := c.commandList.SelectedItem()
123− if selectedItemInx == list.NoSelection {
120+ if selectedItem == nil {
124 return c, nil // No item selected, do nothing
121 return c, nil // No item selected, do nothing
126− items := c.commandList.Items()
123+ command := (*selectedItem).Value()
127− selectedItem := items[selectedItemInx].(completions.CompletionItem).Value().(Command)
128 return c, tea.Sequence(
124 return c, tea.Sequence(
129 util.CmdHandler(dialogs.CloseDialogMsg{}),
125 util.CmdHandler(dialogs.CloseDialogMsg{}),
130− selectedItem.Handler(selectedItem),
126+ command.Handler(command),
132 case key.Matches(msg, c.keyMap.Tab):
128 case key.Matches(msg, c.keyMap.Tab):
129+ if len(c.userCommands) == 0 {
133 // Toggle command type between System and User commands
132 // Toggle command type between System and User commands
134 if c.commandType == SystemCommands {
133 if c.commandType == SystemCommands {
135 return c, c.SetCommandType(UserCommands)
134 return c, c.SetCommandType(UserCommands)
@@ -140,7 +139,7 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
140 return c, util.CmdHandler(dialogs.CloseDialogMsg{})
139 return c, util.CmdHandler(dialogs.CloseDialogMsg{})
142 u, cmd := c.commandList.Update(msg)
141 u, cmd := c.commandList.Update(msg)
143− c.commandList = u.(list.ListModel)
142+ c.commandList = u.(listModel)
144 return c, cmd
143 return c, cmd
@@ -151,9 +150,14 @@ func (c *commandDialogCmp) View() string {
151 t := styles.CurrentTheme()
150 t := styles.CurrentTheme()
152 listView := c.commandList
151 listView := c.commandList
153 radio := c.commandTypeRadio()
152 radio := c.commandTypeRadio()
154+ header := t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Commands", c.width-lipgloss.Width(radio)-5) + " " + radio)
155+ if len(c.userCommands) == 0 {
156+ header = t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Commands", c.width-4))
154 content := lipgloss.JoinVertical(
158 content := lipgloss.JoinVertical(
155 lipgloss.Left,
159 lipgloss.Left,
156− t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Commands", c.width-lipgloss.Width(radio)-5)+" "+radio),
160+ header,
157 listView.View(),
161 listView.View(),
159 t.S().Base.Width(c.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(c.help.View(c.keyMap)),
163 t.S().Base.Width(c.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(c.help.View(c.keyMap)),
@@ -197,13 +201,18 @@ func (c *commandDialogCmp) SetCommandType(commandType int) tea.Cmd {
197 commands = c.userCommands
201 commands = c.userCommands
200− commandItems := []util.Model{}
204+ commandItems := []list.CompletionItem[Command]{}
201 for _, cmd := range commands {
205 for _, cmd := range commands {
202− opts := []completions.CompletionOption{}
206+ opts := []list.CompletionItemOption{
207+ list.WithCompletionID(cmd.ID),
203 if cmd.Shortcut != "" {
209 if cmd.Shortcut != "" {
204− opts = append(opts, completions.WithShortcut(cmd.Shortcut))
210+ opts = append(
212+ list.WithCompletionShortcut(cmd.Shortcut),
206− commandItems = append(commandItems, completions.NewCompletionItem(cmd.Title, cmd, opts...))
215+ commandItems = append(commandItems, list.NewCompletionItem(cmd.Title, cmd, opts...))
208 return c.commandList.SetItems(commandItems)
217 return c.commandList.SetItems(commandItems)