168 lines
4.4 KiB
Go
168 lines
4.4 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"charm.land/bubbles/v2/list"
|
|
tea "charm.land/bubbletea/v2"
|
|
"charm.land/lipgloss/v2"
|
|
"github.com/sahilm/fuzzy"
|
|
)
|
|
|
|
const commandDropdownHeight = 8
|
|
|
|
var (
|
|
dropdownStyle = lipgloss.NewStyle().
|
|
Background(lipgloss.Color("236")).
|
|
Padding(0, 1)
|
|
|
|
inlineTitleStyle = lipgloss.NewStyle().Background(lipgloss.Color("236")).Foreground(lipgloss.Color("86")).Bold(true)
|
|
inlineTitleSelStyle = lipgloss.NewStyle().Background(lipgloss.Color("60")).Foreground(lipgloss.Color("86")).Bold(true)
|
|
inlineDescStyle = lipgloss.NewStyle().Background(lipgloss.Color("236")).Foreground(lipgloss.Color("245"))
|
|
inlineDescSelStyle = lipgloss.NewStyle().Background(lipgloss.Color("60")).Foreground(lipgloss.Color("255"))
|
|
inlinePadStyle = lipgloss.NewStyle().Background(lipgloss.Color("236"))
|
|
inlinePadSelStyle = lipgloss.NewStyle().Background(lipgloss.Color("60"))
|
|
)
|
|
|
|
type inlineDelegate struct{}
|
|
|
|
func (inlineDelegate) Height() int { return 1 }
|
|
func (inlineDelegate) Spacing() int { return 0 }
|
|
func (inlineDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil }
|
|
func (inlineDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
|
|
c, ok := item.(command)
|
|
if !ok {
|
|
return
|
|
}
|
|
if m.Width() <= 0 {
|
|
return
|
|
}
|
|
title := "/" + c.name
|
|
desc := c.description
|
|
|
|
titleStyle := inlineTitleStyle
|
|
descStyle := inlineDescStyle
|
|
padStyle := inlinePadStyle
|
|
if index == m.Index() {
|
|
titleStyle = inlineTitleSelStyle
|
|
descStyle = inlineDescSelStyle
|
|
padStyle = inlinePadSelStyle
|
|
}
|
|
|
|
renderedTitle := titleStyle.Render(title)
|
|
renderedDesc := descStyle.Render(desc)
|
|
pad := max(m.Width()-1-lipgloss.Width(renderedTitle)-lipgloss.Width(renderedDesc), 1)
|
|
fmt.Fprintf(w, "%s%s%s%s", renderedTitle, padStyle.Render(" "), renderedDesc, padStyle.Render(strings.Repeat(" ", pad)))
|
|
}
|
|
|
|
type command struct {
|
|
name string
|
|
description string
|
|
handler func(m uiModel) (uiModel, tea.Cmd)
|
|
}
|
|
|
|
func (c command) FilterValue() string { return c.name }
|
|
func (c command) Title() string { return "/" + c.name }
|
|
func (c command) Description() string { return c.description }
|
|
|
|
type commandList struct {
|
|
list list.Model
|
|
open bool
|
|
available []command
|
|
}
|
|
|
|
func newCommandList(width int) commandList {
|
|
l := list.New(nil, inlineDelegate{}, width, 1)
|
|
l.Title = "Commands"
|
|
l.SetShowFilter(false)
|
|
l.SetFilteringEnabled(false)
|
|
l.SetShowStatusBar(false)
|
|
l.SetShowPagination(false)
|
|
l.SetShowHelp(false)
|
|
l.DisableQuitKeybindings()
|
|
return commandList{list: l}
|
|
}
|
|
|
|
func (m uiModel) buildCommands() []command {
|
|
return []command{
|
|
{
|
|
name: "exit",
|
|
description: "Quit the application",
|
|
handler: func(m uiModel) (uiModel, tea.Cmd) {
|
|
m.logger.Debug("Received exit sequence. Quiting")
|
|
return m, tea.Quit
|
|
},
|
|
},
|
|
{
|
|
name: "history",
|
|
description: "Open conversation history",
|
|
handler: func(m uiModel) (uiModel, tea.Cmd) {
|
|
m.logger.Debug("Received history call")
|
|
m.focus = focusHistory
|
|
return m, m.getHistory()
|
|
},
|
|
},
|
|
{
|
|
name: "models",
|
|
description: "Choose an LLM model",
|
|
handler: func(m uiModel) (uiModel, tea.Cmd) {
|
|
m.logger.Debug("Received models sequence")
|
|
m.focus = focusModels
|
|
return m, m.getModels()
|
|
},
|
|
},
|
|
{
|
|
name: "rename",
|
|
description: "Rename the current conversation",
|
|
handler: func(m uiModel) (uiModel, tea.Cmd) {
|
|
m.logger.Debug("Received rename request")
|
|
m.waiting = true
|
|
return m, m.renameConversation()
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (cl *commandList) setAvailable(cmds []command) {
|
|
cl.available = cmds
|
|
items := make([]list.Item, len(cmds))
|
|
for i, c := range cmds {
|
|
items[i] = c
|
|
}
|
|
cl.list.SetItems(items)
|
|
cl.list.SetHeight(len(items) + 1)
|
|
}
|
|
|
|
func (cl *commandList) filter(query string) {
|
|
var matches []list.Item
|
|
if query == "" {
|
|
matches = make([]list.Item, len(cl.available))
|
|
for i, c := range cl.available {
|
|
matches[i] = c
|
|
}
|
|
} else {
|
|
names := make([]string, len(cl.available))
|
|
for i, c := range cl.available {
|
|
names[i] = c.name
|
|
}
|
|
result := fuzzy.Find(query, names)
|
|
matches = make([]list.Item, len(result))
|
|
for i, r := range result {
|
|
matches[i] = cl.available[r.Index]
|
|
}
|
|
}
|
|
cl.list.SetItems(matches)
|
|
cl.list.ResetSelected()
|
|
}
|
|
|
|
func (cl commandList) view() string {
|
|
return dropdownStyle.Render(cl.list.View())
|
|
}
|
|
|
|
func (cl commandList) selectedItem() (command, bool) {
|
|
c, ok := cl.list.SelectedItem().(command)
|
|
return c, ok
|
|
}
|