textinput.go

  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}