package ttyutil import ( "bytes" "fmt" "io" "os" "os/signal" "github.com/mattn/go-colorable" "github.com/mattn/go-runewidth" "github.com/mattn/go-tty" ) type ctx struct { w io.Writer input []rune last []rune prompt string cursor_x int old_row int old_crow int size int } func (c *ctx) redraw(dirty bool, passwordChar rune) error { var buf bytes.Buffer buf.WriteString("\x1b[5>h") buf.WriteString("\x1b[1G") if dirty { buf.WriteString("\x1b[0K") } for i := 0; i < c.old_row-c.old_crow; i++ { buf.WriteString("\x1b[B") } for i := 0; i < c.old_row; i++ { if dirty { buf.WriteString("\x1b[2K") } buf.WriteString("\x1b[A") } var rs []rune if passwordChar != 0 { for i := 0; i < len(c.input); i++ { rs = append(rs, passwordChar) } } else { rs = c.input } ccol, crow, col, row := -1, 0, 0, 0 plen := len([]rune(c.prompt)) for i, r := range []rune(c.prompt + string(rs)) { if i == plen+c.cursor_x { ccol = col crow = row } rw := runewidth.RuneWidth(r) if col+rw > c.size { col = 0 row++ if dirty { buf.WriteString("\n\r\x1b[0K") } } if dirty { buf.WriteString(string(r)) } col += rw } if dirty { buf.WriteString("\x1b[1G") for i := 0; i < row; i++ { buf.WriteString("\x1b[A") } } if ccol == -1 { ccol = col crow = row } for i := 0; i < crow; i++ { buf.WriteString("\x1b[B") } buf.WriteString(fmt.Sprintf("\x1b[%dG", ccol+1)) buf.WriteString("\x1b[5>l") io.Copy(c.w, &buf) c.old_row = row c.old_crow = crow return nil } func ReadLine(tty *tty.TTY) (string, error) { c := new(ctx) c.w = colorable.NewColorableStdout() quit := false sc := make(chan os.Signal, 1) signal.Notify(sc, os.Interrupt) go func() { <-sc c.input = nil quit = true }() c.size = 80 dirty := true loop: for !quit { err := c.redraw(dirty, 0) if err != nil { return "", err } dirty = false r, err := tty.ReadRune() if err != nil { break } switch r { case 0: case 1: // CTRL-A c.cursor_x = 0 case 2: // CTRL-B if c.cursor_x > 0 { c.cursor_x-- } case 3: // BREAK return "", nil case 4: // CTRL-D if len(c.input) > 0 { continue } return "", io.EOF case 5: // CTRL-E c.cursor_x = len(c.input) case 6: // CTRL-F if c.cursor_x < len(c.input) { c.cursor_x++ } case 8, 0x7F: // BS if c.cursor_x > 0 { c.input = append(c.input[0:c.cursor_x-1], c.input[c.cursor_x:len(c.input)]...) c.cursor_x-- dirty = true } case 27: if !tty.Buffered() { return "", io.EOF } r, err = tty.ReadRune() if err == nil && r == 0x5b { r, err = tty.ReadRune() if err != nil { panic(err) } switch r { case 'C': if c.cursor_x < len(c.input) { c.cursor_x++ } case 'D': if c.cursor_x > 0 { c.cursor_x-- } } } case 10: // LF break loop case 11: // CTRL-K c.input = c.input[:c.cursor_x] dirty = true case 12: // CTRL-L dirty = true case 13: // CR break loop case 21: // CTRL-U c.input = c.input[c.cursor_x:] c.cursor_x = 0 dirty = true case 23: // CTRL-W for i := len(c.input) - 1; i >= 0; i-- { if i == 0 || c.input[i] == ' ' || c.input[i] == '\t' { c.input = append(c.input[:i], c.input[c.cursor_x:]...) c.cursor_x = i dirty = true break } } default: tmp := []rune{} tmp = append(tmp, c.input[0:c.cursor_x]...) tmp = append(tmp, r) c.input = append(tmp, c.input[c.cursor_x:len(c.input)]...) c.cursor_x++ dirty = true } } os.Stdout.WriteString("\n") if c.input == nil { return "", io.EOF } return string(c.input), nil }