1package tview
2
3import (
4	"math"
5	"regexp"
6	"strings"
7	"sync"
8	"unicode/utf8"
9
10	"github.com/gdamore/tcell"
11)
12
13// InputField is a one-line box (three lines if there is a title) where the
14// user can enter text. Use SetAcceptanceFunc() to accept or reject input,
15// SetChangedFunc() to listen for changes, and SetMaskCharacter() to hide input
16// from onlookers (e.g. for password input).
17//
18// The following keys can be used for navigation and editing:
19//
20//   - Left arrow: Move left by one character.
21//   - Right arrow: Move right by one character.
22//   - Home, Ctrl-A, Alt-a: Move to the beginning of the line.
23//   - End, Ctrl-E, Alt-e: Move to the end of the line.
24//   - Alt-left, Alt-b: Move left by one word.
25//   - Alt-right, Alt-f: Move right by one word.
26//   - Backspace: Delete the character before the cursor.
27//   - Delete: Delete the character after the cursor.
28//   - Ctrl-K: Delete from the cursor to the end of the line.
29//   - Ctrl-W: Delete the last word before the cursor.
30//   - Ctrl-U: Delete the entire line.
31//
32// See https://github.com/rivo/tview/wiki/InputField for an example.
33type InputField struct {
34	*Box
35
36	// The text that was entered.
37	text string
38
39	// The text to be displayed before the input area.
40	label string
41
42	// The text to be displayed in the input area when "text" is empty.
43	placeholder string
44
45	// The label color.
46	labelColor tcell.Color
47
48	// The background color of the input area.
49	fieldBackgroundColor tcell.Color
50
51	// The text color of the input area.
52	fieldTextColor tcell.Color
53
54	// The text color of the placeholder.
55	placeholderTextColor tcell.Color
56
57	// The screen width of the label area. A value of 0 means use the width of
58	// the label text.
59	labelWidth int
60
61	// The screen width of the input area. A value of 0 means extend as much as
62	// possible.
63	fieldWidth int
64
65	// A character to mask entered text (useful for password fields). A value of 0
66	// disables masking.
67	maskCharacter rune
68
69	// The cursor position as a byte index into the text string.
70	cursorPos int
71
72	// The number of bytes of the text string skipped ahead while drawing.
73	offset int
74
75	// An optional autocomplete function which receives the current text of the
76	// input field and returns a slice of strings to be displayed in a drop-down
77	// selection.
78	autocomplete func(text string) []string
79
80	// The List object which shows the selectable autocomplete entries. If not
81	// nil, the list's main texts represent the current autocomplete entries.
82	autocompleteList      *List
83	autocompleteListMutex sync.Mutex
84
85	// An optional function which may reject the last character that was entered.
86	accept func(text string, ch rune) bool
87
88	// An optional function which is called when the input has changed.
89	changed func(text string)
90
91	// An optional function which is called when the user indicated that they
92	// are done entering text. The key which was pressed is provided (tab,
93	// shift-tab, enter, or escape).
94	done func(tcell.Key)
95
96	// A callback function set by the Form class and called when the user leaves
97	// this form item.
98	finished func(tcell.Key)
99}
100
101// NewInputField returns a new input field.
102func NewInputField() *InputField {
103	return &InputField{
104		Box:                  NewBox(),
105		labelColor:           Styles.SecondaryTextColor,
106		fieldBackgroundColor: Styles.ContrastBackgroundColor,
107		fieldTextColor:       Styles.PrimaryTextColor,
108		placeholderTextColor: Styles.ContrastSecondaryTextColor,
109	}
110}
111
112// SetText sets the current text of the input field.
113func (i *InputField) SetText(text string) *InputField {
114	i.text = text
115	i.cursorPos = len(text)
116	if i.changed != nil {
117		i.changed(text)
118	}
119	return i
120}
121
122// GetText returns the current text of the input field.
123func (i *InputField) GetText() string {
124	return i.text
125}
126
127// SetLabel sets the text to be displayed before the input area.
128func (i *InputField) SetLabel(label string) *InputField {
129	i.label = label
130	return i
131}
132
133// GetLabel returns the text to be displayed before the input area.
134func (i *InputField) GetLabel() string {
135	return i.label
136}
137
138// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
139// primitive to use the width of the label string.
140func (i *InputField) SetLabelWidth(width int) *InputField {
141	i.labelWidth = width
142	return i
143}
144
145// SetPlaceholder sets the text to be displayed when the input text is empty.
146func (i *InputField) SetPlaceholder(text string) *InputField {
147	i.placeholder = text
148	return i
149}
150
151// SetLabelColor sets the color of the label.
152func (i *InputField) SetLabelColor(color tcell.Color) *InputField {
153	i.labelColor = color
154	return i
155}
156
157// SetFieldBackgroundColor sets the background color of the input area.
158func (i *InputField) SetFieldBackgroundColor(color tcell.Color) *InputField {
159	i.fieldBackgroundColor = color
160	return i
161}
162
163// SetFieldTextColor sets the text color of the input area.
164func (i *InputField) SetFieldTextColor(color tcell.Color) *InputField {
165	i.fieldTextColor = color
166	return i
167}
168
169// SetPlaceholderTextColor sets the text color of placeholder text.
170func (i *InputField) SetPlaceholderTextColor(color tcell.Color) *InputField {
171	i.placeholderTextColor = color
172	return i
173}
174
175// SetFormAttributes sets attributes shared by all form items.
176func (i *InputField) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
177	i.labelWidth = labelWidth
178	i.labelColor = labelColor
179	i.backgroundColor = bgColor
180	i.fieldTextColor = fieldTextColor
181	i.fieldBackgroundColor = fieldBgColor
182	return i
183}
184
185// SetFieldWidth sets the screen width of the input area. A value of 0 means
186// extend as much as possible.
187func (i *InputField) SetFieldWidth(width int) *InputField {
188	i.fieldWidth = width
189	return i
190}
191
192// GetFieldWidth returns this primitive's field width.
193func (i *InputField) GetFieldWidth() int {
194	return i.fieldWidth
195}
196
197// SetMaskCharacter sets a character that masks user input on a screen. A value
198// of 0 disables masking.
199func (i *InputField) SetMaskCharacter(mask rune) *InputField {
200	i.maskCharacter = mask
201	return i
202}
203
204// SetAutocompleteFunc sets an autocomplete callback function which may return
205// strings to be selected from a drop-down based on the current text of the
206// input field. The drop-down appears only if len(entries) > 0. The callback is
207// invoked in this function and whenever the current text changes or when
208// Autocomplete() is called. Entries are cleared when the user selects an entry
209// or presses Escape.
210func (i *InputField) SetAutocompleteFunc(callback func(currentText string) (entries []string)) *InputField {
211	i.autocomplete = callback
212	i.Autocomplete()
213	return i
214}
215
216// Autocomplete invokes the autocomplete callback (if there is one). If the
217// length of the returned autocomplete entries slice is greater than 0, the
218// input field will present the user with a corresponding drop-down list the
219// next time the input field is drawn.
220//
221// It is safe to call this function from any goroutine. Note that the input
222// field is not redrawn automatically unless called from the main goroutine
223// (e.g. in response to events).
224func (i *InputField) Autocomplete() *InputField {
225	i.autocompleteListMutex.Lock()
226	defer i.autocompleteListMutex.Unlock()
227	if i.autocomplete == nil {
228		return i
229	}
230
231	// Do we have any autocomplete entries?
232	entries := i.autocomplete(i.text)
233	if len(entries) == 0 {
234		// No entries, no list.
235		i.autocompleteList = nil
236		return i
237	}
238
239	// Make a list if we have none.
240	if i.autocompleteList == nil {
241		i.autocompleteList = NewList()
242		i.autocompleteList.ShowSecondaryText(false).
243			SetMainTextColor(Styles.PrimitiveBackgroundColor).
244			SetSelectedTextColor(Styles.PrimitiveBackgroundColor).
245			SetSelectedBackgroundColor(Styles.PrimaryTextColor).
246			SetHighlightFullLine(true).
247			SetBackgroundColor(Styles.MoreContrastBackgroundColor)
248	}
249
250	// Fill it with the entries.
251	currentEntry := -1
252	i.autocompleteList.Clear()
253	for index, entry := range entries {
254		i.autocompleteList.AddItem(entry, "", 0, nil)
255		if currentEntry < 0 && entry == i.text {
256			currentEntry = index
257		}
258	}
259
260	// Set the selection if we have one.
261	if currentEntry >= 0 {
262		i.autocompleteList.SetCurrentItem(currentEntry)
263	}
264
265	return i
266}
267
268// SetAcceptanceFunc sets a handler which may reject the last character that was
269// entered (by returning false).
270//
271// This package defines a number of variables prefixed with InputField which may
272// be used for common input (e.g. numbers, maximum text length).
273func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *InputField {
274	i.accept = handler
275	return i
276}
277
278// SetChangedFunc sets a handler which is called whenever the text of the input
279// field has changed. It receives the current text (after the change).
280func (i *InputField) SetChangedFunc(handler func(text string)) *InputField {
281	i.changed = handler
282	return i
283}
284
285// SetDoneFunc sets a handler which is called when the user is done entering
286// text. The callback function is provided with the key that was pressed, which
287// is one of the following:
288//
289//   - KeyEnter: Done entering text.
290//   - KeyEscape: Abort text input.
291//   - KeyTab: Move to the next field.
292//   - KeyBacktab: Move to the previous field.
293func (i *InputField) SetDoneFunc(handler func(key tcell.Key)) *InputField {
294	i.done = handler
295	return i
296}
297
298// SetFinishedFunc sets a callback invoked when the user leaves this form item.
299func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
300	i.finished = handler
301	return i
302}
303
304// Draw draws this primitive onto the screen.
305func (i *InputField) Draw(screen tcell.Screen) {
306	i.Box.Draw(screen)
307
308	// Prepare
309	x, y, width, height := i.GetInnerRect()
310	rightLimit := x + width
311	if height < 1 || rightLimit <= x {
312		return
313	}
314
315	// Draw label.
316	if i.labelWidth > 0 {
317		labelWidth := i.labelWidth
318		if labelWidth > rightLimit-x {
319			labelWidth = rightLimit - x
320		}
321		Print(screen, i.label, x, y, labelWidth, AlignLeft, i.labelColor)
322		x += labelWidth
323	} else {
324		_, drawnWidth := Print(screen, i.label, x, y, rightLimit-x, AlignLeft, i.labelColor)
325		x += drawnWidth
326	}
327
328	// Draw input area.
329	fieldWidth := i.fieldWidth
330	if fieldWidth == 0 {
331		fieldWidth = math.MaxInt32
332	}
333	if rightLimit-x < fieldWidth {
334		fieldWidth = rightLimit - x
335	}
336	fieldStyle := tcell.StyleDefault.Background(i.fieldBackgroundColor)
337	for index := 0; index < fieldWidth; index++ {
338		screen.SetContent(x+index, y, ' ', nil, fieldStyle)
339	}
340
341	// Text.
342	var cursorScreenPos int
343	text := i.text
344	if text == "" && i.placeholder != "" {
345		// Draw placeholder text.
346		Print(screen, Escape(i.placeholder), x, y, fieldWidth, AlignLeft, i.placeholderTextColor)
347		i.offset = 0
348	} else {
349		// Draw entered text.
350		if i.maskCharacter > 0 {
351			text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text))
352		}
353		if fieldWidth >= stringWidth(text) {
354			// We have enough space for the full text.
355			Print(screen, Escape(text), x, y, fieldWidth, AlignLeft, i.fieldTextColor)
356			i.offset = 0
357			iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
358				if textPos >= i.cursorPos {
359					return true
360				}
361				cursorScreenPos += screenWidth
362				return false
363			})
364		} else {
365			// The text doesn't fit. Where is the cursor?
366			if i.cursorPos < 0 {
367				i.cursorPos = 0
368			} else if i.cursorPos > len(text) {
369				i.cursorPos = len(text)
370			}
371			// Shift the text so the cursor is inside the field.
372			var shiftLeft int
373			if i.offset > i.cursorPos {
374				i.offset = i.cursorPos
375			} else if subWidth := stringWidth(text[i.offset:i.cursorPos]); subWidth > fieldWidth-1 {
376				shiftLeft = subWidth - fieldWidth + 1
377			}
378			currentOffset := i.offset
379			iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
380				if textPos >= currentOffset {
381					if shiftLeft > 0 {
382						i.offset = textPos + textWidth
383						shiftLeft -= screenWidth
384					} else {
385						if textPos+textWidth > i.cursorPos {
386							return true
387						}
388						cursorScreenPos += screenWidth
389					}
390				}
391				return false
392			})
393			Print(screen, Escape(text[i.offset:]), x, y, fieldWidth, AlignLeft, i.fieldTextColor)
394		}
395	}
396
397	// Draw autocomplete list.
398	i.autocompleteListMutex.Lock()
399	defer i.autocompleteListMutex.Unlock()
400	if i.autocompleteList != nil {
401		// How much space do we need?
402		lheight := i.autocompleteList.GetItemCount()
403		lwidth := 0
404		for index := 0; index < lheight; index++ {
405			entry, _ := i.autocompleteList.GetItemText(index)
406			width := TaggedStringWidth(entry)
407			if width > lwidth {
408				lwidth = width
409			}
410		}
411
412		// We prefer to drop down but if there is no space, maybe drop up?
413		lx := x
414		ly := y + 1
415		_, sheight := screen.Size()
416		if ly+lheight >= sheight && ly-2 > lheight-ly {
417			ly = y - lheight
418			if ly < 0 {
419				ly = 0
420			}
421		}
422		if ly+lheight >= sheight {
423			lheight = sheight - ly
424		}
425		i.autocompleteList.SetRect(lx, ly, lwidth, lheight)
426		i.autocompleteList.Draw(screen)
427	}
428
429	// Set cursor.
430	if i.focus.HasFocus() {
431		screen.ShowCursor(x+cursorScreenPos, y)
432	}
433}
434
435// InputHandler returns the handler for this primitive.
436func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
437	return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
438		// Trigger changed events.
439		currentText := i.text
440		defer func() {
441			if i.text != currentText {
442				i.Autocomplete()
443				if i.changed != nil {
444					i.changed(i.text)
445				}
446			}
447		}()
448
449		// Movement functions.
450		home := func() { i.cursorPos = 0 }
451		end := func() { i.cursorPos = len(i.text) }
452		moveLeft := func() {
453			iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
454				i.cursorPos -= textWidth
455				return true
456			})
457		}
458		moveRight := func() {
459			iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
460				i.cursorPos += textWidth
461				return true
462			})
463		}
464		moveWordLeft := func() {
465			i.cursorPos = len(regexp.MustCompile(`\S+\s*$`).ReplaceAllString(i.text[:i.cursorPos], ""))
466		}
467		moveWordRight := func() {
468			i.cursorPos = len(i.text) - len(regexp.MustCompile(`^\s*\S+\s*`).ReplaceAllString(i.text[i.cursorPos:], ""))
469		}
470
471		// Add character function. Returns whether or not the rune character is
472		// accepted.
473		add := func(r rune) bool {
474			newText := i.text[:i.cursorPos] + string(r) + i.text[i.cursorPos:]
475			if i.accept != nil && !i.accept(newText, r) {
476				return false
477			}
478			i.text = newText
479			i.cursorPos += len(string(r))
480			return true
481		}
482
483		// Finish up.
484		finish := func(key tcell.Key) {
485			if i.done != nil {
486				i.done(key)
487			}
488			if i.finished != nil {
489				i.finished(key)
490			}
491		}
492
493		// Process key event.
494		i.autocompleteListMutex.Lock()
495		defer i.autocompleteListMutex.Unlock()
496		switch key := event.Key(); key {
497		case tcell.KeyRune: // Regular character.
498			if event.Modifiers()&tcell.ModAlt > 0 {
499				// We accept some Alt- key combinations.
500				switch event.Rune() {
501				case 'a': // Home.
502					home()
503				case 'e': // End.
504					end()
505				case 'b': // Move word left.
506					moveWordLeft()
507				case 'f': // Move word right.
508					moveWordRight()
509				default:
510					if !add(event.Rune()) {
511						return
512					}
513				}
514			} else {
515				// Other keys are simply accepted as regular characters.
516				if !add(event.Rune()) {
517					return
518				}
519			}
520		case tcell.KeyCtrlU: // Delete all.
521			i.text = ""
522			i.cursorPos = 0
523		case tcell.KeyCtrlK: // Delete until the end of the line.
524			i.text = i.text[:i.cursorPos]
525		case tcell.KeyCtrlW: // Delete last word.
526			lastWord := regexp.MustCompile(`\S+\s*$`)
527			newText := lastWord.ReplaceAllString(i.text[:i.cursorPos], "") + i.text[i.cursorPos:]
528			i.cursorPos -= len(i.text) - len(newText)
529			i.text = newText
530		case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete character before the cursor.
531			iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
532				i.text = i.text[:textPos] + i.text[textPos+textWidth:]
533				i.cursorPos -= textWidth
534				return true
535			})
536			if i.offset >= i.cursorPos {
537				i.offset = 0
538			}
539		case tcell.KeyDelete: // Delete character after the cursor.
540			iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
541				i.text = i.text[:i.cursorPos] + i.text[i.cursorPos+textWidth:]
542				return true
543			})
544		case tcell.KeyLeft:
545			if event.Modifiers()&tcell.ModAlt > 0 {
546				moveWordLeft()
547			} else {
548				moveLeft()
549			}
550		case tcell.KeyRight:
551			if event.Modifiers()&tcell.ModAlt > 0 {
552				moveWordRight()
553			} else {
554				moveRight()
555			}
556		case tcell.KeyHome, tcell.KeyCtrlA:
557			home()
558		case tcell.KeyEnd, tcell.KeyCtrlE:
559			end()
560		case tcell.KeyEnter, tcell.KeyEscape: // We might be done.
561			if i.autocompleteList != nil {
562				i.autocompleteList = nil
563			} else {
564				finish(key)
565			}
566		case tcell.KeyDown, tcell.KeyTab: // Autocomplete selection.
567			if i.autocompleteList != nil {
568				count := i.autocompleteList.GetItemCount()
569				newEntry := i.autocompleteList.GetCurrentItem() + 1
570				if newEntry >= count {
571					newEntry = 0
572				}
573				i.autocompleteList.SetCurrentItem(newEntry)
574				currentText, _ = i.autocompleteList.GetItemText(newEntry) // Don't trigger changed function twice.
575				i.SetText(currentText)
576			} else {
577				finish(key)
578			}
579		case tcell.KeyUp, tcell.KeyBacktab: // Autocomplete selection.
580			if i.autocompleteList != nil {
581				newEntry := i.autocompleteList.GetCurrentItem() - 1
582				if newEntry < 0 {
583					newEntry = i.autocompleteList.GetItemCount() - 1
584				}
585				i.autocompleteList.SetCurrentItem(newEntry)
586				currentText, _ = i.autocompleteList.GetItemText(newEntry) // Don't trigger changed function twice.
587				i.SetText(currentText)
588			} else {
589				finish(key)
590			}
591		}
592	})
593}
594