123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- package terminal
- import (
- "fmt"
- "unicode"
- )
- type RuneReader struct {
- stdio Stdio
- cursor *Cursor
- state runeReaderState
- }
- func NewRuneReader(stdio Stdio) *RuneReader {
- return &RuneReader{
- stdio: stdio,
- state: newRuneReaderState(stdio.In),
- }
- }
- func (rr *RuneReader) printChar(char rune, mask rune) {
- // if we don't need to mask the input
- if mask == 0 {
- // just print the character the user pressed
- fmt.Fprintf(rr.stdio.Out, "%c", char)
- } else {
- // otherwise print the mask we were given
- fmt.Fprintf(rr.stdio.Out, "%c", mask)
- }
- }
- func (rr *RuneReader) ReadLine(mask rune) ([]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,
- }
- // we get the terminal width and height (if resized after this point the property might become invalid)
- terminalSize, _ := cursor.Size(rr.Buffer())
- for {
- // wait for some input
- r, _, err := rr.ReadRune()
- if err != nil {
- return line, err
- }
- // we set the current location of the cursor and update it after every key press
- cursorCurrent, err := cursor.Location(rr.Buffer())
- // 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))
- cursorCurrent.X = terminalSize.X
- cursorCurrent.Y--
- } else {
- cursor.Back(1)
- cursorCurrent.X--
- }
- 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
- fmt.Fprint(rr.stdio.Out, "\r\n")
- // 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
- line = line[:len(line)-1]
- // go back one
- if cursorCurrent.X == 1 {
- cursor.PreviousLine(1)
- cursor.Forward(int(terminalSize.X))
- } else {
- cursor.Back(1)
- }
- // 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
- // 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(1)
- // 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
- rr.printChar(char, mask)
- }
- // 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(1)
- }
- }
- // decrement the index
- index--
- } 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(1)
- }
- //decrement the index
- index--
- } 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(1)
- }
- index++
- } 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.X = terminalSize.X
- cursorCurrent.Y--
- } else {
- cursor.Back(1)
- cursorCurrent.X--
- }
- index--
- }
- continue
- // user pressed end
- } else if r == SpecialKeyEnd {
- for index != len(line) {
- if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
- cursor.NextLine(1)
- cursorCurrent.X = COORDINATE_SYSTEM_BEGIN
- cursorCurrent.Y++
- } else {
- cursor.Forward(1)
- cursorCurrent.X++
- }
- 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
- rr.printChar(char, mask)
- }
- // 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++
- // print out the character
- rr.printChar(r, mask)
- } 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
- rr.printChar(char, mask)
- cursorCurrent.X++
- }
- // 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
- fmt.Fprintln(rr.stdio.Out)
- // restore the position of the cursor horizontally
- cursor.Restore()
- // restore the position of the cursor vertically
- cursor.Up(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(1)
- }
- // increment the index
- index++
- }
- }
- }
|