From e59ce973b6b5a9f7d66ba2058fe127a5194f819d Mon Sep 17 00:00:00 2001 From: Matt Low Date: Mon, 8 Jul 2024 06:40:52 +0000 Subject: [PATCH] Add conversation deletion to conversations view --- pkg/tui/bubbles/confirmprompt.go | 67 ++++++++++++++++++++ pkg/tui/views/conversations/conversations.go | 60 +++++++++++++++++- 2 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 pkg/tui/bubbles/confirmprompt.go diff --git a/pkg/tui/bubbles/confirmprompt.go b/pkg/tui/bubbles/confirmprompt.go new file mode 100644 index 0000000..e664df7 --- /dev/null +++ b/pkg/tui/bubbles/confirmprompt.go @@ -0,0 +1,67 @@ +package bubbles + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type ConfirmPrompt struct { + Question string + Style lipgloss.Style + Payload interface{} + value bool + answered bool + focused bool +} + +func NewConfirmPrompt(question string, payload interface{}) ConfirmPrompt { + return ConfirmPrompt{ + Question: question, + Style: lipgloss.NewStyle(), + Payload: payload, + focused: true, // focus by default + } +} + +type MsgConfirmPromptAnswered struct { + Value bool + Payload interface{} +} + +func (b ConfirmPrompt) Update(msg tea.Msg) (ConfirmPrompt, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + if !b.focused || b.answered { + return b, nil + } + switch msg.String() { + case "y", "Y": + b.value = true + b.answered = true + b.focused = false + return b, func() tea.Msg { return MsgConfirmPromptAnswered{true, b.Payload} } + case "n", "N", "esc": + b.value = false + b.answered = true + b.focused = false + return b, func() tea.Msg { return MsgConfirmPromptAnswered{false, b.Payload} } + } + } + return b, nil +} + +func (b ConfirmPrompt) View() string { + return b.Style.Render(b.Question) + lipgloss.NewStyle().Faint(true).Render(" (y/n)") +} + +func (b *ConfirmPrompt) Focus() { + b.focused = true +} + +func (b *ConfirmPrompt) Blur() { + b.focused = false +} + +func (b ConfirmPrompt) Focused() bool { + return b.focused +} diff --git a/pkg/tui/views/conversations/conversations.go b/pkg/tui/views/conversations/conversations.go index b5b5c18..f835b49 100644 --- a/pkg/tui/views/conversations/conversations.go +++ b/pkg/tui/views/conversations/conversations.go @@ -6,6 +6,7 @@ import ( "time" "git.mlow.ca/mlow/lmcli/pkg/api" + "git.mlow.ca/mlow/lmcli/pkg/tui/bubbles" "git.mlow.ca/mlow/lmcli/pkg/tui/shared" "git.mlow.ca/mlow/lmcli/pkg/tui/styles" tuiutil "git.mlow.ca/mlow/lmcli/pkg/tui/util" @@ -25,6 +26,13 @@ type ( msgConversationsLoaded ([]loadedConversation) // sent when a conversation is selected msgConversationSelected api.Conversation + // sent when a conversation is deleted + msgConversationDeleted struct{} +) + +// Prompt payloads +type ( + deleteConversationPayload api.Conversation ) type Model struct { @@ -36,6 +44,8 @@ type Model struct { itemOffsets []int // keeps track of the viewport y offset of each rendered item content viewport.Model + + confirmPrompt bubbles.ConfirmPrompt } func Conversations(shared shared.Shared) Model { @@ -47,6 +57,14 @@ func Conversations(shared shared.Shared) Model { } func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) { + if m.confirmPrompt.Focused() { + var cmd tea.Cmd + m.confirmPrompt, cmd = m.confirmPrompt.Update(msg) + if cmd != nil { + return true, cmd + } + } + switch msg.String() { case "enter": if len(m.conversations) > 0 && m.cursor < len(m.conversations) { @@ -89,7 +107,20 @@ func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) { case "n": // new conversation case "d": - // show prompt to delete conversation + if !m.confirmPrompt.Focused() && len(m.conversations) > 0 && m.cursor < len(m.conversations) { + title := m.conversations[m.cursor].conv.Title + if title == "" { + title = "(untitled)" + } + m.confirmPrompt = bubbles.NewConfirmPrompt( + fmt.Sprintf("Delete '%s'?", title), + deleteConversationPayload(m.conversations[m.cursor].conv), + ) + m.confirmPrompt.Style = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("3")) + return true, nil + } case "c": // copy/clone conversation case "r": @@ -120,12 +151,23 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { m.content.SetContent(m.renderConversationList()) case msgConversationsLoaded: m.conversations = msg + m.cursor = max(0, min(len(m.conversations), m.cursor)) m.content.SetContent(m.renderConversationList()) case msgConversationSelected: m.Values.ConvShortname = msg.ShortName.String cmds = append(cmds, func() tea.Msg { return shared.MsgViewChange(shared.StateChat) }) + case bubbles.MsgConfirmPromptAnswered: + m.confirmPrompt.Blur() + if msg.Value { + switch payload := msg.Payload.(type) { + case deleteConversationPayload: + cmds = append(cmds, m.deleteConversation(api.Conversation(payload))) + } + } + case msgConversationDeleted: + cmds = append(cmds, m.loadConversations()) } var cmd tea.Cmd @@ -135,8 +177,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } if m.Width > 0 { + wrap := lipgloss.NewStyle().Width(m.Width) m.Header = m.headerView() - m.Footer = "" // TODO: show /something/ + m.Footer = "" // TODO: "Press ? for help" + if m.confirmPrompt.Focused() { + m.Footer = wrap.Render(m.confirmPrompt.View()) + } m.Error = tuiutil.ErrorBanner(m.Err, m.Width) fixedHeight := tuiutil.Height(m.Header) + tuiutil.Height(m.Error) + tuiutil.Height(m.Footer) m.content.Height = m.Height - fixedHeight @@ -162,6 +208,16 @@ func (m *Model) loadConversations() tea.Cmd { } } +func (m *Model) deleteConversation(conv api.Conversation) tea.Cmd { + return func() tea.Msg { + err := m.Ctx.Store.DeleteConversation(&conv) + if err != nil { + return shared.MsgError(fmt.Errorf("Could not delete conversation: %v", err)) + } + return msgConversationDeleted{} + } +} + func (m Model) View() string { if m.Width == 0 { return ""