tui: open input/messages for editing in $EDITOR
This commit is contained in:
parent
7a974d9764
commit
a8ffdc156a
@ -2,8 +2,7 @@ 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:
|
||||||
// - binding to open selected message/input in $EDITOR
|
// - ability to continue an incomplete or missing assistant response
|
||||||
// - ability to continue or retry previous response
|
|
||||||
// - conversation list view
|
// - conversation list view
|
||||||
// - change model
|
// - change model
|
||||||
// - rename conversation
|
// - rename conversation
|
||||||
@ -33,6 +32,13 @@ const (
|
|||||||
focusMessages
|
focusMessages
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type editorTarget int
|
||||||
|
|
||||||
|
const (
|
||||||
|
input editorTarget = iota
|
||||||
|
selectedMessage
|
||||||
|
)
|
||||||
|
|
||||||
type model struct {
|
type model struct {
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
@ -44,6 +50,7 @@ type model struct {
|
|||||||
conversation *models.Conversation
|
conversation *models.Conversation
|
||||||
messages []models.Message
|
messages []models.Message
|
||||||
waitingForReply bool
|
waitingForReply bool
|
||||||
|
editorTarget editorTarget
|
||||||
stopSignal chan interface{}
|
stopSignal chan interface{}
|
||||||
replyChan chan models.Message
|
replyChan chan models.Message
|
||||||
replyChunkChan chan string
|
replyChunkChan chan string
|
||||||
@ -123,6 +130,22 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
case msgTempfileEditorClosed:
|
||||||
|
contents := string(msg)
|
||||||
|
switch m.editorTarget {
|
||||||
|
case input:
|
||||||
|
m.input.SetValue(contents)
|
||||||
|
case selectedMessage:
|
||||||
|
m.setMessageContents(m.selectedMessage, contents)
|
||||||
|
if m.persistence && m.messages[m.selectedMessage].ID > 0 {
|
||||||
|
// update persisted message
|
||||||
|
err := m.ctx.Store.UpdateMessage(&m.messages[m.selectedMessage])
|
||||||
|
if err != nil {
|
||||||
|
cmds = append(cmds, wrapError(fmt.Errorf("Could not save edited message: %v", err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.updateContent()
|
||||||
|
}
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c":
|
case "ctrl+c":
|
||||||
@ -437,6 +460,11 @@ func (m *model) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
|
|||||||
m.focus = focusInput
|
m.focus = focusInput
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
m.input.Focus()
|
m.input.Focus()
|
||||||
|
case "e":
|
||||||
|
message := m.messages[m.selectedMessage]
|
||||||
|
cmd := openTempfileEditor("message.*.md", message.Content, "# Edit the message below\n")
|
||||||
|
m.editorTarget = selectedMessage
|
||||||
|
return 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--
|
||||||
@ -508,6 +536,10 @@ func (m *model) handleInputKey(msg tea.KeyMsg) tea.Cmd {
|
|||||||
m.updateContent()
|
m.updateContent()
|
||||||
m.content.GotoBottom()
|
m.content.GotoBottom()
|
||||||
return m.promptLLM()
|
return m.promptLLM()
|
||||||
|
case "ctrl+e":
|
||||||
|
cmd := openTempfileEditor("message.*.md", m.input.Value(), "# Edit your input below\n")
|
||||||
|
m.editorTarget = input
|
||||||
|
return cmd
|
||||||
case "ctrl+r":
|
case "ctrl+r":
|
||||||
if len(m.messages) == 0 {
|
if len(m.messages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
42
pkg/tui/util.go
Normal file
42
pkg/tui/util.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type msgTempfileEditorClosed string
|
||||||
|
|
||||||
|
// openTempfileEditor opens an $EDITOR on a new temporary file with the given
|
||||||
|
// content. Upon closing, the contents of the file are read back returned
|
||||||
|
// wrapped in a msgTempfileEditorClosed returned by the tea.Cmd
|
||||||
|
func openTempfileEditor(pattern string, content string, placeholder string) tea.Cmd {
|
||||||
|
msgFile, _ := os.CreateTemp("/tmp", pattern)
|
||||||
|
|
||||||
|
err := os.WriteFile(msgFile.Name(), []byte(placeholder+content), os.ModeAppend)
|
||||||
|
if err != nil {
|
||||||
|
return wrapError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
editor := os.Getenv("EDITOR")
|
||||||
|
if editor == "" {
|
||||||
|
editor = "vim"
|
||||||
|
}
|
||||||
|
|
||||||
|
c := exec.Command(editor, msgFile.Name())
|
||||||
|
return tea.ExecProcess(c, func(err error) tea.Msg {
|
||||||
|
bytes, err := os.ReadFile(msgFile.Name())
|
||||||
|
if err != nil {
|
||||||
|
return msgError(err)
|
||||||
|
}
|
||||||
|
fileContents := string(bytes)
|
||||||
|
if strings.HasPrefix(fileContents, placeholder) {
|
||||||
|
fileContents = fileContents[len(placeholder):]
|
||||||
|
}
|
||||||
|
stripped := strings.Trim(fileContents, "\n \t")
|
||||||
|
return msgTempfileEditorClosed(stripped)
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user