tui: update/clean up input handling

This commit is contained in:
Matt Low 2024-04-01 01:06:13 +00:00
parent e1970a315a
commit 105ee2e01b
3 changed files with 77 additions and 61 deletions

View File

@ -161,37 +161,48 @@ var (
footerStyle = lipgloss.NewStyle() 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 { switch m.focus {
case focusInput: case focusInput:
cmd := m.handleInputKey(msg) consumed, cmd := m.handleInputKey(msg)
if cmd != nil { if consumed {
return cmd return true, cmd
} }
case focusMessages: case focusMessages:
cmd := m.handleMessagesKey(msg) consumed, cmd := m.handleMessagesKey(msg)
if cmd != nil { if consumed {
return cmd return true, cmd
} }
} }
switch msg.String() { switch msg.String() {
case "esc": 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) return msgChangeState(stateConversations)
} }
case "ctrl+p": case "ctrl+p":
m.persistence = !m.persistence m.persistence = !m.persistence
return true, nil
case "ctrl+t": case "ctrl+t":
m.showToolResults = !m.showToolResults m.showToolResults = !m.showToolResults
m.rebuildMessageCache() m.rebuildMessageCache()
m.updateContent() m.updateContent()
return true, nil
case "ctrl+w": case "ctrl+w":
m.wrap = !m.wrap m.wrap = !m.wrap
m.rebuildMessageCache() m.rebuildMessageCache()
m.updateContent() m.updateContent()
return true, nil
} }
return nil return false, nil
} }
func (m chatModel) Init() tea.Cmd { 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...) 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() { switch msg.String() {
case "tab": case "tab", "enter":
m.focus = focusInput m.focus = focusInput
m.updateContent() m.updateContent()
m.input.Focus() m.input.Focus()
return true, nil
case "e": case "e":
message := m.messages[m.selectedMessage] message := m.messages[m.selectedMessage]
cmd := openTempfileEditor("message.*.md", message.Content, "# Edit the message below\n") cmd := openTempfileEditor("message.*.md", message.Content, "# Edit the message below\n")
m.editorTarget = selectedMessage m.editorTarget = selectedMessage
return cmd return true, cmd
case "ctrl+k": case "ctrl+k":
if m.selectedMessage > 0 && len(m.messages) == len(m.messageOffsets) { if m.selectedMessage > 0 && len(m.messages) == len(m.messageOffsets) {
m.selectedMessage-- m.selectedMessage--
@ -393,6 +405,7 @@ func (m *chatModel) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
offset := m.messageOffsets[m.selectedMessage] offset := m.messageOffsets[m.selectedMessage]
scrollIntoView(&m.content, offset, 0.1) scrollIntoView(&m.content, offset, 0.1)
} }
return true, nil
case "ctrl+j": case "ctrl+j":
if m.selectedMessage < len(m.messages)-1 && len(m.messages) == len(m.messageOffsets) { if m.selectedMessage < len(m.messages)-1 && len(m.messages) == len(m.messageOffsets) {
m.selectedMessage++ m.selectedMessage++
@ -400,44 +413,43 @@ func (m *chatModel) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
offset := m.messageOffsets[m.selectedMessage] offset := m.messageOffsets[m.selectedMessage]
scrollIntoView(&m.content, offset, 0.1) scrollIntoView(&m.content, offset, 0.1)
} }
return true, nil
case "ctrl+r": case "ctrl+r":
// resubmit the conversation with all messages up until and including the selected message // resubmit the conversation with all messages up until and including the selected message
if m.waitingForReply || len(m.messages) == 0 { if m.waitingForReply || len(m.messages) == 0 {
return nil return true, nil
} }
m.messages = m.messages[:m.selectedMessage+1] m.messages = m.messages[:m.selectedMessage+1]
m.messageCache = m.messageCache[:m.selectedMessage+1] m.messageCache = m.messageCache[:m.selectedMessage+1]
m.updateContent() m.updateContent()
m.content.GotoBottom() 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() { switch msg.String() {
case "esc": case "esc":
return func() tea.Msg { m.focus = focusMessages
return msgChangeState(stateConversations) if len(m.messages) > 0 {
if m.selectedMessage < 0 || m.selectedMessage >= len(m.messages) {
m.selectedMessage = len(m.messages) - 1
} }
//m.focus = focusMessages offset := m.messageOffsets[m.selectedMessage]
//if len(m.messages) > 0 { scrollIntoView(&m.content, offset, 0.1)
// if m.selectedMessage < 0 || m.selectedMessage >= len(m.messages) { }
// m.selectedMessage = len(m.messages) - 1 m.updateContent()
// } m.input.Blur()
// offset := m.messageOffsets[m.selectedMessage] return true, nil
// scrollIntoView(&m.content, offset, 0.1)
//}
//m.updateContent()
//m.input.Blur()
case "ctrl+s": case "ctrl+s":
userInput := strings.TrimSpace(m.input.Value()) userInput := strings.TrimSpace(m.input.Value())
if strings.TrimSpace(userInput) == "" { if strings.TrimSpace(userInput) == "" {
return nil return true, nil
} }
if len(m.messages) > 0 && m.messages[len(m.messages)-1].Role == models.MessageRoleUser { 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{ reply := models.Message{
@ -451,18 +463,18 @@ func (m *chatModel) handleInputKey(msg tea.KeyMsg) tea.Cmd {
err = m.ctx.Store.SaveConversation(m.conversation) err = m.ctx.Store.SaveConversation(m.conversation)
} }
if err != nil { if err != nil {
return wrapError(err) return true, wrapError(err)
} }
// ensure all messages up to the one we're about to add are persisted // ensure all messages up to the one we're about to add are persisted
cmd := m.persistConversation() cmd := m.persistConversation()
if cmd != nil { if cmd != nil {
return cmd return true, cmd
} }
savedReply, err := m.ctx.Store.AddReply(m.conversation, reply) savedReply, err := m.ctx.Store.AddReply(m.conversation, reply)
if err != nil { if err != nil {
return wrapError(err) return true, wrapError(err)
} }
reply = *savedReply reply = *savedReply
} }
@ -472,13 +484,13 @@ func (m *chatModel) handleInputKey(msg tea.KeyMsg) tea.Cmd {
m.updateContent() m.updateContent()
m.content.GotoBottom() m.content.GotoBottom()
return m.promptLLM() return true, m.promptLLM()
case "ctrl+e": case "ctrl+e":
cmd := openTempfileEditor("message.*.md", m.input.Value(), "# Edit your input below\n") cmd := openTempfileEditor("message.*.md", m.input.Value(), "# Edit your input below\n")
m.editorTarget = input m.editorTarget = input
return cmd return true, cmd
} }
return nil return false, nil
} }
func (m *chatModel) renderMessageHeading(i int, message *models.Message) string { func (m *chatModel) renderMessageHeading(i int, message *models.Message) string {

View File

@ -41,11 +41,11 @@ func newConversationsModel(tui *model) conversationsModel {
return m return m
} }
func (m *conversationsModel) handleInput(msg tea.KeyMsg) tea.Cmd { func (m *conversationsModel) handleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
switch msg.String() { switch msg.String() {
case "enter": case "enter":
// how to notify chats model // how to notify chats model
return func() tea.Msg { return true, func() tea.Msg {
return msgChangeState(stateChat) return msgChangeState(stateChat)
} }
case "n": case "n":
@ -59,7 +59,7 @@ func (m *conversationsModel) handleInput(msg tea.KeyMsg) tea.Cmd {
case "shift+r": case "shift+r":
// show prompt to generate name for conversation // show prompt to generate name for conversation
} }
return nil return false, nil
} }
func (m conversationsModel) Init() tea.Cmd { func (m conversationsModel) Init() tea.Cmd {

View File

@ -2,7 +2,6 @@ package tui
// The terminal UI for lmcli, launched from the `lmcli chat` command // The terminal UI for lmcli, launched from the `lmcli chat` command
// TODO: // TODO:
// - conversation list view
// - change model // - change model
// - rename conversation // - rename conversation
// - set system prompt // - set system prompt
@ -83,28 +82,33 @@ func (m model) Init() tea.Cmd {
} }
} }
func (m *model) handleGlobalInput(msg tea.KeyMsg) tea.Cmd { func (m *model) handleGlobalInput(msg tea.KeyMsg) (bool, tea.Cmd) {
switch msg.String() { // delegate input to the active child state first, only handling it at the
case "ctrl+c": // global level if the child state does not
if m.chat.waitingForReply { var cmds []tea.Cmd
m.chat.stopSignal <- struct{}{}
return nil
} else {
return tea.Quit
}
case "q":
if m.chat.focus != focusInput {
return tea.Quit
}
default:
switch m.state { switch m.state {
case stateChat: case stateChat:
return m.chat.handleInput(msg) 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 stateConversations: case stateConversations:
return m.conversations.handleInput(msg) 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) { 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) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
cmd := m.handleGlobalInput(msg) handled, cmd := m.handleGlobalInput(msg)
if cmd != nil { if handled {
return m, cmd return m, cmd
} }
case msgChangeState: case msgChangeState: