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