package main import ( "bufio" "fmt" "os" "strings" "tildegit.org/sloum/qline/termios" ) const ( UpArrow rune = iota - 20 DownArrow LeftArrow RightArrow Delete Home End PrintScreen 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 (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-- } } // TODO rework this so that the changes are made but all drawing to the screen comes // from one source and always draws the whole input area. This will make dealing with // offsets much easier. 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: // FIXME: This still isnt working quite right... 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: if lb.cursor < len(lb.buf) { 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 GetText(prompt string, content string) string { cols, _ := termios.GetWindowSize() b := Buffer{make([]rune, 0, (len(content)+1)*2), 0, cols-len(prompt), 0, 0, prompt} b.seedContent(content) fmt.Print(prompt) fmt.Print("\033[6n") b.printBuf() 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 } 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 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': _, cols, err := parseCursorPosition(escSeq) if err == nil { err = fmt.Errorf("response") } return rune(cols), err default: fmt.Printf("Odd sequence: %s\n", escSeq[1:]) } } 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 } } func PrintKey(k rune) { switch true { case k == UpArrow: fmt.Println("UpArrow") case k == DownArrow: fmt.Println("DownArrow") case k == LeftArrow: fmt.Println("LeftArrow") case k == RightArrow: fmt.Println("RightArrow") case k == Escape: fmt.Println("Escape") case k == NewLine: fmt.Println("NewLine") case k == CarriageReturn: fmt.Println("CarriageReturn") case k == Delete: fmt.Println("Delete") case k == Home: fmt.Println("Home") case k == End: fmt.Println("End") case k == BackSpace: fmt.Println("BackSpace") default: fmt.Printf("%c (%d)\n", k, k) } } func main() { // This main func is just for testing and will // be removed with the intention that this just // gets used as a lib termios.SetCharMode() defer termios.Restore() str := "" for { fmt.Print("\n") str = GetText("> ", str) } }