package qline // qline is a line input library for terminals that utilize // vt100 compatible escape sequences // // Copyright © 2021 Brian Evans // // Permission is hereby granted, free of charge, to any // person obtaining a copy of this software and associated // documentation files (the “Software”), to deal in the // Software without restriction, including without // limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software // is furnished to do so, subject to the following // conditions: // // The above copyright notice and this permission notice // shall be included in all copies or substantial portions // of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY // KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO // THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. import ( "bufio" "fmt" "os" "strings" "tildegit.org/sloum/qline/termios" ) const ( UpArrow rune = iota - 20 DownArrow LeftArrow RightArrow Delete Home End PageUp PageDown Escape rune = 27 NewLine rune = 10 CarriageReturn rune = 13 BackSpace rune = 127 ) var ( width int ) type buffer struct { buf []rune cursor int maxWidth int offset int cursorStart int prompt string } func GetInput(prompt string, content string) string { termios.SetCharMode() cols, _ := termios.GetWindowSize() b := buffer{make([]rune, 0, (len(content)+1)*2), 0, cols-len(prompt), 0, len(prompt), prompt} fmt.Print("\033[25h") // Make sure cursor is visible fmt.Print(prompt) fmt.Print("\033[6n") // Query for cursor position var ch rune var err error reader := bufio.NewReader(os.Stdin) for { ch, err = readKey(reader) if err != nil && err.Error() == "response" { b.cursorStart = int(ch) b.maxWidth = cols - b.cursorStart b.seedContent(content) b.printBuf() } else if err != nil { continue } if ch == CarriageReturn || ch == NewLine { break } if isControl(ch) { b.controlInput(ch) } else { b.addChar(ch, true) } } return b.string() } func (lb buffer) string() string { return string(lb.buf) } func (lb *buffer) deleteChar() { if lb.cursor == len(lb.buf) { return } else if lb.cursor == len(lb.buf)-1 { lb.buf = lb.buf[:len(lb.buf)-1] } else { lb.buf = append(lb.buf[:lb.cursor], lb.buf[lb.cursor+1:]...) } if lb.offset > 0 { lb.offset-- } } func (lb *buffer) addChar(c rune, echo bool) { if c < 9 || (c > 10 && c < 13) || (c > 13 && c < 32) { return } if lb.cursor == len(lb.buf) { lb.buf = append(lb.buf, c) lb.cursor++ } else { lb.buf = append(lb.buf[:lb.cursor+1], lb.buf[lb.cursor:]...) lb.buf[lb.cursor] = c lb.cursor++ } if lb.cursor - lb.offset > lb.maxWidth { lb.offset++ } if echo { lb.printBuf() } } func min(a, b int) int { if a < b { return a } return b } func max(a, b int) int { if a > b { return a } return b } func (lb buffer) printBuf() { out := lb.buf[lb.offset:min(len(lb.buf), lb.offset+lb.maxWidth)] fmt.Printf("\r%s%s\033[0K\r\033[%dC", lb.prompt, string(out), lb.cursor - lb.offset + len(lb.prompt)) } func (lb *buffer) controlInput(c rune) { switch c { case Delete: lb.deleteChar() case BackSpace: if lb.cursor > 0 { lb.cursor-- lb.deleteChar() } case LeftArrow: if lb.offset > 0 && lb.cursor - lb.offset == 0 { lb.offset-- } if lb.cursor > 0 { lb.cursor-- } case RightArrow: // This is still mildly funky, but works enough for ;lb.cursor - lb.offset >= lb.maxWidth && lb.cursor < len(lb.buf); { lb.offset++ } if lb.cursor < len(lb.buf) { lb.cursor++ } case Home: lb.offset = 0 lb.cursor = 0 case End: lb.cursor = len(lb.buf) lb.offset = max(lb.cursor - lb.maxWidth, 0) } lb.printBuf() } func (lb *buffer) seedContent(s string) { for _, r := range s { lb.addChar(r, false) } } func parseCursorPosition(esc string) (int, int, error) { var row, col int r := strings.NewReader(esc) _, err := fmt.Fscanf(r, "\033[%d;%dR", &row, &col) return row, col, err } func readKey(reader *bufio.Reader) (rune, error) { char, _, err := reader.ReadRune() if err != nil { return 0, err } avail := reader.Buffered() if char == Escape && avail > 0 { var b strings.Builder b.WriteRune(27) for ; avail > 0; avail-- { c, _, e := reader.ReadRune() if e != nil { break } b.WriteRune(c) } escSeq := b.String() switch true { case escSeq == "\033[A": char = UpArrow case escSeq == "\033[B": char = DownArrow case escSeq == "\033[C": char = RightArrow case escSeq == "\033[D": char = LeftArrow case escSeq == "\033[5~": char = PageUp case escSeq == "\033[6~": char = PageDown case isHomeKey(escSeq): char = Home case isEndKey(escSeq): char = End case isDeleteKey(escSeq): char = Delete case escSeq[len(escSeq)-1] == 'R': // This is a request for cursor position _, cols, err := parseCursorPosition(escSeq) if err == nil { err = fmt.Errorf("response") } return rune(cols), err } } return char, nil } func isControl(c rune) bool { switch c { case UpArrow, DownArrow, RightArrow, LeftArrow, PageUp, PageDown, Home, End, Delete, BackSpace, CarriageReturn, NewLine, Escape: return true default: return false } } func isDeleteKey(seq string) bool { switch seq { case "\033[3~", "\033[P": return true default: return false } } func isHomeKey(seq string) bool { switch seq { case "\033[1~", "\033[7~", "\033[H", "\033OH": return true default: return false } } func isEndKey(seq string) bool { switch seq { case "\033[4~", "\033[8~", "\033[F", "\033OF": return true default: return false } }