tui: open input/messages for editing in $EDITOR
This commit is contained in:
parent
3d8d3b61b3
commit
c53e952acc
@ -2,8 +2,7 @@ package tui
|
||||
|
||||
// The terminal UI for lmcli, launched from the `lmcli chat` command
|
||||
// TODO:
|
||||
// - binding to open selected message/input in $EDITOR
|
||||
// - ability to continue or retry previous response
|
||||
// - ability to continue an incomplete or missing assistant response
|
||||
// - conversation list view
|
||||
// - change model
|
||||
// - rename conversation
|
||||
@ -33,6 +32,13 @@ const (
|
||||
focusMessages
|
||||
)
|
||||
|
||||
type editorTarget int
|
||||
|
||||
const (
|
||||
input editorTarget = iota
|
||||
selectedMessage
|
||||
)
|
||||
|
||||
type model struct {
|
||||
width int
|
||||
height int
|
||||
@ -44,6 +50,7 @@ type model struct {
|
||||
conversation *models.Conversation
|
||||
messages []models.Message
|
||||
waitingForReply bool
|
||||
editorTarget editorTarget
|
||||
stopSignal chan interface{}
|
||||
replyChan chan models.Message
|
||||
replyChunkChan chan string
|
||||
@ -123,6 +130,22 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
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:
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
@ -437,6 +460,11 @@ func (m *model) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
|
||||
m.focus = focusInput
|
||||
m.updateContent()
|
||||
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":
|
||||
if m.selectedMessage > 0 && len(m.messages) == len(m.messageOffsets) {
|
||||
m.selectedMessage--
|
||||
@ -508,6 +536,10 @@ func (m *model) handleInputKey(msg tea.KeyMsg) tea.Cmd {
|
||||
m.updateContent()
|
||||
m.content.GotoBottom()
|
||||
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":
|
||||
if len(m.messages) == 0 {
|
||||
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