Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105package tui
import ( "cmp" "errors" "fmt" "io" "os" "os/exec" "strings"
tea "charm.land/bubbletea/v2" "github.com/charmbracelet/x/ansi")
func (m ChatModel) openEditorCmd() tea.Cmd { args := strings.Fields(cmp.Or(os.Getenv("EDITOR"), os.Getenv("VISUAL"), "nano")) if len(args) == 0 { args = []string{"nano"} } process := &promptEditorProcess{ editorProcess: editorProcess{exec.Command(args[0], args[1:]...), m.graphemes}, prompt: m.TextArea.Value(), } sessionID, generation := m.SessionID, m.Generation return tea.Exec(process, func(err error) tea.Msg { return ChatEditorFinishedMsg{SessionID: sessionID, Generation: generation, Text: process.text, Edited: process.edited, Err: err} })}
// promptEditorProcess owns the prompt file for the complete editor operation.// Its completion message carries text even when the UI no longer owns the session.type promptEditorProcess struct { editorProcess prompt string text string edited bool}
func (p *promptEditorProcess) Run() error { file, err := os.CreateTemp("", "albedo-prompt-*.md") if err != nil { return fmt.Errorf("could not create temporary file: %w", err) } path := file.Name() defer os.Remove(path) if _, err := file.WriteString(p.prompt); err != nil { file.Close() return fmt.Errorf("could not write to temporary file: %w", err) } if err := file.Close(); err != nil { return fmt.Errorf("could not close temporary file: %w", err) }
p.Args = append(p.Args, path) commandErr, restoreErr := p.runAndRestore() if commandErr != nil { return errors.Join(commandErr, restoreErr) } data, err := os.ReadFile(path) if err != nil { return errors.Join(fmt.Errorf("could not read the edited prompt: %w", err), restoreErr) } p.text = string(data) p.edited = true return restoreErr}
// editorProcess runs the editor on the terminal, then hands the terminal back// the way Bubble Tea left it. Bubble Tea repaints from where it left the// cursor and trusts the column, but leaving the alternate screen puts the// cursor back wherever the editor found it, and a terminal reply echoed// before the editor took raw mode can have moved it along the row; the// repaint then starts mid-row, wraps the header and scrolls it off the top.// Bubble Tea also turns grapheme widths off for the editor and never turns// them back on, while it keeps measuring by grapheme.type editorProcess struct { *exec.Cmd graphemes bool}
func (p editorProcess) SetStdin(r io.Reader) { p.Stdin = r }func (p editorProcess) SetStdout(w io.Writer) { p.Stdout = w }func (p editorProcess) SetStderr(w io.Writer) { p.Stderr = w }
func (p editorProcess) Run() error { return errors.Join(p.runAndRestore())}
func (p editorProcess) runAndRestore() (commandErr, restoreErr error) { commandErr = p.Cmd.Run() restore := "\r" if p.graphemes { restore += ansi.SetModeUnicodeCore } written, err := io.WriteString(p.Stdout, restore) if err == nil && written != len(restore) { err = io.ErrShortWrite } if err != nil { restoreErr = fmt.Errorf("could not restore terminal output after the editor: %w", err) } return commandErr, restoreErr}