blob: 5efa7ac7389c85ce15492bad4c6f35e14b77a2f3 [file] [log] [blame]
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
}