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  }