1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
|
// Package write provides a shell script interface for the text area bubble.
// https://github.com/charmbracelet/bubbles/tree/master/textarea
//
// It can be used to ask the user to write some long form of text (multi-line)
// input. The text the user entered will be sent to stdout.
// Text entry is completed with CTRL+D and aborted with CTRL+C or Escape.
//
// $ gum write > output.text
package write
import (
"io"
"os"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/editor"
)
type keymap struct {
textarea.KeyMap
Submit key.Binding
Quit key.Binding
Abort key.Binding
OpenInEditor key.Binding
}
// FullHelp implements help.KeyMap.
func (k keymap) FullHelp() [][]key.Binding { return nil }
// ShortHelp implements help.KeyMap.
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{
k.InsertNewline,
k.OpenInEditor,
k.Submit,
}
}
func defaultKeymap() keymap {
km := textarea.DefaultKeyMap
km.InsertNewline = key.NewBinding(
key.WithKeys("ctrl+j"),
key.WithHelp("ctrl+j", "insert newline"),
)
return keymap{
KeyMap: km,
Quit: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "quit"),
),
Abort: key.NewBinding(
key.WithKeys("ctrl+c"),
key.WithHelp("ctrl+c", "cancel"),
),
OpenInEditor: key.NewBinding(
key.WithKeys("ctrl+e"),
key.WithHelp("ctrl+e", "open editor"),
),
Submit: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "submit"),
),
}
}
type model struct {
autoWidth bool
header string
headerStyle lipgloss.Style
quitting bool
submitted bool
textarea textarea.Model
showHelp bool
help help.Model
keymap keymap
padding []int
}
func (m model) Init() tea.Cmd { return textarea.Blink }
func (m model) View() string {
if m.quitting {
return ""
}
var parts []string
// Display the header above the text area if it is not empty.
if m.header != "" {
parts = append(parts, m.headerStyle.Render(m.header))
}
parts = append(parts, m.textarea.View())
if m.showHelp {
parts = append(parts, "", m.help.View(m.keymap))
}
return lipgloss.NewStyle().
Padding(m.padding...).
Render(lipgloss.JoinVertical(
lipgloss.Left,
parts...,
))
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
if m.autoWidth {
m.textarea.SetWidth(msg.Width - m.padding[1] - m.padding[3])
}
case tea.FocusMsg, tea.BlurMsg:
var cmd tea.Cmd
m.textarea, cmd = m.textarea.Update(msg)
return m, cmd
case startEditorMsg:
return m, openEditor(msg.path, msg.lineno)
case editorFinishedMsg:
if msg.err != nil {
m.quitting = true
return m, tea.Interrupt
}
m.textarea.SetValue(msg.content)
case tea.KeyMsg:
km := m.keymap
switch {
case key.Matches(msg, km.Abort):
m.quitting = true
return m, tea.Interrupt
case key.Matches(msg, km.Quit):
m.quitting = true
return m, tea.Quit
case key.Matches(msg, km.Submit):
m.quitting = true
m.submitted = true
return m, tea.Quit
case key.Matches(msg, km.OpenInEditor):
//nolint: gosec
return m, createTempFile(m.textarea.Value(), uint(m.textarea.Line())+1)
}
}
var cmd tea.Cmd
m.textarea, cmd = m.textarea.Update(msg)
return m, cmd
}
type startEditorMsg struct {
path string
lineno uint
}
type editorFinishedMsg struct {
content string
err error
}
func createTempFile(content string, lineno uint) tea.Cmd {
return func() tea.Msg {
f, err := os.CreateTemp("", "gum.*.md")
if err != nil {
return editorFinishedMsg{err: err}
}
_, err = io.WriteString(f, content)
if err != nil {
return editorFinishedMsg{err: err}
}
_ = f.Close()
return startEditorMsg{
path: f.Name(),
lineno: lineno,
}
}
}
func openEditor(path string, lineno uint) tea.Cmd {
cb := func(err error) tea.Msg {
if err != nil {
return editorFinishedMsg{
err: err,
}
}
bts, err := os.ReadFile(path)
if err != nil {
return editorFinishedMsg{err: err}
}
return editorFinishedMsg{
content: string(bts),
}
}
cmd, err := editor.Cmd(
"Gum",
path,
editor.LineNumber(int(lineno)),
editor.EndOfLine(),
)
if err != nil {
return func() tea.Msg { return cb(err) }
}
return tea.ExecProcess(cmd, cb)
}
|