1// Package textinput provides a text input component for Bubble Tea
2// applications.
3package textinput
4
5import (
6 "reflect"
7 "slices"
8 "strings"
9 "unicode"
10
11 "github.com/atotto/clipboard"
12 "github.com/charmbracelet/bubbles/v2/cursor"
13 "github.com/charmbracelet/bubbles/v2/internal/runeutil"
14 "github.com/charmbracelet/bubbles/v2/key"
15 tea "github.com/charmbracelet/bubbletea/v2"
16 "github.com/charmbracelet/lipgloss/v2"
17 rw "github.com/mattn/go-runewidth"
18 "github.com/rivo/uniseg"
19)
20
21// Internal messages for clipboard operations.
22type (
23 pasteMsg string
24 pasteErrMsg struct{ error }
25)
26
27// EchoMode sets the input behavior of the text input field.
28type EchoMode int
29
30const (
31 // EchoNormal displays text as is. This is the default behavior.
32 EchoNormal EchoMode = iota
33
34 // EchoPassword displays the EchoCharacter mask instead of actual
35 // characters. This is commonly used for password fields.
36 EchoPassword
37
38 // EchoNone displays nothing as characters are entered. This is commonly
39 // seen for password fields on the command line.
40 EchoNone
41)
42
43// ValidateFunc is a function that returns an error if the input is invalid.
44type ValidateFunc func(string) error
45
46// KeyMap is the key bindings for different actions within the textinput.
47type KeyMap struct {
48 CharacterForward key.Binding
49 CharacterBackward key.Binding
50 WordForward key.Binding
51 WordBackward key.Binding
52 DeleteWordBackward key.Binding
53 DeleteWordForward key.Binding
54 DeleteAfterCursor key.Binding
55 DeleteBeforeCursor key.Binding
56 DeleteCharacterBackward key.Binding
57 DeleteCharacterForward key.Binding
58 LineStart key.Binding
59 LineEnd key.Binding
60 Paste key.Binding
61 AcceptSuggestion key.Binding
62 NextSuggestion key.Binding
63 PrevSuggestion key.Binding
64}
65
66// DefaultKeyMap is the default set of key bindings for navigating and acting
67// upon the textinput.
68func DefaultKeyMap() KeyMap {
69 return KeyMap{
70 CharacterForward: key.NewBinding(key.WithKeys("right", "ctrl+f")),
71 CharacterBackward: key.NewBinding(key.WithKeys("left", "ctrl+b")),
72 WordForward: key.NewBinding(key.WithKeys("alt+right", "ctrl+right", "alt+f")),
73 WordBackward: key.NewBinding(key.WithKeys("alt+left", "ctrl+left", "alt+b")),
74 DeleteWordBackward: key.NewBinding(key.WithKeys("alt+backspace", "ctrl+w")),
75 DeleteWordForward: key.NewBinding(key.WithKeys("alt+delete", "alt+d")),
76 DeleteAfterCursor: key.NewBinding(key.WithKeys("ctrl+k")),
77 DeleteBeforeCursor: key.NewBinding(key.WithKeys("ctrl+u")),
78 DeleteCharacterBackward: key.NewBinding(key.WithKeys("backspace", "ctrl+h")),
79 DeleteCharacterForward: key.NewBinding(key.WithKeys("delete", "ctrl+d")),
80 LineStart: key.NewBinding(key.WithKeys("home", "ctrl+a")),
81 LineEnd: key.NewBinding(key.WithKeys("end", "ctrl+e")),
82 Paste: key.NewBinding(key.WithKeys("ctrl+v")),
83 AcceptSuggestion: key.NewBinding(key.WithKeys("tab")),
84 NextSuggestion: key.NewBinding(key.WithKeys("down", "ctrl+n")),
85 PrevSuggestion: key.NewBinding(key.WithKeys("up", "ctrl+p")),
86 }
87}
88
89// Model is the Bubble Tea model for this text input element.
90type Model struct {
91 Err error
92
93 // General settings.
94 Prompt string
95 Placeholder string
96 EchoMode EchoMode
97 EchoCharacter rune
98
99 // useVirtualCursor determines whether or not to use the virtual cursor. If
100 // set to false, use [Model.Cursor] to return a real cursor for rendering.
101 useVirtualCursor bool
102
103 // Virtual cursor manager.
104 virtualCursor cursor.Model
105
106 // CharLimit is the maximum amount of characters this input element will
107 // accept. If 0 or less, there's no limit.
108 CharLimit int
109
110 // Styling. FocusedStyle and BlurredStyle are used to style the textarea in
111 // focused and blurred states.
112 styles Styles
113
114 // Width is the maximum number of characters that can be displayed at once.
115 // It essentially treats the text field like a horizontally scrolling
116 // viewport. If 0 or less this setting is ignored.
117 width int
118
119 // KeyMap encodes the keybindings recognized by the widget.
120 KeyMap KeyMap
121
122 // Underlying text value.
123 value []rune
124
125 // focus indicates whether user input focus should be on this input
126 // component. When false, ignore keyboard input and hide the cursor.
127 focus bool
128
129 // Cursor position.
130 pos int
131
132 // Used to emulate a viewport when width is set and the content is
133 // overflowing.
134 offset int
135 offsetRight int
136
137 // Validate is a function that checks whether or not the text within the
138 // input is valid. If it is not valid, the `Err` field will be set to the
139 // error returned by the function. If the function is not defined, all
140 // input is considered valid.
141 Validate ValidateFunc
142
143 // rune sanitizer for input.
144 rsan runeutil.Sanitizer
145
146 // Should the input suggest to complete
147 ShowSuggestions bool
148
149 // suggestions is a list of suggestions that may be used to complete the
150 // input.
151 suggestions [][]rune
152 matchedSuggestions [][]rune
153 currentSuggestionIndex int
154}
155
156// New creates a new model with default settings.
157func New() Model {
158 m := Model{
159 Prompt: "> ",
160 EchoCharacter: '*',
161 CharLimit: 0,
162 styles: DefaultDarkStyles(),
163 ShowSuggestions: false,
164 useVirtualCursor: true,
165 virtualCursor: cursor.New(),
166 KeyMap: DefaultKeyMap(),
167 suggestions: [][]rune{},
168 value: nil,
169 focus: false,
170 pos: 0,
171 }
172 m.updateVirtualCursorStyle()
173 return m
174}
175
176// VirtualCursor returns whether the model is using a virtual cursor.
177func (m Model) VirtualCursor() bool {
178 return m.useVirtualCursor
179}
180
181// SetVirtualCursor sets whether the model should use a virtual cursor. If
182// disabled, use [Model.Cursor] to return a real cursor for rendering.
183func (m *Model) SetVirtualCursor(v bool) {
184 m.useVirtualCursor = v
185 m.updateVirtualCursorStyle()
186}
187
188// Styles returns the current set of styles.
189func (m Model) Styles() Styles {
190 return m.styles
191}
192
193// SetStyles sets the styles for the text input.
194func (m *Model) SetStyles(s Styles) {
195 m.styles = s
196 m.updateVirtualCursorStyle()
197}
198
199// Width returns the width of the text input.
200func (m Model) Width() int {
201 return m.width
202}
203
204// SetWidth sets the width of the text input.
205func (m *Model) SetWidth(w int) {
206 m.width = w
207}
208
209// SetValue sets the value of the text input.
210func (m *Model) SetValue(s string) {
211 // Clean up any special characters in the input provided by the
212 // caller. This avoids bugs due to e.g. tab characters and whatnot.
213 runes := m.san().Sanitize([]rune(s))
214 err := m.validate(runes)
215 m.setValueInternal(runes, err)
216}
217
218func (m *Model) setValueInternal(runes []rune, err error) {
219 m.Err = err
220
221 empty := len(m.value) == 0
222
223 if m.CharLimit > 0 && len(runes) > m.CharLimit {
224 m.value = runes[:m.CharLimit]
225 } else {
226 m.value = runes
227 }
228 if (m.pos == 0 && empty) || m.pos > len(m.value) {
229 m.SetCursor(len(m.value))
230 }
231 m.handleOverflow()
232}
233
234// Value returns the value of the text input.
235func (m Model) Value() string {
236 return string(m.value)
237}
238
239// Position returns the cursor position.
240func (m Model) Position() int {
241 return m.pos
242}
243
244// SetCursor moves the cursor to the given position. If the position is
245// out of bounds the cursor will be moved to the start or end accordingly.
246func (m *Model) SetCursor(pos int) {
247 m.pos = clamp(pos, 0, len(m.value))
248 m.handleOverflow()
249}
250
251// CursorStart moves the cursor to the start of the input field.
252func (m *Model) CursorStart() {
253 m.SetCursor(0)
254}
255
256// CursorEnd moves the cursor to the end of the input field.
257func (m *Model) CursorEnd() {
258 m.SetCursor(len(m.value))
259}
260
261// Focused returns the focus state on the model.
262func (m Model) Focused() bool {
263 return m.focus
264}
265
266// Focus sets the focus state on the model. When the model is in focus it can
267// receive keyboard input and the cursor will be shown.
268func (m *Model) Focus() tea.Cmd {
269 m.focus = true
270 return m.virtualCursor.Focus()
271}
272
273// Blur removes the focus state on the model. When the model is blurred it can
274// not receive keyboard input and the cursor will be hidden.
275func (m *Model) Blur() {
276 m.focus = false
277 m.virtualCursor.Blur()
278}
279
280// Reset sets the input to its default state with no input.
281func (m *Model) Reset() {
282 m.value = nil
283 m.SetCursor(0)
284}
285
286// SetSuggestions sets the suggestions for the input.
287func (m *Model) SetSuggestions(suggestions []string) {
288 m.suggestions = make([][]rune, len(suggestions))
289 for i, s := range suggestions {
290 m.suggestions[i] = []rune(s)
291 }
292
293 m.updateSuggestions()
294}
295
296// rsan initializes or retrieves the rune sanitizer.
297func (m *Model) san() runeutil.Sanitizer {
298 if m.rsan == nil {
299 // Textinput has all its input on a single line so collapse
300 // newlines/tabs to single spaces.
301 m.rsan = runeutil.NewSanitizer(
302 runeutil.ReplaceTabs(" "), runeutil.ReplaceNewlines(" "))
303 }
304 return m.rsan
305}
306
307func (m *Model) insertRunesFromUserInput(v []rune) {
308 // Clean up any special characters in the input provided by the
309 // clipboard. This avoids bugs due to e.g. tab characters and
310 // whatnot.
311 paste := m.san().Sanitize(v)
312
313 var availSpace int
314 if m.CharLimit > 0 {
315 availSpace = m.CharLimit - len(m.value)
316
317 // If the char limit's been reached, cancel.
318 if availSpace <= 0 {
319 return
320 }
321
322 // If there's not enough space to paste the whole thing cut the pasted
323 // runes down so they'll fit.
324 if availSpace < len(paste) {
325 paste = paste[:availSpace]
326 }
327 }
328
329 // Stuff before and after the cursor
330 head := m.value[:m.pos]
331 tailSrc := m.value[m.pos:]
332 tail := make([]rune, len(tailSrc))
333 copy(tail, tailSrc)
334
335 // Insert pasted runes
336 for _, r := range paste {
337 head = append(head, r)
338 m.pos++
339 if m.CharLimit > 0 {
340 availSpace--
341 if availSpace <= 0 {
342 break
343 }
344 }
345 }
346
347 // Put it all back together
348 value := append(head, tail...)
349 inputErr := m.validate(value)
350 m.setValueInternal(value, inputErr)
351}
352
353// If a max width is defined, perform some logic to treat the visible area
354// as a horizontally scrolling viewport.
355func (m *Model) handleOverflow() {
356 if m.Width() <= 0 || uniseg.StringWidth(string(m.value)) <= m.Width() {
357 m.offset = 0
358 m.offsetRight = len(m.value)
359 return
360 }
361
362 // Correct right offset if we've deleted characters
363 m.offsetRight = min(m.offsetRight, len(m.value))
364
365 if m.pos < m.offset {
366 m.offset = m.pos
367
368 w := 0
369 i := 0
370 runes := m.value[m.offset:]
371
372 for i < len(runes) && w <= m.Width() {
373 w += rw.RuneWidth(runes[i])
374 if w <= m.Width()+1 {
375 i++
376 }
377 }
378
379 m.offsetRight = m.offset + i
380 } else if m.pos >= m.offsetRight {
381 m.offsetRight = m.pos
382
383 w := 0
384 runes := m.value[:m.offsetRight]
385 i := len(runes) - 1
386
387 for i > 0 && w < m.Width() {
388 w += rw.RuneWidth(runes[i])
389 if w <= m.Width() {
390 i--
391 }
392 }
393
394 m.offset = m.offsetRight - (len(runes) - 1 - i)
395 }
396}
397
398// deleteBeforeCursor deletes all text before the cursor.
399func (m *Model) deleteBeforeCursor() {
400 m.value = m.value[m.pos:]
401 m.Err = m.validate(m.value)
402 m.offset = 0
403 m.SetCursor(0)
404}
405
406// deleteAfterCursor deletes all text after the cursor. If input is masked
407// delete everything after the cursor so as not to reveal word breaks in the
408// masked input.
409func (m *Model) deleteAfterCursor() {
410 m.value = m.value[:m.pos]
411 m.Err = m.validate(m.value)
412 m.SetCursor(len(m.value))
413}
414
415// deleteWordBackward deletes the word left to the cursor.
416func (m *Model) deleteWordBackward() {
417 if m.pos == 0 || len(m.value) == 0 {
418 return
419 }
420
421 if m.EchoMode != EchoNormal {
422 m.deleteBeforeCursor()
423 return
424 }
425
426 // Linter note: it's critical that we acquire the initial cursor position
427 // here prior to altering it via SetCursor() below. As such, moving this
428 // call into the corresponding if clause does not apply here.
429 oldPos := m.pos
430
431 m.SetCursor(m.pos - 1)
432 for unicode.IsSpace(m.value[m.pos]) {
433 if m.pos <= 0 {
434 break
435 }
436 // ignore series of whitespace before cursor
437 m.SetCursor(m.pos - 1)
438 }
439
440 for m.pos > 0 {
441 if !unicode.IsSpace(m.value[m.pos]) {
442 m.SetCursor(m.pos - 1)
443 } else {
444 if m.pos > 0 {
445 // keep the previous space
446 m.SetCursor(m.pos + 1)
447 }
448 break
449 }
450 }
451
452 if oldPos > len(m.value) {
453 m.value = m.value[:m.pos]
454 } else {
455 m.value = append(m.value[:m.pos], m.value[oldPos:]...)
456 }
457 m.Err = m.validate(m.value)
458}
459
460// deleteWordForward deletes the word right to the cursor. If input is masked
461// delete everything after the cursor so as not to reveal word breaks in the
462// masked input.
463func (m *Model) deleteWordForward() {
464 if m.pos >= len(m.value) || len(m.value) == 0 {
465 return
466 }
467
468 if m.EchoMode != EchoNormal {
469 m.deleteAfterCursor()
470 return
471 }
472
473 oldPos := m.pos
474 m.SetCursor(m.pos + 1)
475 for unicode.IsSpace(m.value[m.pos]) {
476 // ignore series of whitespace after cursor
477 m.SetCursor(m.pos + 1)
478
479 if m.pos >= len(m.value) {
480 break
481 }
482 }
483
484 for m.pos < len(m.value) {
485 if !unicode.IsSpace(m.value[m.pos]) {
486 m.SetCursor(m.pos + 1)
487 } else {
488 break
489 }
490 }
491
492 if m.pos > len(m.value) {
493 m.value = m.value[:oldPos]
494 } else {
495 m.value = append(m.value[:oldPos], m.value[m.pos:]...)
496 }
497 m.Err = m.validate(m.value)
498
499 m.SetCursor(oldPos)
500}
501
502// wordBackward moves the cursor one word to the left. If input is masked, move
503// input to the start so as not to reveal word breaks in the masked input.
504func (m *Model) wordBackward() {
505 if m.pos == 0 || len(m.value) == 0 {
506 return
507 }
508
509 if m.EchoMode != EchoNormal {
510 m.CursorStart()
511 return
512 }
513
514 i := m.pos - 1
515 for i >= 0 {
516 if unicode.IsSpace(m.value[i]) {
517 m.SetCursor(m.pos - 1)
518 i--
519 } else {
520 break
521 }
522 }
523
524 for i >= 0 {
525 if !unicode.IsSpace(m.value[i]) {
526 m.SetCursor(m.pos - 1)
527 i--
528 } else {
529 break
530 }
531 }
532}
533
534// wordForward moves the cursor one word to the right. If the input is masked,
535// move input to the end so as not to reveal word breaks in the masked input.
536func (m *Model) wordForward() {
537 if m.pos >= len(m.value) || len(m.value) == 0 {
538 return
539 }
540
541 if m.EchoMode != EchoNormal {
542 m.CursorEnd()
543 return
544 }
545
546 i := m.pos
547 for i < len(m.value) {
548 if unicode.IsSpace(m.value[i]) {
549 m.SetCursor(m.pos + 1)
550 i++
551 } else {
552 break
553 }
554 }
555
556 for i < len(m.value) {
557 if !unicode.IsSpace(m.value[i]) {
558 m.SetCursor(m.pos + 1)
559 i++
560 } else {
561 break
562 }
563 }
564}
565
566func (m Model) echoTransform(v string) string {
567 switch m.EchoMode {
568 case EchoPassword:
569 return strings.Repeat(string(m.EchoCharacter), uniseg.StringWidth(v))
570 case EchoNone:
571 return ""
572 case EchoNormal:
573 return v
574 default:
575 return v
576 }
577}
578
579// Update is the Bubble Tea update loop.
580func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
581 if !m.focus {
582 return m, nil
583 }
584
585 // Need to check for completion before, because key is configurable and might be double assigned
586 keyMsg, ok := msg.(tea.KeyPressMsg)
587 if ok && key.Matches(keyMsg, m.KeyMap.AcceptSuggestion) {
588 if m.canAcceptSuggestion() {
589 m.value = append(m.value, m.matchedSuggestions[m.currentSuggestionIndex][len(m.value):]...)
590 m.CursorEnd()
591 }
592 }
593
594 // Let's remember where the position of the cursor currently is so that if
595 // the cursor position changes, we can reset the blink.
596 oldPos := m.pos
597
598 switch msg := msg.(type) {
599 case tea.KeyPressMsg:
600 switch {
601 case key.Matches(msg, m.KeyMap.DeleteWordBackward):
602 m.deleteWordBackward()
603 case key.Matches(msg, m.KeyMap.DeleteCharacterBackward):
604 m.Err = nil
605 if len(m.value) > 0 {
606 m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
607 m.Err = m.validate(m.value)
608 if m.pos > 0 {
609 m.SetCursor(m.pos - 1)
610 }
611 }
612 case key.Matches(msg, m.KeyMap.WordBackward):
613 m.wordBackward()
614 case key.Matches(msg, m.KeyMap.CharacterBackward):
615 if m.pos > 0 {
616 m.SetCursor(m.pos - 1)
617 }
618 case key.Matches(msg, m.KeyMap.WordForward):
619 m.wordForward()
620 case key.Matches(msg, m.KeyMap.CharacterForward):
621 if m.pos < len(m.value) {
622 m.SetCursor(m.pos + 1)
623 }
624 case key.Matches(msg, m.KeyMap.LineStart):
625 m.CursorStart()
626 case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
627 if len(m.value) > 0 && m.pos < len(m.value) {
628 m.value = slices.Delete(m.value, m.pos, m.pos+1)
629 m.Err = m.validate(m.value)
630 }
631 case key.Matches(msg, m.KeyMap.LineEnd):
632 m.CursorEnd()
633 case key.Matches(msg, m.KeyMap.DeleteAfterCursor):
634 m.deleteAfterCursor()
635 case key.Matches(msg, m.KeyMap.DeleteBeforeCursor):
636 m.deleteBeforeCursor()
637 case key.Matches(msg, m.KeyMap.Paste):
638 return m, Paste
639 case key.Matches(msg, m.KeyMap.DeleteWordForward):
640 m.deleteWordForward()
641 case key.Matches(msg, m.KeyMap.NextSuggestion):
642 m.nextSuggestion()
643 case key.Matches(msg, m.KeyMap.PrevSuggestion):
644 m.previousSuggestion()
645 default:
646 // Input one or more regular characters.
647 m.insertRunesFromUserInput([]rune(msg.Text))
648 }
649
650 // Check again if can be completed
651 // because value might be something that does not match the completion prefix
652 m.updateSuggestions()
653
654 case tea.PasteMsg:
655 m.insertRunesFromUserInput([]rune(msg))
656
657 case pasteMsg:
658 m.insertRunesFromUserInput([]rune(msg))
659
660 case pasteErrMsg:
661 m.Err = msg
662 }
663
664 var cmds []tea.Cmd
665 var cmd tea.Cmd
666
667 if m.useVirtualCursor {
668 m.virtualCursor, cmd = m.virtualCursor.Update(msg)
669 cmds = append(cmds, cmd)
670
671 // If the cursor position changed, reset the blink state. This is a
672 // small UX nuance that makes cursor movement obvious and feel snappy.
673 if oldPos != m.pos && m.virtualCursor.Mode() == cursor.CursorBlink {
674 m.virtualCursor.IsBlinked = false
675 cmds = append(cmds, m.virtualCursor.Blink())
676 }
677 }
678
679 m.handleOverflow()
680 return m, tea.Batch(cmds...)
681}
682
683// View renders the textinput in its current state.
684func (m Model) View() string {
685 // Placeholder text
686 if len(m.value) == 0 && m.Placeholder != "" {
687 return m.placeholderView()
688 }
689
690 styles := m.activeStyle()
691
692 styleText := styles.Text.Inline(true).Render
693
694 value := m.value[m.offset:m.offsetRight]
695 pos := max(0, m.pos-m.offset)
696 v := styleText(m.echoTransform(string(value[:pos])))
697
698 if pos < len(value) { //nolint:nestif
699 char := m.echoTransform(string(value[pos]))
700 m.virtualCursor.SetChar(char)
701 v += m.virtualCursor.View() // cursor and text under it
702 v += styleText(m.echoTransform(string(value[pos+1:]))) // text after cursor
703 v += m.completionView(0) // suggested completion
704 } else {
705 if m.focus && m.canAcceptSuggestion() {
706 suggestion := m.matchedSuggestions[m.currentSuggestionIndex]
707 if len(value) < len(suggestion) {
708 m.virtualCursor.TextStyle = styles.Suggestion
709 m.virtualCursor.SetChar(m.echoTransform(string(suggestion[pos])))
710 v += m.virtualCursor.View()
711 v += m.completionView(1)
712 } else {
713 m.virtualCursor.SetChar(" ")
714 v += m.virtualCursor.View()
715 }
716 } else {
717 m.virtualCursor.SetChar(" ")
718 v += m.virtualCursor.View()
719 }
720 }
721
722 // If a max width and background color were set fill the empty spaces with
723 // the background color.
724 valWidth := uniseg.StringWidth(string(value))
725 if m.Width() > 0 && valWidth <= m.Width() {
726 padding := max(0, m.Width()-valWidth)
727 if valWidth+padding <= m.Width() && pos < len(value) {
728 padding++
729 }
730 v += styleText(strings.Repeat(" ", padding))
731 }
732
733 return m.promptView() + v
734}
735
736func (m Model) promptView() string {
737 return m.activeStyle().Prompt.Render(m.Prompt)
738}
739
740// placeholderView returns the prompt and placeholder view, if any.
741func (m Model) placeholderView() string {
742 var (
743 v string
744 styles = m.activeStyle()
745 render = styles.Placeholder.Render
746 )
747
748 p := make([]rune, m.Width()+1)
749 copy(p, []rune(m.Placeholder))
750
751 m.virtualCursor.TextStyle = styles.Placeholder
752 m.virtualCursor.SetChar(string(p[:1]))
753 v += m.virtualCursor.View()
754
755 // If the entire placeholder is already set and no padding is needed, finish
756 if m.Width() < 1 && len(p) <= 1 {
757 return styles.Prompt.Render(m.Prompt) + v
758 }
759
760 // If Width is set then size placeholder accordingly
761 if m.Width() > 0 {
762 // available width is width - len + cursor offset of 1
763 minWidth := lipgloss.Width(m.Placeholder)
764 availWidth := m.Width() - minWidth + 1
765
766 // if width < len, 'subtract'(add) number to len and dont add padding
767 if availWidth < 0 {
768 minWidth += availWidth
769 availWidth = 0
770 }
771 // append placeholder[len] - cursor, append padding
772 v += render(string(p[1:minWidth]))
773 v += render(strings.Repeat(" ", availWidth))
774 } else {
775 // if there is no width, the placeholder can be any length
776 v += render(string(p[1:]))
777 }
778
779 return styles.Prompt.Render(m.Prompt) + v
780}
781
782// Blink is a command used to initialize cursor blinking.
783func Blink() tea.Msg {
784 return cursor.Blink()
785}
786
787// Paste is a command for pasting from the clipboard into the text input.
788func Paste() tea.Msg {
789 str, err := clipboard.ReadAll()
790 if err != nil {
791 return pasteErrMsg{err}
792 }
793 return pasteMsg(str)
794}
795
796func clamp(v, low, high int) int {
797 if high < low {
798 low, high = high, low
799 }
800 return min(high, max(low, v))
801}
802
803func (m Model) completionView(offset int) string {
804 if !m.canAcceptSuggestion() {
805 return ""
806 }
807 value := m.value
808 suggestion := m.matchedSuggestions[m.currentSuggestionIndex]
809 if len(value) < len(suggestion) {
810 return m.activeStyle().Suggestion.Inline(true).
811 Render(string(suggestion[len(value)+offset:]))
812 }
813 return ""
814}
815
816func (m *Model) getSuggestions(sugs [][]rune) []string {
817 suggestions := make([]string, len(sugs))
818 for i, s := range sugs {
819 suggestions[i] = string(s)
820 }
821 return suggestions
822}
823
824// AvailableSuggestions returns the list of available suggestions.
825func (m *Model) AvailableSuggestions() []string {
826 return m.getSuggestions(m.suggestions)
827}
828
829// MatchedSuggestions returns the list of matched suggestions.
830func (m *Model) MatchedSuggestions() []string {
831 return m.getSuggestions(m.matchedSuggestions)
832}
833
834// CurrentSuggestionIndex returns the currently selected suggestion index.
835func (m *Model) CurrentSuggestionIndex() int {
836 return m.currentSuggestionIndex
837}
838
839// CurrentSuggestion returns the currently selected suggestion.
840func (m *Model) CurrentSuggestion() string {
841 if m.currentSuggestionIndex >= len(m.matchedSuggestions) {
842 return ""
843 }
844
845 return string(m.matchedSuggestions[m.currentSuggestionIndex])
846}
847
848// canAcceptSuggestion returns whether there is an acceptable suggestion to
849// autocomplete the current value.
850func (m *Model) canAcceptSuggestion() bool {
851 return len(m.matchedSuggestions) > 0
852}
853
854// updateSuggestions refreshes the list of matching suggestions.
855func (m *Model) updateSuggestions() {
856 if !m.ShowSuggestions {
857 return
858 }
859
860 if len(m.value) <= 0 || len(m.suggestions) <= 0 {
861 m.matchedSuggestions = [][]rune{}
862 return
863 }
864
865 matches := [][]rune{}
866 for _, s := range m.suggestions {
867 suggestion := string(s)
868
869 if strings.HasPrefix(strings.ToLower(suggestion), strings.ToLower(string(m.value))) {
870 matches = append(matches, []rune(suggestion))
871 }
872 }
873 if !reflect.DeepEqual(matches, m.matchedSuggestions) {
874 m.currentSuggestionIndex = 0
875 }
876
877 m.matchedSuggestions = matches
878}
879
880// nextSuggestion selects the next suggestion.
881func (m *Model) nextSuggestion() {
882 m.currentSuggestionIndex = (m.currentSuggestionIndex + 1)
883 if m.currentSuggestionIndex >= len(m.matchedSuggestions) {
884 m.currentSuggestionIndex = 0
885 }
886}
887
888// previousSuggestion selects the previous suggestion.
889func (m *Model) previousSuggestion() {
890 m.currentSuggestionIndex = (m.currentSuggestionIndex - 1)
891 if m.currentSuggestionIndex < 0 {
892 m.currentSuggestionIndex = len(m.matchedSuggestions) - 1
893 }
894}
895
896func (m Model) validate(v []rune) error {
897 if m.Validate != nil {
898 return m.Validate(string(v))
899 }
900 return nil
901}
902
903// Cursor returns a [tea.Cursor] for rendering a real cursor in a Bubble Tea
904// program. This requires that [Model.VirtualCursor] is set to false.
905//
906// Note that you will almost certainly also need to adjust the offset cursor
907// position per the textarea's per the textarea's position in the terminal.
908//
909// Example:
910//
911// // In your top-level View function:
912// f := tea.NewFrame(m.textarea.View())
913// f.Cursor = m.textarea.Cursor()
914// f.Cursor.Position.X += offsetX
915// f.Cursor.Position.Y += offsetY
916func (m Model) Cursor() *tea.Cursor {
917 if m.useVirtualCursor || !m.Focused() {
918 return nil
919 }
920
921 w := lipgloss.Width
922
923 promptWidth := w(m.promptView())
924 xOffset := m.Position() +
925 promptWidth
926 if m.width > 0 {
927 xOffset = min(xOffset, m.width+promptWidth)
928 }
929
930 style := m.styles.Cursor
931 c := tea.NewCursor(xOffset, 0)
932 c.Blink = style.Blink
933 c.Color = style.Color
934 c.Shape = style.Shape
935 return c
936}
937
938// updateVirtualCursorStyle sets styling on the virtual cursor based on the
939// textarea's style settings.
940func (m *Model) updateVirtualCursorStyle() {
941 if !m.useVirtualCursor {
942 // Hide the virtual cursor if we're using a real cursor.
943 m.virtualCursor.SetMode(cursor.CursorHide)
944 return
945 }
946
947 m.virtualCursor.Style = lipgloss.NewStyle().Foreground(m.styles.Cursor.Color)
948
949 // By default, the blink speed of the cursor is set to a default
950 // internally.
951 if m.styles.Cursor.Blink {
952 if m.styles.Cursor.BlinkSpeed > 0 {
953 m.virtualCursor.BlinkSpeed = m.styles.Cursor.BlinkSpeed
954 }
955 m.virtualCursor.SetMode(cursor.CursorBlink)
956 return
957 }
958 m.virtualCursor.SetMode(cursor.CursorStatic)
959}
960
961// activeStyle returns the appropriate set of styles to use depending on
962// whether the textarea is focused or blurred.
963func (m Model) activeStyle() *StyleState {
964 if m.focus {
965 return &m.styles.Focused
966 }
967 return &m.styles.Blurred
968}