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}