From 105ee2e01ba4c4a90bdfd061af8d807321141fda Mon Sep 17 00:00:00 2001 From: Matt Low Date: Mon, 1 Apr 2024 01:06:13 +0000 Subject: [PATCH] tui: update/clean up input handling --- pkg/tui/chat.go | 84 +++++++++++++++++++++++----------------- pkg/tui/conversations.go | 6 +-- pkg/tui/tui.go | 48 ++++++++++++----------- 3 files changed, 77 insertions(+), 61 deletions(-) diff --git a/pkg/tui/chat.go b/pkg/tui/chat.go index a3c8b2a..f7a8ec9 100644 --- a/pkg/tui/chat.go +++ b/pkg/tui/chat.go @@ -161,37 +161,48 @@ var ( footerStyle = lipgloss.NewStyle() ) -func (m *chatModel) handleInput(msg tea.KeyMsg) tea.Cmd { +func (m *chatModel) handleInput(msg tea.KeyMsg) (bool, tea.Cmd) { switch m.focus { case focusInput: - cmd := m.handleInputKey(msg) - if cmd != nil { - return cmd + consumed, cmd := m.handleInputKey(msg) + if consumed { + return true, cmd } case focusMessages: - cmd := m.handleMessagesKey(msg) - if cmd != nil { - return cmd + consumed, cmd := m.handleMessagesKey(msg) + if consumed { + return true, cmd } } switch msg.String() { case "esc": - return func() tea.Msg { + return true, func() tea.Msg { + return msgChangeState(stateConversations) + } + case "ctrl+c": + if m.waitingForReply { + m.stopSignal <- struct{}{} + return true, nil + } + return true, func() tea.Msg { return msgChangeState(stateConversations) } case "ctrl+p": m.persistence = !m.persistence + return true, nil case "ctrl+t": m.showToolResults = !m.showToolResults m.rebuildMessageCache() m.updateContent() + return true, nil case "ctrl+w": m.wrap = !m.wrap m.rebuildMessageCache() m.updateContent() + return true, nil } - return nil + return false, nil } func (m chatModel) Init() tea.Cmd { @@ -375,17 +386,18 @@ func (m chatModel) Update(msg tea.Msg) (chatModel, tea.Cmd) { return m, tea.Batch(cmds...) } -func (m *chatModel) handleMessagesKey(msg tea.KeyMsg) tea.Cmd { +func (m *chatModel) handleMessagesKey(msg tea.KeyMsg) (bool, tea.Cmd) { switch msg.String() { - case "tab": + case "tab", "enter": m.focus = focusInput m.updateContent() m.input.Focus() + return true, nil case "e": message := m.messages[m.selectedMessage] cmd := openTempfileEditor("message.*.md", message.Content, "# Edit the message below\n") m.editorTarget = selectedMessage - return cmd + return true, cmd case "ctrl+k": if m.selectedMessage > 0 && len(m.messages) == len(m.messageOffsets) { m.selectedMessage-- @@ -393,6 +405,7 @@ func (m *chatModel) handleMessagesKey(msg tea.KeyMsg) tea.Cmd { offset := m.messageOffsets[m.selectedMessage] scrollIntoView(&m.content, offset, 0.1) } + return true, nil case "ctrl+j": if m.selectedMessage < len(m.messages)-1 && len(m.messages) == len(m.messageOffsets) { m.selectedMessage++ @@ -400,44 +413,43 @@ func (m *chatModel) handleMessagesKey(msg tea.KeyMsg) tea.Cmd { offset := m.messageOffsets[m.selectedMessage] scrollIntoView(&m.content, offset, 0.1) } + return true, nil case "ctrl+r": // resubmit the conversation with all messages up until and including the selected message if m.waitingForReply || len(m.messages) == 0 { - return nil + return true, nil } m.messages = m.messages[:m.selectedMessage+1] m.messageCache = m.messageCache[:m.selectedMessage+1] m.updateContent() m.content.GotoBottom() - return m.promptLLM() + return true, m.promptLLM() } - return nil + return false, nil } -func (m *chatModel) handleInputKey(msg tea.KeyMsg) tea.Cmd { +func (m *chatModel) handleInputKey(msg tea.KeyMsg) (bool, tea.Cmd) { switch msg.String() { case "esc": - return func() tea.Msg { - return msgChangeState(stateConversations) + m.focus = focusMessages + if len(m.messages) > 0 { + if m.selectedMessage < 0 || m.selectedMessage >= len(m.messages) { + m.selectedMessage = len(m.messages) - 1 + } + offset := m.messageOffsets[m.selectedMessage] + scrollIntoView(&m.content, offset, 0.1) } - //m.focus = focusMessages - //if len(m.messages) > 0 { - // if m.selectedMessage < 0 || m.selectedMessage >= len(m.messages) { - // m.selectedMessage = len(m.messages) - 1 - // } - // offset := m.messageOffsets[m.selectedMessage] - // scrollIntoView(&m.content, offset, 0.1) - //} - //m.updateContent() - //m.input.Blur() + m.updateContent() + m.input.Blur() + return true, nil case "ctrl+s": userInput := strings.TrimSpace(m.input.Value()) if strings.TrimSpace(userInput) == "" { - return nil + return true, nil } if len(m.messages) > 0 && m.messages[len(m.messages)-1].Role == models.MessageRoleUser { - return wrapError(fmt.Errorf("Can't reply to a user message")) + return true, wrapError(fmt.Errorf("Can't reply to a user message")) } reply := models.Message{ @@ -451,18 +463,18 @@ func (m *chatModel) handleInputKey(msg tea.KeyMsg) tea.Cmd { err = m.ctx.Store.SaveConversation(m.conversation) } if err != nil { - return wrapError(err) + return true, wrapError(err) } // ensure all messages up to the one we're about to add are persisted cmd := m.persistConversation() if cmd != nil { - return cmd + return true, cmd } savedReply, err := m.ctx.Store.AddReply(m.conversation, reply) if err != nil { - return wrapError(err) + return true, wrapError(err) } reply = *savedReply } @@ -472,13 +484,13 @@ func (m *chatModel) handleInputKey(msg tea.KeyMsg) tea.Cmd { m.updateContent() m.content.GotoBottom() - return m.promptLLM() + return true, m.promptLLM() case "ctrl+e": cmd := openTempfileEditor("message.*.md", m.input.Value(), "# Edit your input below\n") m.editorTarget = input - return cmd + return true, cmd } - return nil + return false, nil } func (m *chatModel) renderMessageHeading(i int, message *models.Message) string { diff --git a/pkg/tui/conversations.go b/pkg/tui/conversations.go index 9094c05..a16cd00 100644 --- a/pkg/tui/conversations.go +++ b/pkg/tui/conversations.go @@ -41,11 +41,11 @@ func newConversationsModel(tui *model) conversationsModel { return m } -func (m *conversationsModel) handleInput(msg tea.KeyMsg) tea.Cmd { +func (m *conversationsModel) handleInput(msg tea.KeyMsg) (bool, tea.Cmd) { switch msg.String() { case "enter": // how to notify chats model - return func() tea.Msg { + return true, func() tea.Msg { return msgChangeState(stateChat) } case "n": @@ -59,7 +59,7 @@ func (m *conversationsModel) handleInput(msg tea.KeyMsg) tea.Cmd { case "shift+r": // show prompt to generate name for conversation } - return nil + return false, nil } func (m conversationsModel) Init() tea.Cmd { diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index 79ef931..1f76852 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -2,7 +2,6 @@ package tui // The terminal UI for lmcli, launched from the `lmcli chat` command // TODO: -// - conversation list view // - change model // - rename conversation // - set system prompt @@ -83,28 +82,33 @@ func (m model) Init() tea.Cmd { } } -func (m *model) handleGlobalInput(msg tea.KeyMsg) tea.Cmd { - switch msg.String() { - case "ctrl+c": - if m.chat.waitingForReply { - m.chat.stopSignal <- struct{}{} - return nil - } else { - return tea.Quit +func (m *model) handleGlobalInput(msg tea.KeyMsg) (bool, tea.Cmd) { + // delegate input to the active child state first, only handling it at the + // global level if the child state does not + var cmds []tea.Cmd + switch m.state { + case stateChat: + handled, cmd := m.chat.handleInput(msg) + cmds = append(cmds, cmd) + if handled { + m.chat, cmd = m.chat.Update(nil) + cmds = append(cmds, cmd) + return true, tea.Batch(cmds...) } - case "q": - if m.chat.focus != focusInput { - return tea.Quit - } - default: - switch m.state { - case stateChat: - return m.chat.handleInput(msg) - case stateConversations: - return m.conversations.handleInput(msg) + case stateConversations: + handled, cmd := m.conversations.handleInput(msg) + cmds = append(cmds, cmd) + if handled { + m.conversations, cmd = m.conversations.Update(nil) + cmds = append(cmds, cmd) + return true, tea.Batch(cmds...) } } - return nil + switch msg.String() { + case "ctrl+c", "ctrl+q": + return true, tea.Quit + } + return false, nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -112,8 +116,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: - cmd := m.handleGlobalInput(msg) - if cmd != nil { + handled, cmd := m.handleGlobalInput(msg) + if handled { return m, cmd } case msgChangeState: