forked from toolshed/abra
		
	
		
			
				
	
	
		
			418 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			418 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package terminal
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"unicode"
 | |
| 
 | |
| 	"golang.org/x/text/width"
 | |
| )
 | |
| 
 | |
| type RuneReader struct {
 | |
| 	stdio Stdio
 | |
| 	state runeReaderState
 | |
| }
 | |
| 
 | |
| func NewRuneReader(stdio Stdio) *RuneReader {
 | |
| 	return &RuneReader{
 | |
| 		stdio: stdio,
 | |
| 		state: newRuneReaderState(stdio.In),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (rr *RuneReader) printChar(char rune, mask rune) error {
 | |
| 	// if we don't need to mask the input
 | |
| 	if mask == 0 {
 | |
| 		// just print the character the user pressed
 | |
| 		_, err := fmt.Fprintf(rr.stdio.Out, "%c", char)
 | |
| 		return err
 | |
| 	}
 | |
| 	// otherwise print the mask we were given
 | |
| 	_, err := fmt.Fprintf(rr.stdio.Out, "%c", mask)
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| type OnRuneFn func(rune, []rune) ([]rune, bool, error)
 | |
| 
 | |
| func (rr *RuneReader) ReadLine(mask rune, onRunes ...OnRuneFn) ([]rune, error) {
 | |
| 	return rr.ReadLineWithDefault(mask, []rune{}, onRunes...)
 | |
| }
 | |
| 
 | |
| func (rr *RuneReader) ReadLineWithDefault(mask rune, d []rune, onRunes ...OnRuneFn) ([]rune, error) {
 | |
| 	line := []rune{}
 | |
| 	// we only care about horizontal displacements from the origin so start counting at 0
 | |
| 	index := 0
 | |
| 
 | |
| 	cursor := &Cursor{
 | |
| 		In:  rr.stdio.In,
 | |
| 		Out: rr.stdio.Out,
 | |
| 	}
 | |
| 
 | |
| 	onRune := func(r rune, line []rune) ([]rune, bool, error) {
 | |
| 		return line, false, nil
 | |
| 	}
 | |
| 
 | |
| 	// if the user pressed a key the caller was interested in capturing
 | |
| 	if len(onRunes) > 0 {
 | |
| 		onRune = onRunes[0]
 | |
| 	}
 | |
| 
 | |
| 	// we get the terminal width and height (if resized after this point the property might become invalid)
 | |
| 	terminalSize, _ := cursor.Size(rr.Buffer())
 | |
| 	// we set the current location of the cursor once
 | |
| 	cursorCurrent, _ := cursor.Location(rr.Buffer())
 | |
| 
 | |
| 	increment := func() {
 | |
| 		if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
 | |
| 			cursorCurrent.X = COORDINATE_SYSTEM_BEGIN
 | |
| 			cursorCurrent.Y++
 | |
| 		} else {
 | |
| 			cursorCurrent.X++
 | |
| 		}
 | |
| 	}
 | |
| 	decrement := func() {
 | |
| 		if cursorCurrent.CursorIsAtLineBegin() {
 | |
| 			cursorCurrent.X = terminalSize.X
 | |
| 			cursorCurrent.Y--
 | |
| 		} else {
 | |
| 			cursorCurrent.X--
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if len(d) > 0 {
 | |
| 		index = len(d)
 | |
| 		if _, err := fmt.Fprint(rr.stdio.Out, string(d)); err != nil {
 | |
| 			return d, err
 | |
| 		}
 | |
| 		line = d
 | |
| 		for range d {
 | |
| 			increment()
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for {
 | |
| 		// wait for some input
 | |
| 		r, _, err := rr.ReadRune()
 | |
| 		if err != nil {
 | |
| 			return line, err
 | |
| 		}
 | |
| 
 | |
| 		if l, stop, err := onRune(r, line); stop || err != nil {
 | |
| 			return l, err
 | |
| 		}
 | |
| 
 | |
| 		// if the user pressed enter or some other newline/termination like ctrl+d
 | |
| 		if r == '\r' || r == '\n' || r == KeyEndTransmission {
 | |
| 			// delete what's printed out on the console screen (cleanup)
 | |
| 			for index > 0 {
 | |
| 				if cursorCurrent.CursorIsAtLineBegin() {
 | |
| 					EraseLine(rr.stdio.Out, ERASE_LINE_END)
 | |
| 					cursor.PreviousLine(1)
 | |
| 					cursor.Forward(int(terminalSize.X))
 | |
| 				} else {
 | |
| 					cursor.Back(1)
 | |
| 				}
 | |
| 				decrement()
 | |
| 				index--
 | |
| 			}
 | |
| 			// move the cursor the a new line
 | |
| 			cursor.MoveNextLine(cursorCurrent, terminalSize)
 | |
| 
 | |
| 			// we're done processing the input
 | |
| 			return line, nil
 | |
| 		}
 | |
| 		// if the user interrupts (ie with ctrl+c)
 | |
| 		if r == KeyInterrupt {
 | |
| 			// go to the beginning of the next line
 | |
| 			if _, err := fmt.Fprint(rr.stdio.Out, "\r\n"); err != nil {
 | |
| 				return line, err
 | |
| 			}
 | |
| 
 | |
| 			// we're done processing the input, and treat interrupt like an error
 | |
| 			return line, InterruptErr
 | |
| 		}
 | |
| 
 | |
| 		// allow for backspace/delete editing of inputs
 | |
| 		if r == KeyBackspace || r == KeyDelete {
 | |
| 			// and we're not at the beginning of the line
 | |
| 			if index > 0 && len(line) > 0 {
 | |
| 				// if we are at the end of the word
 | |
| 				if index == len(line) {
 | |
| 					// just remove the last letter from the internal representation
 | |
| 					// also count the number of cells the rune before the cursor occupied
 | |
| 					cells := runeWidth(line[len(line)-1])
 | |
| 					line = line[:len(line)-1]
 | |
| 					// go back one
 | |
| 					if cursorCurrent.X == 1 {
 | |
| 						cursor.PreviousLine(1)
 | |
| 						cursor.Forward(int(terminalSize.X))
 | |
| 					} else {
 | |
| 						cursor.Back(cells)
 | |
| 					}
 | |
| 
 | |
| 					// clear the rest of the line
 | |
| 					EraseLine(rr.stdio.Out, ERASE_LINE_END)
 | |
| 				} else {
 | |
| 					// we need to remove a character from the middle of the word
 | |
| 
 | |
| 					cells := runeWidth(line[index-1])
 | |
| 
 | |
| 					// remove the current index from the list
 | |
| 					line = append(line[:index-1], line[index:]...)
 | |
| 
 | |
| 					// save the current position of the cursor, as we have to move the cursor one back to erase the current symbol
 | |
| 					// and then move the cursor for each symbol in line[index-1:] to print it out, afterwards we want to restore
 | |
| 					// the cursor to its previous location.
 | |
| 					cursor.Save()
 | |
| 
 | |
| 					// clear the rest of the line
 | |
| 					cursor.Back(cells)
 | |
| 
 | |
| 					// print what comes after
 | |
| 					for _, char := range line[index-1:] {
 | |
| 						//Erase symbols which are left over from older print
 | |
| 						EraseLine(rr.stdio.Out, ERASE_LINE_END)
 | |
| 						// print characters to the new line appropriately
 | |
| 						if err := rr.printChar(char, mask); err != nil {
 | |
| 							return line, err
 | |
| 						}
 | |
| 					}
 | |
| 					// erase what's left over from last print
 | |
| 					if cursorCurrent.Y < terminalSize.Y {
 | |
| 						cursor.NextLine(1)
 | |
| 						EraseLine(rr.stdio.Out, ERASE_LINE_END)
 | |
| 					}
 | |
| 					// restore cursor
 | |
| 					cursor.Restore()
 | |
| 					if cursorCurrent.CursorIsAtLineBegin() {
 | |
| 						cursor.PreviousLine(1)
 | |
| 						cursor.Forward(int(terminalSize.X))
 | |
| 					} else {
 | |
| 						cursor.Back(cells)
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				// decrement the index
 | |
| 				index--
 | |
| 				decrement()
 | |
| 			} else {
 | |
| 				// otherwise the user pressed backspace while at the beginning of the line
 | |
| 				_ = soundBell(rr.stdio.Out)
 | |
| 			}
 | |
| 
 | |
| 			// we're done processing this key
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// if the left arrow is pressed
 | |
| 		if r == KeyArrowLeft {
 | |
| 			// if we have space to the left
 | |
| 			if index > 0 {
 | |
| 				//move the cursor to the prev line if necessary
 | |
| 				if cursorCurrent.CursorIsAtLineBegin() {
 | |
| 					cursor.PreviousLine(1)
 | |
| 					cursor.Forward(int(terminalSize.X))
 | |
| 				} else {
 | |
| 					cursor.Back(runeWidth(line[index-1]))
 | |
| 				}
 | |
| 				//decrement the index
 | |
| 				index--
 | |
| 				decrement()
 | |
| 
 | |
| 			} else {
 | |
| 				// otherwise we are at the beginning of where we started reading lines
 | |
| 				// sound the bell
 | |
| 				_ = soundBell(rr.stdio.Out)
 | |
| 			}
 | |
| 
 | |
| 			// we're done processing this key press
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// if the right arrow is pressed
 | |
| 		if r == KeyArrowRight {
 | |
| 			// if we have space to the right
 | |
| 			if index < len(line) {
 | |
| 				// move the cursor to the next line if necessary
 | |
| 				if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
 | |
| 					cursor.NextLine(1)
 | |
| 				} else {
 | |
| 					cursor.Forward(runeWidth(line[index]))
 | |
| 				}
 | |
| 				index++
 | |
| 				increment()
 | |
| 
 | |
| 			} else {
 | |
| 				// otherwise we are at the end of the word and can't go past
 | |
| 				// sound the bell
 | |
| 				_ = soundBell(rr.stdio.Out)
 | |
| 			}
 | |
| 
 | |
| 			// we're done processing this key press
 | |
| 			continue
 | |
| 		}
 | |
| 		// the user pressed one of the special keys
 | |
| 		if r == SpecialKeyHome {
 | |
| 			for index > 0 {
 | |
| 				if cursorCurrent.CursorIsAtLineBegin() {
 | |
| 					cursor.PreviousLine(1)
 | |
| 					cursor.Forward(int(terminalSize.X))
 | |
| 					cursorCurrent.Y--
 | |
| 					cursorCurrent.X = terminalSize.X
 | |
| 				} else {
 | |
| 					cursor.Back(runeWidth(line[index-1]))
 | |
| 					cursorCurrent.X -= Short(runeWidth(line[index-1]))
 | |
| 				}
 | |
| 				index--
 | |
| 			}
 | |
| 			continue
 | |
| 			// user pressed end
 | |
| 		} else if r == SpecialKeyEnd {
 | |
| 			for index != len(line) {
 | |
| 				if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
 | |
| 					cursor.NextLine(1)
 | |
| 					cursorCurrent.Y++
 | |
| 					cursorCurrent.X = COORDINATE_SYSTEM_BEGIN
 | |
| 				} else {
 | |
| 					cursor.Forward(runeWidth(line[index]))
 | |
| 					cursorCurrent.X += Short(runeWidth(line[index]))
 | |
| 				}
 | |
| 				index++
 | |
| 			}
 | |
| 			continue
 | |
| 			// user pressed forward delete key
 | |
| 		} else if r == SpecialKeyDelete {
 | |
| 			// if index at the end of the line nothing to delete
 | |
| 			if index != len(line) {
 | |
| 				// save the current position of the cursor, as we have to  erase the current symbol
 | |
| 				// and then move the cursor for each symbol in line[index:] to print it out, afterwards we want to restore
 | |
| 				// the cursor to its previous location.
 | |
| 				cursor.Save()
 | |
| 				// remove the symbol after the cursor
 | |
| 				line = append(line[:index], line[index+1:]...)
 | |
| 				// print the updated line
 | |
| 				for _, char := range line[index:] {
 | |
| 					EraseLine(rr.stdio.Out, ERASE_LINE_END)
 | |
| 					// print out the character
 | |
| 					if err := rr.printChar(char, mask); err != nil {
 | |
| 						return line, err
 | |
| 					}
 | |
| 				}
 | |
| 				// erase what's left on last line
 | |
| 				if cursorCurrent.Y < terminalSize.Y {
 | |
| 					cursor.NextLine(1)
 | |
| 					EraseLine(rr.stdio.Out, ERASE_LINE_END)
 | |
| 				}
 | |
| 				// restore cursor
 | |
| 				cursor.Restore()
 | |
| 				if len(line) == 0 || index == len(line) {
 | |
| 					EraseLine(rr.stdio.Out, ERASE_LINE_END)
 | |
| 				}
 | |
| 			}
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// if the letter is another escape sequence
 | |
| 		if unicode.IsControl(r) || r == IgnoreKey {
 | |
| 			// ignore it
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// the user pressed a regular key
 | |
| 
 | |
| 		// if we are at the end of the line
 | |
| 		if index == len(line) {
 | |
| 			// just append the character at the end of the line
 | |
| 			line = append(line, r)
 | |
| 			// save the location of the cursor
 | |
| 			index++
 | |
| 			increment()
 | |
| 			// print out the character
 | |
| 			if err := rr.printChar(r, mask); err != nil {
 | |
| 				return line, err
 | |
| 			}
 | |
| 		} else {
 | |
| 			// we are in the middle of the word so we need to insert the character the user pressed
 | |
| 			line = append(line[:index], append([]rune{r}, line[index:]...)...)
 | |
| 			// save the current position of the cursor, as we have to move the cursor back to erase the current symbol
 | |
| 			// and then move for each symbol in line[index:] to print it out, afterwards we want to restore
 | |
| 			// cursor's location to its previous one.
 | |
| 			cursor.Save()
 | |
| 			EraseLine(rr.stdio.Out, ERASE_LINE_END)
 | |
| 			// remove the symbol after the cursor
 | |
| 			// print the updated line
 | |
| 			for _, char := range line[index:] {
 | |
| 				EraseLine(rr.stdio.Out, ERASE_LINE_END)
 | |
| 				// print out the character
 | |
| 				if err := rr.printChar(char, mask); err != nil {
 | |
| 					return line, err
 | |
| 				}
 | |
| 				increment()
 | |
| 			}
 | |
| 			// if we are at the last line, we want to visually insert a new line and append to it.
 | |
| 			if cursorCurrent.CursorIsAtLineEnd(terminalSize) && cursorCurrent.Y == terminalSize.Y {
 | |
| 				// add a new line to the terminal
 | |
| 				if _, err := fmt.Fprintln(rr.stdio.Out); err != nil {
 | |
| 					return line, err
 | |
| 				}
 | |
| 				// restore the position of the cursor horizontally
 | |
| 				cursor.Restore()
 | |
| 				// restore the position of the cursor vertically
 | |
| 				cursor.PreviousLine(1)
 | |
| 			} else {
 | |
| 				// restore cursor
 | |
| 				cursor.Restore()
 | |
| 			}
 | |
| 			// check if cursor needs to move to next line
 | |
| 			cursorCurrent, _ = cursor.Location(rr.Buffer())
 | |
| 			if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
 | |
| 				cursor.NextLine(1)
 | |
| 			} else {
 | |
| 				cursor.Forward(runeWidth(r))
 | |
| 			}
 | |
| 			// increment the index
 | |
| 			index++
 | |
| 			increment()
 | |
| 
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // runeWidth returns the number of columns spanned by a rune when printed to the terminal
 | |
| func runeWidth(r rune) int {
 | |
| 	switch width.LookupRune(r).Kind() {
 | |
| 	case width.EastAsianWide, width.EastAsianFullwidth:
 | |
| 		return 2
 | |
| 	}
 | |
| 
 | |
| 	if !unicode.IsPrint(r) {
 | |
| 		return 0
 | |
| 	}
 | |
| 	return 1
 | |
| }
 | |
| 
 | |
| // isAnsiMarker returns if a rune denotes the start of an ANSI sequence
 | |
| func isAnsiMarker(r rune) bool {
 | |
| 	return r == '\x1B'
 | |
| }
 | |
| 
 | |
| // isAnsiTerminator returns if a rune denotes the end of an ANSI sequence
 | |
| func isAnsiTerminator(r rune) bool {
 | |
| 	return (r >= 0x40 && r <= 0x5a) || (r == 0x5e) || (r >= 0x60 && r <= 0x7e)
 | |
| }
 | |
| 
 | |
| // StringWidth returns the visible width of a string when printed to the terminal
 | |
| func StringWidth(str string) int {
 | |
| 	w := 0
 | |
| 	ansi := false
 | |
| 
 | |
| 	for _, r := range str {
 | |
| 		// increase width only when outside of ANSI escape sequences
 | |
| 		if ansi || isAnsiMarker(r) {
 | |
| 			ansi = !isAnsiTerminator(r)
 | |
| 		} else {
 | |
| 			w += runeWidth(r)
 | |
| 		}
 | |
| 	}
 | |
| 	return w
 | |
| }
 |