github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/utils/readline/readline.go (about) 1 package readline 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "regexp" 8 "strings" 9 "sync/atomic" 10 ) 11 12 var rxMultiline = regexp.MustCompile(`[\r\n]+`) 13 14 // Readline displays the readline prompt. 15 // It will return a string (user entered data) or an error. 16 func (rl *Instance) Readline() (_ string, err error) { 17 rl.fdMutex.Lock() 18 rl.Active = true 19 20 state, err := MakeRaw(int(os.Stdin.Fd())) 21 rl.sigwinch() 22 23 rl.fdMutex.Unlock() 24 25 if err != nil { 26 return "", fmt.Errorf("unable to modify fd %d: %s", os.Stdout.Fd(), err.Error()) 27 } 28 29 defer func() { 30 print(rl.clearPreviewStr()) 31 32 rl.fdMutex.Lock() 33 34 rl.closeSigwinch() 35 36 rl.Active = false 37 // return an error if Restore fails. However we don't want to return 38 // `nil` if there is no error because there might be a CtrlC or EOF 39 // that needs to be returned 40 r := Restore(int(os.Stdin.Fd()), state) 41 if r != nil { 42 err = r 43 } 44 45 rl.fdMutex.Unlock() 46 }() 47 48 x, _ := rl.getCursorPos() 49 switch x { 50 case -1: 51 print(string(leftMost())) 52 case 0: 53 // do nothing 54 default: 55 print("\r\n") 56 } 57 print(rl.prompt) 58 59 rl.line.Set(rl, []rune{}) 60 rl.line.SetRunePos(0) 61 rl.lineChange = "" 62 rl.viUndoHistory = []*UnicodeT{rl.line.Duplicate()} 63 rl.histPos = rl.History.Len() 64 rl.modeViMode = vimInsert 65 atomic.StoreInt32(&rl.delayedSyntaxCount, 0) 66 rl.resetHintText() 67 rl.resetTabCompletion() 68 69 if len(rl.multiSplit) > 0 { 70 r := []rune(rl.multiSplit[0]) 71 print(rl.readlineInputStr(r)) 72 print(rl.carriageReturnStr()) 73 if len(rl.multiSplit) > 1 { 74 rl.multiSplit = rl.multiSplit[1:] 75 } else { 76 rl.multiSplit = []string{} 77 } 78 return rl.line.String(), nil 79 } 80 81 rl.termWidth = GetTermWidth() 82 rl.getHintText() 83 print(rl.renderHelpersStr()) 84 85 for { 86 if rl.line.RuneLen() == 0 { 87 // clear the cache when the line is cleared 88 rl.cacheHint.Init(rl) 89 rl.cacheSyntax.Init(rl) 90 } 91 92 go delayedSyntaxTimer(rl, atomic.LoadInt32(&rl.delayedSyntaxCount)) 93 rl.viUndoSkipAppend = false 94 b := make([]byte, 1024*1024) 95 var i int 96 97 if !rl.skipStdinRead { 98 i, err = read(b) 99 if err != nil { 100 return "", err 101 } 102 rl.termWidth = GetTermWidth() 103 } 104 atomic.AddInt32(&rl.delayedSyntaxCount, 1) 105 106 rl.skipStdinRead = false 107 r := []rune(string(b)) 108 109 if isMultiline(r[:i]) || len(rl.multiline) > 0 { 110 rl.multiline = append(rl.multiline, b[:i]...) 111 112 if !rl.allowMultiline(rl.multiline) { 113 rl.multiline = []byte{} 114 continue 115 } 116 117 s := string(rl.multiline) 118 rl.multiSplit = rxMultiline.Split(s, -1) 119 120 r = []rune(rl.multiSplit[0]) 121 rl.modeViMode = vimInsert 122 print(rl.readlineInputStr(r)) 123 print(rl.carriageReturnStr()) 124 rl.multiline = []byte{} 125 if len(rl.multiSplit) > 1 { 126 rl.multiSplit = rl.multiSplit[1:] 127 } else { 128 rl.multiSplit = []string{} 129 } 130 return rl.line.String(), nil 131 } 132 133 s := string(r[:i]) 134 if rl.evtKeyPress[s] != nil { 135 ret := rl.evtKeyPress[s](s, rl.line.Runes(), rl.line.RunePos()) 136 137 rl.clearPrompt() 138 rl.line.Set(rl, append(ret.NewLine, []rune{}...)) 139 print(rl.echoStr()) 140 // TODO: should this be above echo? 141 rl.line.SetRunePos(ret.NewPos) 142 143 if ret.ClearHelpers { 144 rl.resetHelpers() 145 } else { 146 output := rl.updateHelpersStr() 147 output += rl.renderHelpersStr() 148 print(output) 149 } 150 151 if len(ret.HintText) > 0 { 152 rl.hintText = ret.HintText 153 output := rl.clearHelpersStr() 154 output += rl.renderHelpersStr() 155 print(output) 156 } 157 158 if ret.DisplayPreview { 159 if rl.previewMode == previewModeClosed { 160 HkFnPreviewToggle(rl) 161 } 162 } 163 164 //rl.previewItem 165 166 if ret.Callback != nil { 167 err = ret.Callback() 168 if err != nil { 169 rl.hintText = []rune(err.Error()) 170 output := rl.clearHelpersStr() 171 output += rl.renderHelpersStr() 172 print(output) 173 } 174 } 175 176 if !ret.ForwardKey { 177 continue 178 } 179 if ret.CloseReadline { 180 print(rl.clearHelpersStr()) 181 return rl.line.String(), nil 182 } 183 } 184 185 i = removeNonPrintableChars(b[:i]) 186 187 // Used for syntax completion 188 rl.lineChange = string(b[:i]) 189 190 // Slow or invisible tab completions shouldn't lock up cursor movement 191 rl.tabMutex.Lock() 192 lenTcS := len(rl.tcSuggestions) 193 rl.tabMutex.Unlock() 194 if rl.modeTabCompletion && lenTcS == 0 { 195 if rl.delayedTabContext.cancel != nil { 196 rl.delayedTabContext.cancel() 197 } 198 rl.modeTabCompletion = false 199 print(rl.updateHelpersStr()) 200 } 201 202 switch b[0] { 203 case charCtrlA: 204 HkFnMoveToStartOfLine(rl) 205 206 case charCtrlC: 207 output := rl.clearPreviewStr() 208 output += rl.clearHelpersStr() 209 print(output) 210 return "", CtrlC 211 212 case charEOF: 213 if rl.line.RuneLen() == 0 { 214 output := rl.clearPreviewStr() 215 output += rl.clearHelpersStr() 216 print(output) 217 return "", EOF 218 } 219 220 case charCtrlE: 221 HkFnMoveToEndOfLine(rl) 222 223 case charCtrlF: 224 HkFnFuzzyFind(rl) 225 226 case charCtrlG: 227 HkFnCancelAction(rl) 228 229 case charCtrlK: 230 HkFnClearAfterCursor(rl) 231 232 case charCtrlL: 233 HkFnClearScreen(rl) 234 235 case charCtrlR: 236 HkFnSearchHistory(rl) 237 238 case charCtrlU: 239 HkFnClearLine(rl) 240 241 case charCtrlZ: 242 HkFnUndo(rl) 243 244 case charTab: 245 HkFnAutocomplete(rl) 246 247 case '\r': 248 fallthrough 249 case '\n': 250 var output string 251 rl.tabMutex.Lock() 252 var suggestions *suggestionsT 253 if rl.modeTabFind { 254 suggestions = newSuggestionsT(rl, rl.tfSuggestions) 255 } else { 256 suggestions = newSuggestionsT(rl, rl.tcSuggestions) 257 } 258 rl.tabMutex.Unlock() 259 260 switch { 261 case rl.previewMode == previewModeOpen: 262 output += rl.clearPreviewStr() 263 output += rl.clearHelpersStr() 264 print(output) 265 continue 266 case rl.previewMode == previewModeAutocomplete: 267 rl.previewMode = previewModeOpen 268 if !rl.modeTabCompletion { 269 output += rl.clearPreviewStr() 270 output += rl.clearHelpersStr() 271 print(output) 272 continue 273 } 274 } 275 276 if rl.modeTabCompletion || len(rl.tfLine) != 0 /*&& len(suggestions) > 0*/ { 277 tfLine := rl.tfLine 278 cell := (rl.tcMaxX * (rl.tcPosY - 1)) + rl.tcOffset + rl.tcPosX - 1 279 output += rl.clearHelpersStr() 280 rl.resetTabCompletion() 281 output += rl.renderHelpersStr() 282 if suggestions.Len() > 0 { 283 prefix, line := suggestions.ItemCompletionReturn(cell) 284 if len(prefix) == 0 && len(rl.tcPrefix) > 0 { 285 l := -len(rl.tcPrefix) 286 if l == -1 && rl.line.RuneLen() > 0 && rl.line.RunePos() == rl.line.RuneLen() { 287 rl.line.Set(rl, rl.line.Runes()[:rl.line.RuneLen()-1]) 288 } else { 289 output += rl.viDeleteByAdjustStr(l) 290 } 291 } 292 output += rl.insertStr([]rune(line)) 293 } else { 294 output += rl.insertStr(tfLine) 295 } 296 print(output) 297 continue 298 } 299 output += rl.carriageReturnStr() 300 print(output) 301 return rl.line.String(), nil 302 303 case charBackspace, charBackspace2: 304 if rl.modeTabFind { 305 print(rl.backspaceTabFindStr()) 306 rl.viUndoSkipAppend = true 307 } else { 308 print(rl.backspaceStr()) 309 } 310 311 case charEscape: 312 print(rl.escapeSeq(r[:i])) 313 314 default: 315 if rl.modeTabFind { 316 print(rl.updateTabFindStr(r[:i])) 317 rl.viUndoSkipAppend = true 318 } else { 319 print(rl.readlineInputStr(r[:i])) 320 if len(rl.multiline) > 0 && rl.modeViMode == vimKeys { 321 rl.skipStdinRead = true 322 } 323 } 324 } 325 326 rl.undoAppendHistory() 327 } 328 } 329 330 func (rl *Instance) escapeSeq(r []rune) string { 331 var output string 332 switch string(r) { 333 case seqEscape: 334 HkFnCancelAction(rl) 335 336 case seqDelete: 337 if rl.modeTabFind { 338 output += rl.backspaceTabFindStr() 339 } else { 340 output += rl.deleteStr() 341 } 342 343 case seqUp: 344 rl.viUndoSkipAppend = true 345 346 if rl.modeTabCompletion { 347 rl.moveTabCompletionHighlight(0, -1) 348 output += rl.renderHelpersStr() 349 return output 350 } 351 352 // are we midway through a long line that wrap multiple terminal lines? 353 posX, posY := rl.lineWrapCellPos() 354 if posY > 0 { 355 pos := rl.line.CellPos() - rl.termWidth + rl.promptLen 356 rl.line.SetCellPos(pos) 357 358 newX, _ := rl.lineWrapCellPos() 359 offset := newX - posX 360 switch { 361 case offset > 0: 362 output += moveCursorForwardsStr(offset) 363 case offset < 0: 364 output += moveCursorBackwardsStr(offset * -1) 365 } 366 367 output += moveCursorUpStr(1) 368 return output 369 } 370 371 rl.walkHistory(-1) 372 373 case seqDown: 374 rl.viUndoSkipAppend = true 375 376 if rl.modeTabCompletion { 377 rl.moveTabCompletionHighlight(0, 1) 378 output += rl.renderHelpersStr() 379 return output 380 } 381 382 // are we midway through a long line that wrap multiple terminal lines? 383 posX, posY := rl.lineWrapCellPos() 384 _, lineY := rl.lineWrapCellLen() 385 if posY < lineY { 386 pos := rl.line.CellPos() + rl.termWidth - rl.promptLen 387 rl.line.SetCellPos(pos) 388 389 newX, _ := rl.lineWrapCellPos() 390 offset := newX - posX 391 switch { 392 case offset > 0: 393 output += moveCursorForwardsStr(offset) 394 case offset < 0: 395 output += moveCursorBackwardsStr(offset * -1) 396 } 397 398 output += moveCursorDownStr(1) 399 return output 400 } 401 402 rl.walkHistory(1) 403 404 case seqBackwards: 405 if rl.modeTabCompletion { 406 rl.moveTabCompletionHighlight(-1, 0) 407 output += rl.renderHelpersStr() 408 return output 409 } 410 411 output += rl.moveCursorByRuneAdjustStr(-1) 412 rl.viUndoSkipAppend = true 413 414 case seqForwards: 415 if rl.modeTabCompletion { 416 rl.moveTabCompletionHighlight(1, 0) 417 output += rl.renderHelpersStr() 418 return output 419 } 420 421 //if (rl.modeViMode == vimInsert && rl.line.RunePos() < rl.line.RuneLen()) || 422 // (rl.modeViMode != vimInsert && rl.line.RunePos() < rl.line.RuneLen()-1) { 423 output += rl.moveCursorByRuneAdjustStr(1) 424 //} 425 rl.viUndoSkipAppend = true 426 427 case seqHome, seqHomeSc: 428 if rl.modeTabCompletion { 429 return output 430 } 431 432 output += rl.moveCursorByRuneAdjustStr(-rl.line.RunePos()) 433 rl.viUndoSkipAppend = true 434 435 case seqEnd, seqEndSc: 436 if rl.modeTabCompletion { 437 return output 438 } 439 440 output += rl.moveCursorByRuneAdjustStr(rl.line.RuneLen() - rl.line.RunePos()) 441 rl.viUndoSkipAppend = true 442 443 case seqShiftTab: 444 if rl.modeTabCompletion { 445 rl.moveTabCompletionHighlight(-1, 0) 446 output += rl.renderHelpersStr() 447 return output 448 } 449 450 case seqPageUp, seqOptUp, seqCtrlUp: 451 output += rl.previewPageUpStr() 452 return output 453 454 case seqPageDown, seqOptDown, seqCtrlDown: 455 output += rl.previewPageDownStr() 456 return output 457 458 case seqF1, seqF1VT100: 459 HkFnPreviewToggle(rl) 460 return output 461 462 case seqF9: 463 HkFnPreviewLine(rl) 464 return output 465 466 case seqAltF, seqOptRight, seqCtrlRight: 467 HkFnJumpForwards(rl) 468 469 case seqAltB, seqOptLeft, seqCtrlLeft: 470 HkFnJumpBackwards(rl) 471 472 // TODO: test me 473 case seqShiftF1: 474 HkFnRecallWord1(rl) 475 case seqShiftF2: 476 HkFnRecallWord2(rl) 477 case seqShiftF3: 478 HkFnRecallWord3(rl) 479 case seqShiftF4: 480 HkFnRecallWord4(rl) 481 case seqShiftF5: 482 HkFnRecallWord5(rl) 483 case seqShiftF6: 484 HkFnRecallWord6(rl) 485 case seqShiftF7: 486 HkFnRecallWord7(rl) 487 case seqShiftF8: 488 HkFnRecallWord8(rl) 489 case seqShiftF9: 490 HkFnRecallWord9(rl) 491 case seqShiftF10: 492 HkFnRecallWord10(rl) 493 case seqShiftF11: 494 HkFnRecallWord11(rl) 495 case seqShiftF12: 496 HkFnRecallWord12(rl) 497 498 default: 499 if rl.modeTabFind /*|| rl.modeAutoFind*/ { 500 //rl.modeTabFind = false 501 //rl.modeAutoFind = false 502 return output 503 } 504 // alt+numeric append / delete 505 if len(r) == 2 && '1' <= r[1] && r[1] <= '9' { 506 if rl.modeViMode == vimDelete { 507 output += rl.vimDeleteStr(r) 508 return output 509 } 510 511 } else { 512 rl.viUndoSkipAppend = true 513 } 514 } 515 516 return output 517 } 518 519 // readlineInput is an unexported function used to determine what mode of text 520 // entry readline is currently configured for and then update the line entries 521 // accordingly. 522 func (rl *Instance) readlineInputStr(r []rune) string { 523 if len(r) == 0 { 524 return "" 525 } 526 527 var output string 528 529 switch rl.modeViMode { 530 case vimKeys: 531 output += rl.vi(r[0]) 532 output += rl.viHintMessageStr() 533 534 case vimDelete: 535 output += rl.vimDeleteStr(r) 536 output += rl.viHintMessageStr() 537 538 case vimReplaceOnce: 539 rl.modeViMode = vimKeys 540 output += rl.deleteStr() 541 output += rl.insertStr([]rune{r[0]}) 542 output += rl.viHintMessageStr() 543 544 case vimReplaceMany: 545 for _, char := range r { 546 output += rl.deleteStr() 547 output += rl.insertStr([]rune{char}) 548 } 549 output += rl.viHintMessageStr() 550 551 default: 552 output += rl.insertStr(r) 553 } 554 555 return output 556 } 557 558 // SetPrompt will define the readline prompt string. 559 // It also calculates the runes in the string as well as any non-printable 560 // escape codes. 561 func (rl *Instance) SetPrompt(s string) { 562 s = strings.ReplaceAll(s, "\r", "") 563 s = strings.ReplaceAll(s, "\t", " ") 564 split := strings.Split(s, "\n") 565 if len(split) > 1 { 566 print(strings.Join(split[:len(split)-1], "\r\n") + "\r\n") 567 s = split[len(split)-1] 568 } 569 rl.prompt = s 570 rl.promptLen = strLen(s) 571 } 572 573 func (rl *Instance) carriageReturnStr() string { 574 output := rl.clearHelpersStr() 575 output += "\r\n" 576 if rl.HistoryAutoWrite { 577 var err error 578 rl.histPos, err = rl.History.Write(rl.line.String()) 579 if err != nil { 580 output += err.Error() + "\r\n" 581 } 582 } 583 return output 584 } 585 586 func isMultiline(r []rune) bool { 587 for i := range r { 588 if (r[i] == '\r' || r[i] == '\n') && i != len(r)-1 { 589 return true 590 } 591 } 592 return false 593 } 594 595 func (rl *Instance) allowMultiline(data []byte) bool { 596 print(rl.clearHelpersStr()) 597 printf("\r\nWARNING: %d bytes of multiline data was dumped into the shell!", len(data)) 598 for { 599 print("\r\nDo you wish to proceed (yes|no|preview)? [y/n/p] ") 600 601 b := make([]byte, 1024*1024) 602 603 i, err := read(b) 604 if err != nil { 605 return false 606 } 607 608 if i > 1 { 609 rl.multiline = append(rl.multiline, b[:i]...) 610 print(moveCursorUpStr(2)) 611 return rl.allowMultiline(append(data, b[:i]...)) 612 } 613 614 s := string(b[:i]) 615 print(s) 616 617 switch s { 618 case "y", "Y": 619 print("\r\n" + rl.prompt) 620 return true 621 622 case "n", "N": 623 print("\r\n" + rl.prompt) 624 return false 625 626 case "p", "P": 627 preview := string(bytes.Replace(data, []byte{'\r'}, []byte{'\r', '\n'}, -1)) 628 if rl.SyntaxHighlighter != nil { 629 preview = rl.SyntaxHighlighter([]rune(preview)) 630 } 631 print("\r\n" + preview) 632 633 default: 634 print("\r\nInvalid response. Please answer `y` (yes), `n` (no) or `p` (preview)") 635 } 636 } 637 }