| 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 |
| } |