editor.go

  1// Package tuieditor provides a dialog component for embedding terminal-based
  2// editors (vim, nvim, nano, etc.) in the TUI.
  3package tuieditor
  4
  5import (
  6	"context"
  7	"os"
  8	"path/filepath"
  9	"slices"
 10	"strings"
 11
 12	tea "charm.land/bubbletea/v2"
 13
 14	"github.com/charmbracelet/crush/internal/terminal"
 15	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
 16	"github.com/charmbracelet/crush/internal/tui/components/dialogs/termdialog"
 17	"github.com/charmbracelet/crush/internal/tui/util"
 18)
 19
 20// DialogID is the unique identifier for the embedded editor dialog.
 21const DialogID dialogs.DialogID = "tui_editor"
 22
 23// EditorResultMsg is sent when the embedded editor closes with the file content.
 24type EditorResultMsg struct {
 25	Content string
 26	Err     error
 27}
 28
 29// knownTUIEditors is a list of terminal-based editors that can be embedded.
 30var knownTUIEditors = []string{
 31	"vim",
 32	"nvim",
 33	"vi",
 34	"nano",
 35	"helix",
 36	"hx",
 37	"micro",
 38	"emacs",
 39	"joe",
 40	"ne",
 41	"jed",
 42	"kak",
 43	"pico",
 44	"mcedit",
 45	"mg",
 46	"zile",
 47}
 48
 49// IsTUIEditor returns true if the given editor command is a known TUI editor.
 50func IsTUIEditor(editor string) bool {
 51	base := filepath.Base(editor)
 52	if idx := strings.Index(base, " "); idx != -1 {
 53		base = base[:idx]
 54	}
 55	return slices.Contains(knownTUIEditors, base)
 56}
 57
 58// Config holds configuration for the embedded editor dialog.
 59type Config struct {
 60	// FilePath is the path to the file to edit.
 61	FilePath string
 62	// Editor is the editor command to use.
 63	Editor string
 64	// WorkingDir is the working directory for the editor.
 65	WorkingDir string
 66}
 67
 68// NewDialog creates a new embedded editor dialog. The context controls the
 69// lifetime of the editor process - when cancelled, the process will be killed.
 70// When the editor exits, an EditorResultMsg is emitted with the file content.
 71func NewDialog(ctx context.Context, cfg Config) *termdialog.Dialog {
 72	editorCmd := cfg.Editor
 73	if editorCmd == "" {
 74		editorCmd = "nvim"
 75	}
 76
 77	parts := strings.Fields(editorCmd)
 78	cmdName := parts[0]
 79	args := append(parts[1:], cfg.FilePath)
 80
 81	cmd := terminal.PrepareCmd(
 82		ctx,
 83		cmdName,
 84		args,
 85		cfg.WorkingDir,
 86		nil,
 87	)
 88
 89	filePath := cfg.FilePath
 90
 91	return termdialog.New(termdialog.Config{
 92		ID:         DialogID,
 93		Title:      "Editor",
 94		LoadingMsg: "Starting editor...",
 95		Term:       terminal.New(terminal.Config{Context: ctx, Cmd: cmd}),
 96		OnClose: func() tea.Cmd {
 97			content, err := os.ReadFile(filePath)
 98			_ = os.Remove(filePath)
 99
100			if err != nil {
101				return util.CmdHandler(EditorResultMsg{Err: err})
102			}
103			return util.CmdHandler(EditorResultMsg{
104				Content: strings.TrimSpace(string(content)),
105			})
106		},
107	})
108}