textinput.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. package textinput
  2. import (
  3. "strings"
  4. "time"
  5. "unicode"
  6. "github.com/atotto/clipboard"
  7. "github.com/charmbracelet/bubbles/cursor"
  8. "github.com/charmbracelet/bubbles/key"
  9. "github.com/charmbracelet/bubbles/runeutil"
  10. tea "github.com/charmbracelet/bubbletea"
  11. "github.com/charmbracelet/lipgloss"
  12. rw "github.com/mattn/go-runewidth"
  13. )
  14. // Internal messages for clipboard operations.
  15. type pasteMsg string
  16. type pasteErrMsg struct{ error }
  17. // EchoMode sets the input behavior of the text input field.
  18. type EchoMode int
  19. const (
  20. // EchoNormal displays text as is. This is the default behavior.
  21. EchoNormal EchoMode = iota
  22. // EchoPassword displays the EchoCharacter mask instead of actual
  23. // characters. This is commonly used for password fields.
  24. EchoPassword
  25. // EchoNone displays nothing as characters are entered. This is commonly
  26. // seen for password fields on the command line.
  27. EchoNone
  28. // EchoOnEdit.
  29. )
  30. // ValidateFunc is a function that returns an error if the input is invalid.
  31. type ValidateFunc func(string) error
  32. // KeyMap is the key bindings for different actions within the textinput.
  33. type KeyMap struct {
  34. CharacterForward key.Binding
  35. CharacterBackward key.Binding
  36. WordForward key.Binding
  37. WordBackward key.Binding
  38. DeleteWordBackward key.Binding
  39. DeleteWordForward key.Binding
  40. DeleteAfterCursor key.Binding
  41. DeleteBeforeCursor key.Binding
  42. DeleteCharacterBackward key.Binding
  43. DeleteCharacterForward key.Binding
  44. LineStart key.Binding
  45. LineEnd key.Binding
  46. Paste key.Binding
  47. }
  48. // DefaultKeyMap is the default set of key bindings for navigating and acting
  49. // upon the textinput.
  50. var DefaultKeyMap = KeyMap{
  51. CharacterForward: key.NewBinding(key.WithKeys("right", "ctrl+f")),
  52. CharacterBackward: key.NewBinding(key.WithKeys("left", "ctrl+b")),
  53. WordForward: key.NewBinding(key.WithKeys("alt+right", "alt+f")),
  54. WordBackward: key.NewBinding(key.WithKeys("alt+left", "alt+b")),
  55. DeleteWordBackward: key.NewBinding(key.WithKeys("alt+backspace", "ctrl+w")),
  56. DeleteWordForward: key.NewBinding(key.WithKeys("alte+delete", "alt+d")),
  57. DeleteAfterCursor: key.NewBinding(key.WithKeys("ctrl+k")),
  58. DeleteBeforeCursor: key.NewBinding(key.WithKeys("ctrl+u")),
  59. DeleteCharacterBackward: key.NewBinding(key.WithKeys("backspace", "ctrl+h")),
  60. DeleteCharacterForward: key.NewBinding(key.WithKeys("delete", "ctrl+d")),
  61. LineStart: key.NewBinding(key.WithKeys("home", "ctrl+a")),
  62. LineEnd: key.NewBinding(key.WithKeys("end", "ctrl+e")),
  63. Paste: key.NewBinding(key.WithKeys("ctrl+v")),
  64. }
  65. // Model is the Bubble Tea model for this text input element.
  66. type Model struct {
  67. Err error
  68. // General settings.
  69. Prompt string
  70. Placeholder string
  71. EchoMode EchoMode
  72. EchoCharacter rune
  73. Cursor cursor.Model
  74. // Deprecated: use [cursor.BlinkSpeed] instead.
  75. BlinkSpeed time.Duration
  76. // Styles. These will be applied as inline styles.
  77. //
  78. // For an introduction to styling with Lip Gloss see:
  79. // https://github.com/charmbracelet/lipgloss
  80. PromptStyle lipgloss.Style
  81. TextStyle lipgloss.Style
  82. BackgroundStyle lipgloss.Style
  83. PlaceholderStyle lipgloss.Style
  84. CursorStyle lipgloss.Style
  85. // CharLimit is the maximum amount of characters this input element will
  86. // accept. If 0 or less, there's no limit.
  87. CharLimit int
  88. // Width is the maximum number of characters that can be displayed at once.
  89. // It essentially treats the text field like a horizontally scrolling
  90. // viewport. If 0 or less this setting is ignored.
  91. Width int
  92. // KeyMap encodes the keybindings recognized by the widget.
  93. KeyMap KeyMap
  94. // Underlying text value.
  95. value []rune
  96. // focus indicates whether user input focus should be on this input
  97. // component. When false, ignore keyboard input and hide the cursor.
  98. focus bool
  99. // Cursor position.
  100. pos int
  101. // Used to emulate a viewport when width is set and the content is
  102. // overflowing.
  103. offset int
  104. offsetRight int
  105. // Validate is a function that checks whether or not the text within the
  106. // input is valid. If it is not valid, the `Err` field will be set to the
  107. // error returned by the function. If the function is not defined, all
  108. // input is considered valid.
  109. Validate ValidateFunc
  110. // rune sanitizer for input.
  111. rsan runeutil.Sanitizer
  112. }
  113. // New creates a new model with default settings.
  114. func New() Model {
  115. return Model{
  116. Prompt: "> ",
  117. EchoCharacter: '*',
  118. CharLimit: 0,
  119. PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
  120. Cursor: cursor.New(),
  121. KeyMap: DefaultKeyMap,
  122. value: nil,
  123. focus: false,
  124. pos: 0,
  125. }
  126. }
  127. // NewModel creates a new model with default settings.
  128. //
  129. // Deprecated: Use [New] instead.
  130. var NewModel = New
  131. // SetValue sets the value of the text input.
  132. func (m *Model) SetValue(s string) {
  133. // Clean up any special characters in the input provided by the
  134. // caller. This avoids bugs due to e.g. tab characters and whatnot.
  135. runes := m.san().Sanitize([]rune(s))
  136. m.setValueInternal(runes)
  137. }
  138. func (m *Model) setValueInternal(runes []rune) {
  139. if m.Validate != nil {
  140. if err := m.Validate(string(runes)); err != nil {
  141. m.Err = err
  142. return
  143. }
  144. }
  145. empty := len(m.value) == 0
  146. m.Err = nil
  147. if m.CharLimit > 0 && len(runes) > m.CharLimit {
  148. m.value = runes[:m.CharLimit]
  149. } else {
  150. m.value = runes
  151. }
  152. if (m.pos == 0 && empty) || m.pos > len(m.value) {
  153. m.SetCursor(len(m.value))
  154. }
  155. m.handleOverflow()
  156. }
  157. // Value returns the value of the text input.
  158. func (m Model) Value() string {
  159. return string(m.value)
  160. }
  161. // Position returns the cursor position.
  162. func (m Model) Position() int {
  163. return m.pos
  164. }
  165. // SetCursor moves the cursor to the given position. If the position is
  166. // out of bounds the cursor will be moved to the start or end accordingly.
  167. func (m *Model) SetCursor(pos int) {
  168. m.pos = clamp(pos, 0, len(m.value))
  169. m.handleOverflow()
  170. }
  171. // CursorStart moves the cursor to the start of the input field.
  172. func (m *Model) CursorStart() {
  173. m.SetCursor(0)
  174. }
  175. // CursorEnd moves the cursor to the end of the input field.
  176. func (m *Model) CursorEnd() {
  177. m.SetCursor(len(m.value))
  178. }
  179. // Focused returns the focus state on the model.
  180. func (m Model) Focused() bool {
  181. return m.focus
  182. }
  183. // Focus sets the focus state on the model. When the model is in focus it can
  184. // receive keyboard input and the cursor will be shown.
  185. func (m *Model) Focus() tea.Cmd {
  186. m.focus = true
  187. return m.Cursor.Focus()
  188. }
  189. // Blur removes the focus state on the model. When the model is blurred it can
  190. // not receive keyboard input and the cursor will be hidden.
  191. func (m *Model) Blur() {
  192. m.focus = false
  193. m.Cursor.Blur()
  194. }
  195. // Reset sets the input to its default state with no input.
  196. func (m *Model) Reset() {
  197. m.value = nil
  198. m.SetCursor(0)
  199. }
  200. // rsan initializes or retrieves the rune sanitizer.
  201. func (m *Model) san() runeutil.Sanitizer {
  202. if m.rsan == nil {
  203. // Textinput has all its input on a single line so collapse
  204. // newlines/tabs to single spaces.
  205. m.rsan = runeutil.NewSanitizer(
  206. runeutil.ReplaceTabs(" "), runeutil.ReplaceNewlines(" "))
  207. }
  208. return m.rsan
  209. }
  210. func (m *Model) insertRunesFromUserInput(v []rune) {
  211. // Clean up any special characters in the input provided by the
  212. // clipboard. This avoids bugs due to e.g. tab characters and
  213. // whatnot.
  214. paste := m.san().Sanitize(v)
  215. var availSpace int
  216. if m.CharLimit > 0 {
  217. availSpace = m.CharLimit - len(m.value)
  218. // If the char limit's been reached, cancel.
  219. if availSpace <= 0 {
  220. return
  221. }
  222. // If there's not enough space to paste the whole thing cut the pasted
  223. // runes down so they'll fit.
  224. if availSpace < len(paste) {
  225. paste = paste[:len(paste)-availSpace]
  226. }
  227. }
  228. // Stuff before and after the cursor
  229. head := m.value[:m.pos]
  230. tailSrc := m.value[m.pos:]
  231. tail := make([]rune, len(tailSrc))
  232. copy(tail, tailSrc)
  233. oldPos := m.pos
  234. // Insert pasted runes
  235. for _, r := range paste {
  236. head = append(head, r)
  237. m.pos++
  238. if m.CharLimit > 0 {
  239. availSpace--
  240. if availSpace <= 0 {
  241. break
  242. }
  243. }
  244. }
  245. // Put it all back together
  246. value := append(head, tail...)
  247. m.setValueInternal(value)
  248. if m.Err != nil {
  249. m.pos = oldPos
  250. }
  251. }
  252. // If a max width is defined, perform some logic to treat the visible area
  253. // as a horizontally scrolling viewport.
  254. func (m *Model) handleOverflow() {
  255. if m.Width <= 0 || rw.StringWidth(string(m.value)) <= m.Width {
  256. m.offset = 0
  257. m.offsetRight = len(m.value)
  258. return
  259. }
  260. // Correct right offset if we've deleted characters
  261. m.offsetRight = min(m.offsetRight, len(m.value))
  262. if m.pos < m.offset {
  263. m.offset = m.pos
  264. w := 0
  265. i := 0
  266. runes := m.value[m.offset:]
  267. for i < len(runes) && w <= m.Width {
  268. w += rw.RuneWidth(runes[i])
  269. if w <= m.Width+1 {
  270. i++
  271. }
  272. }
  273. m.offsetRight = m.offset + i
  274. } else if m.pos >= m.offsetRight {
  275. m.offsetRight = m.pos
  276. w := 0
  277. runes := m.value[:m.offsetRight]
  278. i := len(runes) - 1
  279. for i > 0 && w < m.Width {
  280. w += rw.RuneWidth(runes[i])
  281. if w <= m.Width {
  282. i--
  283. }
  284. }
  285. m.offset = m.offsetRight - (len(runes) - 1 - i)
  286. }
  287. }
  288. // deleteBeforeCursor deletes all text before the cursor.
  289. func (m *Model) deleteBeforeCursor() {
  290. m.value = m.value[m.pos:]
  291. m.offset = 0
  292. m.SetCursor(0)
  293. }
  294. // deleteAfterCursor deletes all text after the cursor. If input is masked
  295. // delete everything after the cursor so as not to reveal word breaks in the
  296. // masked input.
  297. func (m *Model) deleteAfterCursor() {
  298. m.value = m.value[:m.pos]
  299. m.SetCursor(len(m.value))
  300. }
  301. // deleteWordBackward deletes the word left to the cursor.
  302. func (m *Model) deleteWordBackward() {
  303. if m.pos == 0 || len(m.value) == 0 {
  304. return
  305. }
  306. if m.EchoMode != EchoNormal {
  307. m.deleteBeforeCursor()
  308. return
  309. }
  310. // Linter note: it's critical that we acquire the initial cursor position
  311. // here prior to altering it via SetCursor() below. As such, moving this
  312. // call into the corresponding if clause does not apply here.
  313. oldPos := m.pos //nolint:ifshort
  314. m.SetCursor(m.pos - 1)
  315. for unicode.IsSpace(m.value[m.pos]) {
  316. if m.pos <= 0 {
  317. break
  318. }
  319. // ignore series of whitespace before cursor
  320. m.SetCursor(m.pos - 1)
  321. }
  322. for m.pos > 0 {
  323. if !unicode.IsSpace(m.value[m.pos]) {
  324. m.SetCursor(m.pos - 1)
  325. } else {
  326. if m.pos > 0 {
  327. // keep the previous space
  328. m.SetCursor(m.pos + 1)
  329. }
  330. break
  331. }
  332. }
  333. if oldPos > len(m.value) {
  334. m.value = m.value[:m.pos]
  335. } else {
  336. m.value = append(m.value[:m.pos], m.value[oldPos:]...)
  337. }
  338. }
  339. // deleteWordForward deletes the word right to the cursor If input is masked
  340. // delete everything after the cursor so as not to reveal word breaks in the
  341. // masked input.
  342. func (m *Model) deleteWordForward() {
  343. if m.pos >= len(m.value) || len(m.value) == 0 {
  344. return
  345. }
  346. if m.EchoMode != EchoNormal {
  347. m.deleteAfterCursor()
  348. return
  349. }
  350. oldPos := m.pos
  351. m.SetCursor(m.pos + 1)
  352. for unicode.IsSpace(m.value[m.pos]) {
  353. // ignore series of whitespace after cursor
  354. m.SetCursor(m.pos + 1)
  355. if m.pos >= len(m.value) {
  356. break
  357. }
  358. }
  359. for m.pos < len(m.value) {
  360. if !unicode.IsSpace(m.value[m.pos]) {
  361. m.SetCursor(m.pos + 1)
  362. } else {
  363. break
  364. }
  365. }
  366. if m.pos > len(m.value) {
  367. m.value = m.value[:oldPos]
  368. } else {
  369. m.value = append(m.value[:oldPos], m.value[m.pos:]...)
  370. }
  371. m.SetCursor(oldPos)
  372. }
  373. // wordBackward moves the cursor one word to the left. If input is masked, move
  374. // input to the start so as not to reveal word breaks in the masked input.
  375. func (m *Model) wordBackward() {
  376. if m.pos == 0 || len(m.value) == 0 {
  377. return
  378. }
  379. if m.EchoMode != EchoNormal {
  380. m.CursorStart()
  381. return
  382. }
  383. i := m.pos - 1
  384. for i >= 0 {
  385. if unicode.IsSpace(m.value[i]) {
  386. m.SetCursor(m.pos - 1)
  387. i--
  388. } else {
  389. break
  390. }
  391. }
  392. for i >= 0 {
  393. if !unicode.IsSpace(m.value[i]) {
  394. m.SetCursor(m.pos - 1)
  395. i--
  396. } else {
  397. break
  398. }
  399. }
  400. }
  401. // wordForward moves the cursor one word to the right. If the input is masked,
  402. // move input to the end so as not to reveal word breaks in the masked input.
  403. func (m *Model) wordForward() {
  404. if m.pos >= len(m.value) || len(m.value) == 0 {
  405. return
  406. }
  407. if m.EchoMode != EchoNormal {
  408. m.CursorEnd()
  409. return
  410. }
  411. i := m.pos
  412. for i < len(m.value) {
  413. if unicode.IsSpace(m.value[i]) {
  414. m.SetCursor(m.pos + 1)
  415. i++
  416. } else {
  417. break
  418. }
  419. }
  420. for i < len(m.value) {
  421. if !unicode.IsSpace(m.value[i]) {
  422. m.SetCursor(m.pos + 1)
  423. i++
  424. } else {
  425. break
  426. }
  427. }
  428. }
  429. func (m Model) echoTransform(v string) string {
  430. switch m.EchoMode {
  431. case EchoPassword:
  432. return strings.Repeat(string(m.EchoCharacter), rw.StringWidth(v))
  433. case EchoNone:
  434. return ""
  435. default:
  436. return v
  437. }
  438. }
  439. // Update is the Bubble Tea update loop.
  440. func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
  441. if !m.focus {
  442. return m, nil
  443. }
  444. // Let's remember where the position of the cursor currently is so that if
  445. // the cursor position changes, we can reset the blink.
  446. oldPos := m.pos //nolint
  447. switch msg := msg.(type) {
  448. case tea.KeyMsg:
  449. switch {
  450. case key.Matches(msg, m.KeyMap.DeleteWordBackward):
  451. m.Err = nil
  452. m.deleteWordBackward()
  453. case key.Matches(msg, m.KeyMap.DeleteCharacterBackward):
  454. m.Err = nil
  455. if len(m.value) > 0 {
  456. m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
  457. if m.pos > 0 {
  458. m.SetCursor(m.pos - 1)
  459. }
  460. }
  461. case key.Matches(msg, m.KeyMap.WordBackward):
  462. m.wordBackward()
  463. case key.Matches(msg, m.KeyMap.CharacterBackward):
  464. if m.pos > 0 {
  465. m.SetCursor(m.pos - 1)
  466. }
  467. case key.Matches(msg, m.KeyMap.WordForward):
  468. m.wordForward()
  469. case key.Matches(msg, m.KeyMap.CharacterForward):
  470. if m.pos < len(m.value) {
  471. m.SetCursor(m.pos + 1)
  472. }
  473. case key.Matches(msg, m.KeyMap.DeleteWordBackward):
  474. m.deleteWordBackward()
  475. case key.Matches(msg, m.KeyMap.LineStart):
  476. m.CursorStart()
  477. case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
  478. if len(m.value) > 0 && m.pos < len(m.value) {
  479. m.value = append(m.value[:m.pos], m.value[m.pos+1:]...)
  480. }
  481. case key.Matches(msg, m.KeyMap.LineEnd):
  482. m.CursorEnd()
  483. case key.Matches(msg, m.KeyMap.DeleteAfterCursor):
  484. m.deleteAfterCursor()
  485. case key.Matches(msg, m.KeyMap.DeleteBeforeCursor):
  486. m.deleteBeforeCursor()
  487. case key.Matches(msg, m.KeyMap.Paste):
  488. return m, Paste
  489. case key.Matches(msg, m.KeyMap.DeleteWordForward):
  490. m.deleteWordForward()
  491. default:
  492. // Input one or more regular characters.
  493. m.insertRunesFromUserInput(msg.Runes)
  494. }
  495. case pasteMsg:
  496. m.insertRunesFromUserInput([]rune(msg))
  497. case pasteErrMsg:
  498. m.Err = msg
  499. }
  500. var cmds []tea.Cmd
  501. var cmd tea.Cmd
  502. m.Cursor, cmd = m.Cursor.Update(msg)
  503. cmds = append(cmds, cmd)
  504. if oldPos != m.pos {
  505. m.Cursor.Blink = false
  506. cmds = append(cmds, m.Cursor.BlinkCmd())
  507. }
  508. m.handleOverflow()
  509. return m, tea.Batch(cmds...)
  510. }
  511. // View renders the textinput in its current state.
  512. func (m Model) View() string {
  513. // Placeholder text
  514. if len(m.value) == 0 && m.Placeholder != "" {
  515. return m.placeholderView()
  516. }
  517. styleText := m.TextStyle.Inline(true).Render
  518. value := m.value[m.offset:m.offsetRight]
  519. pos := max(0, m.pos-m.offset)
  520. v := styleText(m.echoTransform(string(value[:pos])))
  521. if pos < len(value) {
  522. char := m.echoTransform(string(value[pos]))
  523. m.Cursor.SetChar(char)
  524. v += m.Cursor.View() // cursor and text under it
  525. v += styleText(m.echoTransform(string(value[pos+1:]))) // text after cursor
  526. } else {
  527. m.Cursor.SetChar(" ")
  528. v += m.Cursor.View()
  529. }
  530. // If a max width and background color were set fill the empty spaces with
  531. // the background color.
  532. valWidth := rw.StringWidth(string(value))
  533. if m.Width > 0 && valWidth <= m.Width {
  534. padding := max(0, m.Width-valWidth)
  535. if valWidth+padding <= m.Width && pos < len(value) {
  536. padding++
  537. }
  538. v += styleText(strings.Repeat(" ", padding))
  539. }
  540. return m.PromptStyle.Render(m.Prompt) + v
  541. }
  542. // placeholderView returns the prompt and placeholder view, if any.
  543. func (m Model) placeholderView() string {
  544. var (
  545. v string
  546. p = m.Placeholder
  547. style = m.PlaceholderStyle.Inline(true).Render
  548. )
  549. m.Cursor.TextStyle = m.PlaceholderStyle
  550. m.Cursor.SetChar(p[:1])
  551. v += m.Cursor.View()
  552. // The rest of the placeholder text
  553. v += style(p[1:])
  554. return m.PromptStyle.Render(m.Prompt) + v
  555. }
  556. // Blink is a command used to initialize cursor blinking.
  557. func Blink() tea.Msg {
  558. return cursor.Blink()
  559. }
  560. // Paste is a command for pasting from the clipboard into the text input.
  561. func Paste() tea.Msg {
  562. str, err := clipboard.ReadAll()
  563. if err != nil {
  564. return pasteErrMsg{err}
  565. }
  566. return pasteMsg(str)
  567. }
  568. func clamp(v, low, high int) int {
  569. if high < low {
  570. low, high = high, low
  571. }
  572. return min(high, max(low, v))
  573. }
  574. func min(a, b int) int {
  575. if a < b {
  576. return a
  577. }
  578. return b
  579. }
  580. func max(a, b int) int {
  581. if a > b {
  582. return a
  583. }
  584. return b
  585. }
  586. // Deprecated.
  587. // Deprecated: use cursor.Mode.
  588. type CursorMode int
  589. const (
  590. // Deprecated: use cursor.CursorBlink.
  591. CursorBlink = CursorMode(cursor.CursorBlink)
  592. // Deprecated: use cursor.CursorStatic.
  593. CursorStatic = CursorMode(cursor.CursorStatic)
  594. // Deprecated: use cursor.CursorHide.
  595. CursorHide = CursorMode(cursor.CursorHide)
  596. )
  597. func (c CursorMode) String() string {
  598. return cursor.Mode(c).String()
  599. }
  600. // Deprecated: use cursor.Mode().
  601. func (m Model) CursorMode() CursorMode {
  602. return CursorMode(m.Cursor.Mode())
  603. }
  604. // Deprecated: use cursor.SetMode().
  605. func (m *Model) SetCursorMode(mode CursorMode) tea.Cmd {
  606. return m.Cursor.SetMode(cursor.Mode(mode))
  607. }