diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index 091231039c71e24b918a755d56ba0a0de27ae509..4168bfd8b96d67542cc15f01609499339a9cead2 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -27,6 +27,10 @@ type SessionSelectedMsg = session.Session type SessionClearedMsg struct{} +type SessionDeletedMsg struct { + Session session.Session +} + const ( NotFound = -1 ) @@ -91,6 +95,15 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.session = session.Session{} return m, m.listCmp.SetItems([]util.Model{}) + case SessionDeletedMsg: + if msg.Session.ID == m.session.ID { + m.session = session.Session{} + return m, tea.Batch( + m.listCmp.SetItems([]util.Model{}), + func() tea.Msg { return SessionClearedMsg{} }, + ) + } + return m, nil case pubsub.Event[message.Message]: cmd := m.handleMessageEvent(msg) return m, cmd diff --git a/internal/tui/components/dialogs/sessions/delete.go b/internal/tui/components/dialogs/sessions/delete.go new file mode 100644 index 0000000000000000000000000000000000000000..d4639e7200eddb2a245d001e93c1eeddfd4cdf61 --- /dev/null +++ b/internal/tui/components/dialogs/sessions/delete.go @@ -0,0 +1,160 @@ +package sessions + +import ( + "github.com/charmbracelet/bubbles/v2/key" + tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/crush/internal/session" + "github.com/charmbracelet/crush/internal/tui/components/chat" + "github.com/charmbracelet/crush/internal/tui/components/dialogs" + "github.com/charmbracelet/crush/internal/tui/styles" + "github.com/charmbracelet/crush/internal/tui/util" + "github.com/charmbracelet/lipgloss/v2" +) + +const DeleteSessionDialogID dialogs.DialogID = "delete-session" + +type DeleteSessionDialog interface { + dialogs.DialogModel +} + +type deleteSessionDialogCmp struct { + wWidth int + wHeight int + session session.Session + selectedNo bool + keymap DeleteKeyMap +} + +type DeleteKeyMap struct { + LeftRight, + EnterSpace, + Yes, + No, + Tab, + Close key.Binding +} + +func DefaultDeleteKeymap() DeleteKeyMap { + return DeleteKeyMap{ + LeftRight: key.NewBinding( + key.WithKeys("left", "right"), + key.WithHelp("←/→", "switch options"), + ), + EnterSpace: key.NewBinding( + key.WithKeys("enter", " "), + key.WithHelp("enter/space", "confirm"), + ), + Yes: key.NewBinding( + key.WithKeys("y", "Y"), + key.WithHelp("y/Y", "yes"), + ), + No: key.NewBinding( + key.WithKeys("n", "N"), + key.WithHelp("n/N", "no"), + ), + Tab: key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("tab", "switch options"), + ), + Close: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "cancel"), + ), + } +} + +func NewDeleteSessionDialog(session session.Session) DeleteSessionDialog { + return &deleteSessionDialogCmp{ + session: session, + selectedNo: true, + keymap: DefaultDeleteKeymap(), + } +} + +func (d *deleteSessionDialogCmp) Init() tea.Cmd { + return nil +} + +func (d *deleteSessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + d.wWidth = msg.Width + d.wHeight = msg.Height + case tea.KeyPressMsg: + switch { + case key.Matches(msg, d.keymap.LeftRight, d.keymap.Tab): + d.selectedNo = !d.selectedNo + return d, nil + case key.Matches(msg, d.keymap.EnterSpace): + if !d.selectedNo { + return d, tea.Sequence( + util.CmdHandler(dialogs.CloseDialogMsg{}), + util.CmdHandler(chat.SessionDeletedMsg{Session: d.session}), + ) + } + return d, util.CmdHandler(dialogs.CloseDialogMsg{}) + case key.Matches(msg, d.keymap.Yes): + return d, tea.Sequence( + util.CmdHandler(dialogs.CloseDialogMsg{}), + util.CmdHandler(chat.SessionDeletedMsg{Session: d.session}), + ) + case key.Matches(msg, d.keymap.No, d.keymap.Close): + return d, util.CmdHandler(dialogs.CloseDialogMsg{}) + } + } + return d, nil +} + +func (d *deleteSessionDialogCmp) View() string { + t := styles.CurrentTheme() + baseStyle := t.S().Base + yesStyle := t.S().Text + noStyle := yesStyle + + if d.selectedNo { + noStyle = noStyle.Foreground(t.White).Background(t.Secondary) + yesStyle = yesStyle.Background(t.BgSubtle) + } else { + yesStyle = yesStyle.Foreground(t.White).Background(t.Secondary) + noStyle = noStyle.Background(t.BgSubtle) + } + + question := "Delete session \"" + d.session.Title + "\"?" + const horizontalPadding = 3 + yesButton := yesStyle.Padding(0, horizontalPadding).Render("Delete") + noButton := noStyle.Padding(0, horizontalPadding).Render("Cancel") + + buttons := baseStyle.Width(lipgloss.Width(question)).Align(lipgloss.Right).Render( + lipgloss.JoinHorizontal(lipgloss.Center, yesButton, " ", noButton), + ) + + content := baseStyle.Render( + lipgloss.JoinVertical( + lipgloss.Center, + question, + "", + buttons, + ), + ) + + deleteDialogStyle := baseStyle. + Padding(1, 2). + Border(lipgloss.RoundedBorder()). + BorderForeground(t.BorderFocus) + + return deleteDialogStyle.Render(content) +} + +func (d *deleteSessionDialogCmp) Position() (int, int) { + question := "Delete session \"" + d.session.Title + "\"?" + row := d.wHeight / 2 + row -= 7 / 2 + col := d.wWidth / 2 + col -= (lipgloss.Width(question) + 4) / 2 + + return row, col +} + +func (d *deleteSessionDialogCmp) ID() dialogs.DialogID { + return DeleteSessionDialogID +} diff --git a/internal/tui/components/dialogs/sessions/keys.go b/internal/tui/components/dialogs/sessions/keys.go index a3ca4b31f0c04c491fa7990f7e69ac546f608a7d..4d62bf7b4f9e3db4b55e9c744e1800fd1d5984a4 100644 --- a/internal/tui/components/dialogs/sessions/keys.go +++ b/internal/tui/components/dialogs/sessions/keys.go @@ -8,6 +8,7 @@ type KeyMap struct { Select, Next, Previous, + Delete, Close key.Binding } @@ -25,6 +26,10 @@ func DefaultKeyMap() KeyMap { key.WithKeys("up", "ctrl+p"), key.WithHelp("↑", "previous item"), ), + Delete: key.NewBinding( + key.WithKeys("delete", "d"), + key.WithHelp("d/del", "delete session"), + ), Close: key.NewBinding( key.WithKeys("esc"), key.WithHelp("esc", "cancel"), @@ -38,6 +43,7 @@ func (k KeyMap) KeyBindings() []key.Binding { k.Select, k.Next, k.Previous, + k.Delete, k.Close, } } @@ -57,11 +63,11 @@ func (k KeyMap) FullHelp() [][]key.Binding { func (k KeyMap) ShortHelp() []key.Binding { return []key.Binding{ key.NewBinding( - key.WithKeys("down", "up"), key.WithHelp("↑↓", "choose"), ), k.Select, + k.Delete, k.Close, } } diff --git a/internal/tui/components/dialogs/sessions/sessions.go b/internal/tui/components/dialogs/sessions/sessions.go index a95ae0c5ce9b07d499d4f78834a69ccd7ed5635f..3a9d0c9c46c742b0c311277043a1bbec13059bc9 100644 --- a/internal/tui/components/dialogs/sessions/sessions.go +++ b/internal/tui/components/dialogs/sessions/sessions.go @@ -111,6 +111,15 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ), ) } + case key.Matches(msg, s.keyMap.Delete): + if len(s.sessionsList.Items()) > 0 { + items := s.sessionsList.Items() + selectedItemInx := s.sessionsList.SelectedIndex() + selectedSession := items[selectedItemInx].(completions.CompletionItem).Value().(session.Session) + return s, util.CmdHandler(dialogs.OpenDialogMsg{ + Model: NewDeleteSessionDialog(selectedSession), + }) + } case key.Matches(msg, s.keyMap.Close): return s, util.CmdHandler(dialogs.CloseDialogMsg{}) default: diff --git a/internal/tui/tui.go b/internal/tui/tui.go index b7bfc068abe8bade0248f5d23105a52cf315b98d..0bf0abeb29ee488f6de7f5bc026e8650b8d9137f 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -134,6 +134,17 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Session case cmpChat.SessionSelectedMsg: a.selectedSessionID = msg.ID + case cmpChat.SessionDeletedMsg: + err := a.app.Sessions.Delete(context.Background(), msg.Session.ID) + if err != nil { + return a, util.ReportError(err) + } + + if msg.Session.ID == a.selectedSessionID { + a.selectedSessionID = "" + } + + return a, util.CmdHandler(dialogs.CloseDialogMsg{}) case cmpChat.SessionClearedMsg: a.selectedSessionID = "" // Commands