1 package tview
2
3 import (
4 "math"
5 "regexp"
6 "strings"
7 "sync"
8 "unicode/utf8"
9
10 "github.com/gdamore/tcell/v2"
11 "github.com/rivo/uniseg"
12 )
13
14 const (
15 AutocompletedNavigate = iota
16 AutocompletedTab
17 AutocompletedEnter
18 AutocompletedClick
19 )
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49 type InputField struct {
50 *Box
51
52
53 disabled bool
54
55
56 text string
57
58
59 label string
60
61
62 placeholder string
63
64
65 labelStyle tcell.Style
66
67
68 fieldStyle tcell.Style
69
70
71 placeholderStyle tcell.Style
72
73
74
75 labelWidth int
76
77
78
79 fieldWidth int
80
81
82
83 maskCharacter rune
84
85
86 cursorPos int
87
88
89
90
91 autocomplete func(text string) []string
92
93
94
95 autocompleteList *List
96 autocompleteListMutex sync.Mutex
97
98
99 autocompleteStyles struct {
100 main tcell.Style
101 selected tcell.Style
102 background tcell.Color
103 }
104
105
106
107
108
109
110
111 autocompleted func(text string, index int, source int) bool
112
113
114 accept func(text string, ch rune) bool
115
116
117 changed func(text string)
118
119
120
121
122 done func(tcell.Key)
123
124
125
126 finished func(tcell.Key)
127
128 fieldX int
129 offset int
130 }
131
132
133 func NewInputField() *InputField {
134 i := &InputField{
135 Box: NewBox(),
136 labelStyle: tcell.StyleDefault.Foreground(Styles.SecondaryTextColor),
137 fieldStyle: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.PrimaryTextColor),
138 placeholderStyle: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.ContrastSecondaryTextColor),
139 }
140 i.autocompleteStyles.main = tcell.StyleDefault.Foreground(Styles.PrimitiveBackgroundColor)
141 i.autocompleteStyles.selected = tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.PrimitiveBackgroundColor)
142 i.autocompleteStyles.background = Styles.MoreContrastBackgroundColor
143 return i
144 }
145
146
147 func (i *InputField) SetText(text string) *InputField {
148 i.text = text
149 i.cursorPos = len(text)
150 if i.changed != nil {
151 i.changed(text)
152 }
153 return i
154 }
155
156
157 func (i *InputField) GetText() string {
158 return i.text
159 }
160
161
162 func (i *InputField) SetLabel(label string) *InputField {
163 i.label = label
164 return i
165 }
166
167
168 func (i *InputField) GetLabel() string {
169 return i.label
170 }
171
172
173
174 func (i *InputField) SetLabelWidth(width int) *InputField {
175 i.labelWidth = width
176 return i
177 }
178
179
180 func (i *InputField) SetPlaceholder(text string) *InputField {
181 i.placeholder = text
182 return i
183 }
184
185
186 func (i *InputField) SetLabelColor(color tcell.Color) *InputField {
187 i.labelStyle = i.labelStyle.Foreground(color)
188 return i
189 }
190
191
192 func (i *InputField) SetLabelStyle(style tcell.Style) *InputField {
193 i.labelStyle = style
194 return i
195 }
196
197
198 func (i *InputField) GetLabelStyle() tcell.Style {
199 return i.labelStyle
200 }
201
202
203 func (i *InputField) SetFieldBackgroundColor(color tcell.Color) *InputField {
204 i.fieldStyle = i.fieldStyle.Background(color)
205 return i
206 }
207
208
209 func (i *InputField) SetFieldTextColor(color tcell.Color) *InputField {
210 i.fieldStyle = i.fieldStyle.Foreground(color)
211 return i
212 }
213
214
215
216 func (i *InputField) SetFieldStyle(style tcell.Style) *InputField {
217 i.fieldStyle = style
218 return i
219 }
220
221
222
223 func (i *InputField) GetFieldStyle() tcell.Style {
224 return i.fieldStyle
225 }
226
227
228 func (i *InputField) SetPlaceholderTextColor(color tcell.Color) *InputField {
229 i.placeholderStyle = i.placeholderStyle.Foreground(color)
230 return i
231 }
232
233
234
235 func (i *InputField) SetPlaceholderStyle(style tcell.Style) *InputField {
236 i.placeholderStyle = style
237 return i
238 }
239
240
241
242 func (i *InputField) GetPlaceholderStyle() tcell.Style {
243 return i.placeholderStyle
244 }
245
246
247
248
249 func (i *InputField) SetAutocompleteStyles(background tcell.Color, main, selected tcell.Style) *InputField {
250 i.autocompleteStyles.background = background
251 i.autocompleteStyles.main = main
252 i.autocompleteStyles.selected = selected
253 return i
254 }
255
256
257 func (i *InputField) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
258 i.labelWidth = labelWidth
259 i.backgroundColor = bgColor
260 i.SetLabelColor(labelColor).
261 SetFieldTextColor(fieldTextColor).
262 SetFieldBackgroundColor(fieldBgColor)
263 return i
264 }
265
266
267
268 func (i *InputField) SetFieldWidth(width int) *InputField {
269 i.fieldWidth = width
270 return i
271 }
272
273
274 func (i *InputField) GetFieldWidth() int {
275 return i.fieldWidth
276 }
277
278
279 func (i *InputField) GetFieldHeight() int {
280 return 1
281 }
282
283
284 func (i *InputField) SetDisabled(disabled bool) FormItem {
285 i.disabled = disabled
286 if i.finished != nil {
287 i.finished(-1)
288 }
289 return i
290 }
291
292
293
294 func (i *InputField) SetMaskCharacter(mask rune) *InputField {
295 i.maskCharacter = mask
296 return i
297 }
298
299
300
301
302
303
304
305 func (i *InputField) SetAutocompleteFunc(callback func(currentText string) (entries []string)) *InputField {
306 i.autocomplete = callback
307 i.Autocomplete()
308 return i
309 }
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324 func (i *InputField) SetAutocompletedFunc(autocompleted func(text string, index int, source int) bool) *InputField {
325 i.autocompleted = autocompleted
326 return i
327 }
328
329
330
331
332
333
334
335
336
337 func (i *InputField) Autocomplete() *InputField {
338 i.autocompleteListMutex.Lock()
339 defer i.autocompleteListMutex.Unlock()
340 if i.autocomplete == nil {
341 return i
342 }
343
344
345 entries := i.autocomplete(i.text)
346 if len(entries) == 0 {
347
348 i.autocompleteList = nil
349 return i
350 }
351
352
353 if i.autocompleteList == nil {
354 i.autocompleteList = NewList()
355 i.autocompleteList.ShowSecondaryText(false).
356 SetMainTextStyle(i.autocompleteStyles.main).
357 SetSelectedStyle(i.autocompleteStyles.selected).
358 SetHighlightFullLine(true).
359 SetBackgroundColor(i.autocompleteStyles.background)
360 }
361
362
363 currentEntry := -1
364 suffixLength := 9999
365 i.autocompleteList.Clear()
366 for index, entry := range entries {
367 i.autocompleteList.AddItem(entry, "", 0, nil)
368 if strings.HasPrefix(entry, i.text) && len(entry)-len(i.text) < suffixLength {
369 currentEntry = index
370 suffixLength = len(i.text) - len(entry)
371 }
372 }
373
374
375 if currentEntry >= 0 {
376 i.autocompleteList.SetCurrentItem(currentEntry)
377 }
378
379 return i
380 }
381
382
383
384
385
386
387 func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *InputField {
388 i.accept = handler
389 return i
390 }
391
392
393
394 func (i *InputField) SetChangedFunc(handler func(text string)) *InputField {
395 i.changed = handler
396 return i
397 }
398
399
400
401
402
403
404
405
406
407 func (i *InputField) SetDoneFunc(handler func(key tcell.Key)) *InputField {
408 i.done = handler
409 return i
410 }
411
412
413 func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
414 i.finished = handler
415 return i
416 }
417
418
419 func (i *InputField) Focus(delegate func(p Primitive)) {
420
421
422 if i.finished != nil && i.disabled {
423 i.finished(-1)
424 return
425 }
426
427 i.Box.Focus(delegate)
428 }
429
430
431 func (i *InputField) Blur() {
432 i.Box.Blur()
433 i.autocompleteList = nil
434 }
435
436
437 func (i *InputField) Draw(screen tcell.Screen) {
438 i.Box.DrawForSubclass(screen, i)
439
440
441 x, y, width, height := i.GetInnerRect()
442 rightLimit := x + width
443 if height < 1 || rightLimit <= x {
444 return
445 }
446
447
448 _, labelBg, _ := i.labelStyle.Decompose()
449 if i.labelWidth > 0 {
450 labelWidth := i.labelWidth
451 if labelWidth > width {
452 labelWidth = width
453 }
454 printWithStyle(screen, i.label, x, y, 0, labelWidth, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
455 x += labelWidth
456 } else {
457 _, drawnWidth, _, _ := printWithStyle(screen, i.label, x, y, 0, width, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
458 x += drawnWidth
459 }
460
461
462 i.fieldX = x
463 fieldWidth := i.fieldWidth
464 text := i.text
465 inputStyle := i.fieldStyle
466 placeholder := text == "" && i.placeholder != ""
467 if placeholder {
468 inputStyle = i.placeholderStyle
469 }
470 _, inputBg, _ := inputStyle.Decompose()
471 if fieldWidth == 0 {
472 fieldWidth = math.MaxInt32
473 }
474 if rightLimit-x < fieldWidth {
475 fieldWidth = rightLimit - x
476 }
477 if i.disabled {
478 inputStyle = inputStyle.Background(i.backgroundColor)
479 }
480 if inputBg != tcell.ColorDefault {
481 for index := 0; index < fieldWidth; index++ {
482 screen.SetContent(x+index, y, ' ', nil, inputStyle)
483 }
484 }
485
486
487 var cursorScreenPos int
488 if placeholder {
489
490 printWithStyle(screen, Escape(i.placeholder), x, y, 0, fieldWidth, AlignLeft, i.placeholderStyle, true)
491 i.offset = 0
492 } else {
493
494 if i.maskCharacter > 0 {
495 text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text))
496 }
497 if fieldWidth >= uniseg.StringWidth(text) {
498
499 printWithStyle(screen, Escape(text), x, y, 0, fieldWidth, AlignLeft, i.fieldStyle, true)
500 i.offset = 0
501 iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
502 if textPos >= i.cursorPos {
503 return true
504 }
505 cursorScreenPos += screenWidth
506 return false
507 })
508 } else {
509
510 if i.cursorPos < 0 {
511 i.cursorPos = 0
512 } else if i.cursorPos > len(text) {
513 i.cursorPos = len(text)
514 }
515
516 var shiftLeft int
517 if i.offset > i.cursorPos {
518 i.offset = i.cursorPos
519 } else if subWidth := uniseg.StringWidth(text[i.offset:i.cursorPos]); subWidth > fieldWidth-1 {
520 shiftLeft = subWidth - fieldWidth + 1
521 }
522 currentOffset := i.offset
523 iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
524 if textPos >= currentOffset {
525 if shiftLeft > 0 {
526 i.offset = textPos + textWidth
527 shiftLeft -= screenWidth
528 } else {
529 if textPos+textWidth > i.cursorPos {
530 return true
531 }
532 cursorScreenPos += screenWidth
533 }
534 }
535 return false
536 })
537 printWithStyle(screen, Escape(text[i.offset:]), x, y, 0, fieldWidth, AlignLeft, i.fieldStyle, true)
538 }
539 }
540
541
542 i.autocompleteListMutex.Lock()
543 defer i.autocompleteListMutex.Unlock()
544 if i.autocompleteList != nil {
545
546 lheight := i.autocompleteList.GetItemCount()
547 lwidth := 0
548 for index := 0; index < lheight; index++ {
549 entry, _ := i.autocompleteList.GetItemText(index)
550 width := TaggedStringWidth(entry)
551 if width > lwidth {
552 lwidth = width
553 }
554 }
555
556
557 lx := x
558 ly := y + 1
559 _, sheight := screen.Size()
560 if ly+lheight >= sheight && ly-2 > lheight-ly {
561 ly = y - lheight
562 if ly < 0 {
563 ly = 0
564 }
565 }
566 if ly+lheight >= sheight {
567 lheight = sheight - ly
568 }
569 i.autocompleteList.SetRect(lx, ly, lwidth, lheight)
570 i.autocompleteList.Draw(screen)
571 }
572
573
574 if i.HasFocus() {
575 screen.ShowCursor(x+cursorScreenPos, y)
576 }
577 }
578
579
580 func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
581 return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
582 if i.disabled {
583 return
584 }
585
586
587 currentText := i.text
588 defer func() {
589 if i.text != currentText {
590 i.Autocomplete()
591 if i.changed != nil {
592 i.changed(i.text)
593 }
594 }
595 }()
596
597
598 home := func() { i.cursorPos = 0 }
599 end := func() { i.cursorPos = len(i.text) }
600 moveLeft := func() {
601 iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
602 i.cursorPos -= textWidth
603 return true
604 })
605 }
606 moveRight := func() {
607 iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
608 i.cursorPos += textWidth
609 return true
610 })
611 }
612 moveWordLeft := func() {
613 i.cursorPos = len(regexp.MustCompile(`\S+\s*$`).ReplaceAllString(i.text[:i.cursorPos], ""))
614 }
615 moveWordRight := func() {
616 i.cursorPos = len(i.text) - len(regexp.MustCompile(`^\s*\S+\s*`).ReplaceAllString(i.text[i.cursorPos:], ""))
617 }
618
619
620
621 add := func(r rune) bool {
622 newText := i.text[:i.cursorPos] + string(r) + i.text[i.cursorPos:]
623 if i.accept != nil && !i.accept(newText, r) {
624 return false
625 }
626 i.text = newText
627 i.cursorPos += len(string(r))
628 return true
629 }
630
631
632 finish := func(key tcell.Key) {
633 if i.done != nil {
634 i.done(key)
635 }
636 if i.finished != nil {
637 i.finished(key)
638 }
639 }
640
641
642
643 i.autocompleteListMutex.Lock()
644 defer i.autocompleteListMutex.Unlock()
645 if i.autocompleteList != nil {
646 i.autocompleteList.SetChangedFunc(nil)
647 switch key := event.Key(); key {
648 case tcell.KeyEscape:
649 i.autocompleteList = nil
650 return
651 case tcell.KeyEnter, tcell.KeyTab:
652 if i.autocompleted != nil {
653 index := i.autocompleteList.GetCurrentItem()
654 text, _ := i.autocompleteList.GetItemText(index)
655 source := AutocompletedEnter
656 if key == tcell.KeyTab {
657 source = AutocompletedTab
658 }
659 if i.autocompleted(stripTags(text), index, source) {
660 i.autocompleteList = nil
661 currentText = i.GetText()
662 }
663 } else {
664 i.autocompleteList = nil
665 }
666 return
667 case tcell.KeyDown, tcell.KeyUp, tcell.KeyPgDn, tcell.KeyPgUp:
668 i.autocompleteList.SetChangedFunc(func(index int, text, secondaryText string, shortcut rune) {
669 text = stripTags(text)
670 if i.autocompleted != nil {
671 if i.autocompleted(text, index, AutocompletedNavigate) {
672 i.autocompleteList = nil
673 currentText = i.GetText()
674 }
675 } else {
676 i.SetText(text)
677 currentText = stripTags(text)
678 }
679 })
680 i.autocompleteList.InputHandler()(event, setFocus)
681 return
682 }
683 }
684
685
686 switch key := event.Key(); key {
687 case tcell.KeyRune:
688 if event.Modifiers()&tcell.ModAlt > 0 {
689
690 switch event.Rune() {
691 case 'a':
692 home()
693 case 'e':
694 end()
695 case 'b':
696 moveWordLeft()
697 case 'f':
698 moveWordRight()
699 default:
700 if !add(event.Rune()) {
701 return
702 }
703 }
704 } else {
705
706 if !add(event.Rune()) {
707 return
708 }
709 }
710 case tcell.KeyCtrlU:
711 i.text = ""
712 i.cursorPos = 0
713 case tcell.KeyCtrlK:
714 i.text = i.text[:i.cursorPos]
715 case tcell.KeyCtrlW:
716 lastWord := regexp.MustCompile(`\S+\s*$`)
717 newText := lastWord.ReplaceAllString(i.text[:i.cursorPos], "") + i.text[i.cursorPos:]
718 i.cursorPos -= len(i.text) - len(newText)
719 i.text = newText
720 case tcell.KeyBackspace, tcell.KeyBackspace2:
721 iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
722 i.text = i.text[:textPos] + i.text[textPos+textWidth:]
723 i.cursorPos -= textWidth
724 return true
725 })
726 if i.offset >= i.cursorPos {
727 i.offset = 0
728 }
729 case tcell.KeyDelete, tcell.KeyCtrlD:
730 iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
731 i.text = i.text[:i.cursorPos] + i.text[i.cursorPos+textWidth:]
732 return true
733 })
734 case tcell.KeyLeft:
735 if event.Modifiers()&tcell.ModAlt > 0 {
736 moveWordLeft()
737 } else {
738 moveLeft()
739 }
740 case tcell.KeyCtrlB:
741 moveLeft()
742 case tcell.KeyRight:
743 if event.Modifiers()&tcell.ModAlt > 0 {
744 moveWordRight()
745 } else {
746 moveRight()
747 }
748 case tcell.KeyCtrlF:
749 moveRight()
750 case tcell.KeyHome, tcell.KeyCtrlA:
751 home()
752 case tcell.KeyEnd, tcell.KeyCtrlE:
753 end()
754 case tcell.KeyDown:
755 i.autocompleteListMutex.Unlock()
756 i.Autocomplete()
757 i.autocompleteListMutex.Lock()
758 case tcell.KeyEnter, tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
759 finish(key)
760 }
761 })
762 }
763
764
765 func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
766 return i.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
767 if i.disabled {
768 return false, nil
769 }
770
771 currentText := i.GetText()
772 defer func() {
773 if i.GetText() != currentText {
774 i.Autocomplete()
775 if i.changed != nil {
776 i.changed(i.text)
777 }
778 }
779 }()
780
781
782 i.autocompleteListMutex.Lock()
783 defer i.autocompleteListMutex.Unlock()
784 if i.autocompleteList != nil {
785 i.autocompleteList.SetChangedFunc(func(index int, text, secondaryText string, shortcut rune) {
786 text = stripTags(text)
787 if i.autocompleted != nil {
788 if i.autocompleted(text, index, AutocompletedClick) {
789 i.autocompleteList = nil
790 currentText = i.GetText()
791 }
792 return
793 }
794 i.SetText(text)
795 i.autocompleteList = nil
796 })
797 if consumed, _ = i.autocompleteList.MouseHandler()(action, event, setFocus); consumed {
798 setFocus(i)
799 return
800 }
801 }
802
803
804 x, y := event.Position()
805 _, rectY, _, _ := i.GetInnerRect()
806 if !i.InRect(x, y) {
807 return false, nil
808 }
809
810
811 if y == rectY {
812 if action == MouseLeftDown {
813 setFocus(i)
814 consumed = true
815 } else if action == MouseLeftClick {
816
817 if x >= i.fieldX {
818 if !iterateString(i.text[i.offset:], func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth, boundaries int) bool {
819 if x-i.fieldX < screenPos+screenWidth {
820 i.cursorPos = textPos + i.offset
821 return true
822 }
823 return false
824 }) {
825 i.cursorPos = len(i.text)
826 }
827 }
828 consumed = true
829 }
830 }
831
832 return
833 })
834 }
835
View as plain text