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()
)
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 {

View File

@ -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 {

View File

@ -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: