Revamp escape sequence processing for WSL

Also add support for alt-[0-9] and f1[12]
This commit is contained in:
Junegunn Choi 2016-11-19 22:40:28 +09:00
parent 4b332d831e
commit 8c661d4e8c
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
4 changed files with 184 additions and 243 deletions

View File

@ -330,6 +330,10 @@ func isAlphabet(char uint8) bool {
return char >= 'a' && char <= 'z' return char >= 'a' && char <= 'z'
} }
func isNumeric(char uint8) bool {
return char >= '0' && char <= '9'
}
func parseAlgo(str string) algo.Algo { func parseAlgo(str string) algo.Algo {
switch str { switch str {
case "v1": case "v1":
@ -406,11 +410,17 @@ func parseKeyChords(str string, message string) map[int]string {
chord = tui.DoubleClick chord = tui.DoubleClick
case "f10": case "f10":
chord = tui.F10 chord = tui.F10
case "f11":
chord = tui.F11
case "f12":
chord = tui.F12
default: default:
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chord = tui.CtrlA + int(lkey[5]) - 'a' chord = tui.CtrlA + int(lkey[5]) - 'a'
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
chord = tui.AltA + int(lkey[4]) - 'a' chord = tui.AltA + int(lkey[4]) - 'a'
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isNumeric(lkey[4]) {
chord = tui.Alt0 + int(lkey[4]) - '0'
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' { } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' {
chord = tui.F1 + int(key[1]) - '1' chord = tui.F1 + int(key[1]) - '1'
} else if utf8.RuneCountInString(key) == 1 { } else if utf8.RuneCountInString(key) == 1 {

View File

@ -10,8 +10,12 @@ package tui
#cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl #cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl
#cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch #cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch
SCREEN *c_newterm () { FILE* c_tty() {
return newterm(NULL, stderr, stdin); return fopen("/dev/tty", "r");
}
SCREEN* c_newterm(FILE* tty) {
return newterm(NULL, stderr, tty);
} }
*/ */
import "C" import "C"
@ -20,7 +24,6 @@ import (
"fmt" "fmt"
"os" "os"
"strings" "strings"
"syscall"
"time" "time"
"unicode/utf8" "unicode/utf8"
) )
@ -59,7 +62,6 @@ const (
) )
var ( var (
_in *os.File
_screen *C.SCREEN _screen *C.SCREEN
_colorMap map[int]ColorPair _colorMap map[int]ColorPair
_colorFn func(ColorPair, Attr) C.int _colorFn func(ColorPair, Attr) C.int
@ -81,18 +83,13 @@ func DefaultTheme() *ColorTheme {
} }
func Init(theme *ColorTheme, black bool, mouse bool) { func Init(theme *ColorTheme, black bool, mouse bool) {
{
in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0)
if err != nil {
panic("Failed to open /dev/tty")
}
_in = in
// Break STDIN
// syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd()))
}
C.setlocale(C.LC_ALL, C.CString("")) C.setlocale(C.LC_ALL, C.CString(""))
_screen = C.c_newterm() tty := C.c_tty()
if tty == nil {
fmt.Println("Failed to open /dev/tty")
os.Exit(2)
}
_screen = C.c_newterm(tty)
if _screen == nil { if _screen == nil {
fmt.Println("Invalid $TERM: " + os.Getenv("TERM")) fmt.Println("Invalid $TERM: " + os.Getenv("TERM"))
os.Exit(2) os.Exit(2)
@ -100,9 +97,14 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
C.set_term(_screen) C.set_term(_screen)
if mouse { if mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil) C.mousemask(C.ALL_MOUSE_EVENTS, nil)
C.mouseinterval(0)
} }
C.noecho() C.noecho()
C.raw() // stty dsusp undef C.raw() // stty dsusp undef
C.nonl()
C.keypad(C.stdscr, true)
C.set_escdelay(200)
C.timeout(100) // ESCDELAY 200ms + timeout 100ms
_color = theme != nil _color = theme != nil
if _color { if _color {
@ -283,246 +285,167 @@ func PairFor(fg Color, bg Color) ColorPair {
return id return id
} }
func getch(nonblock bool) int { func consume(expects ...rune) bool {
b := make([]byte, 1) for _, r := range expects {
syscall.SetNonblock(int(_in.Fd()), nonblock) if int(C.getch()) != int(r) {
_, err := _in.Read(b) return false
if err != nil {
return -1
} }
return int(b[0]) }
return true
} }
func GetBytes() []byte { func escSequence() Event {
c := getch(false) // nodelay is not thread-safe (e.g. <ESC><CTRL-P>)
retries := 0 // C.nodelay(C.stdscr, true)
if c == 27 { c := C.getch()
// Wait for additional keys after ESC for 100ms (10 * 10ms) switch c {
retries = 10 case C.ERR:
}
_buf = append(_buf, byte(c))
for {
c = getch(true)
if c == -1 {
if retries > 0 {
retries--
time.Sleep(10 * time.Millisecond)
continue
}
break
}
retries = 0
_buf = append(_buf, byte(c))
}
return _buf
}
// 27 (91 79) 77 type x y
func mouseSequence(sz *int) Event {
if len(_buf) < 6 {
return Event{Invalid, 0, nil}
}
*sz = 6
switch _buf[3] {
case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl
35, 39, 43, 51: // mouse-up / shift / cmd / ctrl
mod := _buf[3] >= 36
down := _buf[3]%2 == 0
x := int(_buf[4] - 33)
y := int(_buf[5] - 33)
double := false
if down {
now := time.Now()
if now.Sub(_prevDownTime) < doubleClickDuration {
_clickY = append(_clickY, y)
} else {
_clickY = []int{y}
}
_prevDownTime = now
} else {
if len(_clickY) > 1 && _clickY[0] == _clickY[1] &&
time.Now().Sub(_prevDownTime) < doubleClickDuration {
double = true
}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}}
case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
mod := _buf[3] >= 100
s := 1 - int(_buf[3]%2)*2
x := int(_buf[4] - 33)
y := int(_buf[5] - 33)
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}}
}
return Event{Invalid, 0, nil}
}
func escSequence(sz *int) Event {
if len(_buf) < 2 {
return Event{ESC, 0, nil} return Event{ESC, 0, nil}
} case CtrlM:
*sz = 2
switch _buf[1] {
case 13:
return Event{AltEnter, 0, nil} return Event{AltEnter, 0, nil}
case 32: case '/':
return Event{AltSpace, 0, nil}
case 47:
return Event{AltSlash, 0, nil} return Event{AltSlash, 0, nil}
case 98: case ' ':
return Event{AltB, 0, nil} return Event{AltSpace, 0, nil}
case 100: case 127, C.KEY_BACKSPACE:
return Event{AltD, 0, nil}
case 102:
return Event{AltF, 0, nil}
case 127:
return Event{AltBS, 0, nil} return Event{AltBS, 0, nil}
case 91, 79: case '[':
if len(_buf) < 3 { // Bracketed paste mode (printf "\e[?2004h")
// \e[200~ TEXT \e[201~
if consume('2', '0', '0', '~') {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
*sz = 3
switch _buf[2] {
case 68:
return Event{Left, 0, nil}
case 67:
return Event{Right, 0, nil}
case 66:
return Event{Down, 0, nil}
case 65:
return Event{Up, 0, nil}
case 90:
return Event{BTab, 0, nil}
case 72:
return Event{Home, 0, nil}
case 70:
return Event{End, 0, nil}
case 77:
return mouseSequence(sz)
case 80:
return Event{F1, 0, nil}
case 81:
return Event{F2, 0, nil}
case 82:
return Event{F3, 0, nil}
case 83:
return Event{F4, 0, nil}
case 49, 50, 51, 52, 53, 54:
if len(_buf) < 4 {
return Event{Invalid, 0, nil}
} }
*sz = 4 if c >= 'a' && c <= 'z' {
switch _buf[2] { return Event{AltA + int(c) - 'a', 0, nil}
case 50:
if len(_buf) == 5 && _buf[4] == 126 {
*sz = 5
switch _buf[3] {
case 48:
return Event{F9, 0, nil}
case 49:
return Event{F10, 0, nil}
} }
if c >= '0' && c <= '9' {
return Event{Alt0 + int(c) - '0', 0, nil}
} }
// Bracketed paste mode \e[200~ / \e[201
if _buf[3] == 48 && (_buf[4] == 48 || _buf[4] == 49) && _buf[5] == 126 { // Don't care. Ignore the rest.
*sz = 6 for ; c != C.ERR; c = C.getch() {
return Event{Invalid, 0, nil}
}
return Event{Invalid, 0, nil} // INS
case 51:
return Event{Del, 0, nil}
case 52:
return Event{End, 0, nil}
case 53:
return Event{PgUp, 0, nil}
case 54:
return Event{PgDn, 0, nil}
case 49:
switch _buf[3] {
case 126:
return Event{Home, 0, nil}
case 53, 55, 56, 57:
if len(_buf) == 5 && _buf[4] == 126 {
*sz = 5
switch _buf[3] {
case 53:
return Event{F5, 0, nil}
case 55:
return Event{F6, 0, nil}
case 56:
return Event{F7, 0, nil}
case 57:
return Event{F8, 0, nil}
}
}
return Event{Invalid, 0, nil}
case 59:
if len(_buf) != 6 {
return Event{Invalid, 0, nil}
}
*sz = 6
switch _buf[4] {
case 50:
switch _buf[5] {
case 68:
return Event{Home, 0, nil}
case 67:
return Event{End, 0, nil}
}
case 53:
switch _buf[5] {
case 68:
return Event{SLeft, 0, nil}
case 67:
return Event{SRight, 0, nil}
}
} // _buf[4]
} // _buf[3]
} // _buf[2]
} // _buf[2]
} // _buf[1]
if _buf[1] >= 'a' && _buf[1] <= 'z' {
return Event{AltA + int(_buf[1]) - 'a', 0, nil}
} }
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
func GetChar() Event { func GetChar() Event {
if len(_buf) == 0 { c := C.getch()
_buf = GetBytes() switch c {
case C.ERR:
return Event{Invalid, 0, nil}
case C.KEY_UP:
return Event{Up, 0, nil}
case C.KEY_DOWN:
return Event{Down, 0, nil}
case C.KEY_LEFT:
return Event{Left, 0, nil}
case C.KEY_RIGHT:
return Event{Right, 0, nil}
case C.KEY_HOME:
return Event{Home, 0, nil}
case C.KEY_END:
return Event{End, 0, nil}
case C.KEY_BACKSPACE:
return Event{BSpace, 0, nil}
case C.KEY_F0 + 1:
return Event{F1, 0, nil}
case C.KEY_F0 + 2:
return Event{F2, 0, nil}
case C.KEY_F0 + 3:
return Event{F3, 0, nil}
case C.KEY_F0 + 4:
return Event{F4, 0, nil}
case C.KEY_F0 + 5:
return Event{F5, 0, nil}
case C.KEY_F0 + 6:
return Event{F6, 0, nil}
case C.KEY_F0 + 7:
return Event{F7, 0, nil}
case C.KEY_F0 + 8:
return Event{F8, 0, nil}
case C.KEY_F0 + 9:
return Event{F9, 0, nil}
case C.KEY_F0 + 10:
return Event{F10, 0, nil}
case C.KEY_F0 + 11:
return Event{F11, 0, nil}
case C.KEY_F0 + 12:
return Event{F12, 0, nil}
case C.KEY_DC:
return Event{Del, 0, nil}
case C.KEY_PPAGE:
return Event{PgUp, 0, nil}
case C.KEY_NPAGE:
return Event{PgDn, 0, nil}
case C.KEY_BTAB:
return Event{BTab, 0, nil}
case C.KEY_ENTER:
return Event{CtrlM, 0, nil}
case C.KEY_SLEFT:
return Event{SLeft, 0, nil}
case C.KEY_SRIGHT:
return Event{SRight, 0, nil}
case C.KEY_MOUSE:
var me C.MEVENT
if C.getmouse(&me) != C.ERR {
mod := ((me.bstate & C.BUTTON_SHIFT) | (me.bstate & C.BUTTON_CTRL) | (me.bstate & C.BUTTON_ALT)) > 0
x := int(me.x)
y := int(me.y)
/* Cannot use BUTTON1_DOUBLE_CLICKED due to mouseinterval(0) */
if (me.bstate & C.BUTTON1_PRESSED) > 0 {
now := time.Now()
if now.Sub(_prevDownTime) < doubleClickDuration {
_clickY = append(_clickY, y)
} else {
_clickY = []int{y}
_prevDownTime = now
} }
if len(_buf) == 0 { return Event{Mouse, 0, &MouseEvent{y, x, 0, true, false, mod}}
panic("Empty _buffer") } else if (me.bstate & C.BUTTON1_RELEASED) > 0 {
double := false
if len(_clickY) > 1 && _clickY[0] == _clickY[1] &&
time.Now().Sub(_prevDownTime) < doubleClickDuration {
double = true
} }
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, double, mod}}
sz := 1 } else if (me.bstate&0x8000000) > 0 || (me.bstate&0x80) > 0 {
defer func() { return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}}
_buf = _buf[sz:] } else if (me.bstate & C.BUTTON4_PRESSED) > 0 {
}() return Event{Mouse, 0, &MouseEvent{y, x, 1, false, false, mod}}
}
switch _buf[0] { }
case CtrlC: return Event{Invalid, 0, nil}
return Event{CtrlC, 0, nil} case C.KEY_RESIZE:
case CtrlG: return Event{Invalid, 0, nil}
return Event{CtrlG, 0, nil} case ESC:
case CtrlQ: return escSequence()
return Event{CtrlQ, 0, nil}
case 127: case 127:
return Event{BSpace, 0, nil} return Event{BSpace, 0, nil}
case ESC: }
return escSequence(&sz) // CTRL-A ~ CTRL-Z
if c >= CtrlA && c <= CtrlZ {
return Event{int(c), 0, nil}
} }
// CTRL-A ~ CTRL-Z // Multi-byte character
if _buf[0] <= CtrlZ { buffer := []byte{byte(c)}
return Event{int(_buf[0]), 0, nil} for {
} r, _ := utf8.DecodeRune(buffer)
r, rsz := utf8.DecodeRune(_buf) if r != utf8.RuneError {
if r == utf8.RuneError {
return Event{ESC, 0, nil}
}
sz = rsz
return Event{Rune, r, nil} return Event{Rune, r, nil}
}
c := C.getch()
if c == C.ERR {
break
}
if c >= C.KEY_CODE_YES {
C.ungetch(c)
break
}
buffer = append(buffer, byte(c))
}
return Event{Invalid, 0, nil}
} }

View File

@ -207,8 +207,8 @@ func GetChar() Event {
_clickY = append(_clickY, x) _clickY = append(_clickY, x)
} else { } else {
_clickY = []int{x} _clickY = []int{x}
}
_prevDownTime = now _prevDownTime = now
}
} else { } else {
if len(_clickY) > 1 && _clickY[0] == _clickY[1] && if len(_clickY) > 1 && _clickY[0] == _clickY[1] &&
time.Now().Sub(_prevDownTime) < doubleClickDuration { time.Now().Sub(_prevDownTime) < doubleClickDuration {
@ -326,9 +326,9 @@ func GetChar() Event {
case tcell.KeyF10: case tcell.KeyF10:
return Event{F10, 0, nil} return Event{F10, 0, nil}
case tcell.KeyF11: case tcell.KeyF11:
return Event{Invalid, 0, nil} return Event{F11, 0, nil}
case tcell.KeyF12: case tcell.KeyF12:
return Event{Invalid, 0, nil} return Event{F12, 0, nil}
// ev.Ch doesn't work for some reason for space: // ev.Ch doesn't work for some reason for space:
case tcell.KeyRune: case tcell.KeyRune:
@ -343,6 +343,9 @@ func GetChar() Event {
if r >= 'a' && r <= 'z' { if r >= 'a' && r <= 'z' {
return Event{AltA + int(r) - 'a', 0, nil} return Event{AltA + int(r) - 'a', 0, nil}
} }
if r >= '0' && r <= '9' {
return Event{Alt0 + int(r) - '0', 0, nil}
}
} }
return Event{Rune, r, nil} return Event{Rune, r, nil}

View File

@ -67,18 +67,24 @@ const (
F8 F8
F9 F9
F10 F10
F11
F12
AltEnter AltEnter
AltSpace AltSpace
AltSlash AltSlash
AltBS AltBS
AltA
Alt0
)
const ( // Reset iota
AltA = Alt0 + 'a' - '0' + iota
AltB AltB
AltC AltC
AltD AltD
AltE AltE
AltF AltF
AltZ = AltA + 'z' - 'a' AltZ = AltA + 'z' - 'a'
) )
@ -136,7 +142,6 @@ type MouseEvent struct {
} }
var ( var (
_buf []byte
_color bool _color bool
_prevDownTime time.Time _prevDownTime time.Time
_clickY []int _clickY []int