369 lines
8.9 KiB
Go
369 lines
8.9 KiB
Go
package liner
|
|
|
|
import (
|
|
"bufio"
|
|
"os"
|
|
"syscall"
|
|
"unicode/utf16"
|
|
"unsafe"
|
|
)
|
|
|
|
var (
|
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
|
|
|
procGetStdHandle = kernel32.NewProc("GetStdHandle")
|
|
procReadConsoleInput = kernel32.NewProc("ReadConsoleInputW")
|
|
procGetNumberOfConsoleInputEvents = kernel32.NewProc("GetNumberOfConsoleInputEvents")
|
|
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
|
|
procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
|
|
procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
|
|
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
|
|
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
|
|
)
|
|
|
|
// These names are from the Win32 api, so they use underscores (contrary to
|
|
// what golint suggests)
|
|
const (
|
|
std_input_handle = uint32(-10 & 0xFFFFFFFF)
|
|
std_output_handle = uint32(-11 & 0xFFFFFFFF)
|
|
std_error_handle = uint32(-12 & 0xFFFFFFFF)
|
|
invalid_handle_value = ^uintptr(0)
|
|
)
|
|
|
|
type inputMode uint32
|
|
|
|
// State represents an open terminal
|
|
type State struct {
|
|
commonState
|
|
handle syscall.Handle
|
|
hOut syscall.Handle
|
|
origMode inputMode
|
|
defaultMode inputMode
|
|
key interface{}
|
|
repeat uint16
|
|
}
|
|
|
|
const (
|
|
enableEchoInput = 0x4
|
|
enableInsertMode = 0x20
|
|
enableLineInput = 0x2
|
|
enableMouseInput = 0x10
|
|
enableProcessedInput = 0x1
|
|
enableQuickEditMode = 0x40
|
|
enableWindowInput = 0x8
|
|
)
|
|
|
|
// NewLiner initializes a new *State, and sets the terminal into raw mode. To
|
|
// restore the terminal to its previous state, call State.Close().
|
|
func NewLiner() *State {
|
|
var s State
|
|
hIn, _, _ := procGetStdHandle.Call(uintptr(std_input_handle))
|
|
s.handle = syscall.Handle(hIn)
|
|
hOut, _, _ := procGetStdHandle.Call(uintptr(std_output_handle))
|
|
s.hOut = syscall.Handle(hOut)
|
|
|
|
s.terminalSupported = true
|
|
if m, err := TerminalMode(); err == nil {
|
|
s.origMode = m.(inputMode)
|
|
mode := s.origMode
|
|
mode &^= enableEchoInput
|
|
mode &^= enableInsertMode
|
|
mode &^= enableLineInput
|
|
mode &^= enableMouseInput
|
|
mode |= enableWindowInput
|
|
mode.ApplyMode()
|
|
} else {
|
|
s.inputRedirected = true
|
|
s.r = bufio.NewReader(os.Stdin)
|
|
}
|
|
|
|
s.getColumns()
|
|
s.outputRedirected = s.columns <= 0
|
|
|
|
return &s
|
|
}
|
|
|
|
// These names are from the Win32 api, so they use underscores (contrary to
|
|
// what golint suggests)
|
|
const (
|
|
focus_event = 0x0010
|
|
key_event = 0x0001
|
|
menu_event = 0x0008
|
|
mouse_event = 0x0002
|
|
window_buffer_size_event = 0x0004
|
|
)
|
|
|
|
type input_record struct {
|
|
eventType uint16
|
|
pad uint16
|
|
blob [16]byte
|
|
}
|
|
|
|
type key_event_record struct {
|
|
KeyDown int32
|
|
RepeatCount uint16
|
|
VirtualKeyCode uint16
|
|
VirtualScanCode uint16
|
|
Char uint16
|
|
ControlKeyState uint32
|
|
}
|
|
|
|
// These names are from the Win32 api, so they use underscores (contrary to
|
|
// what golint suggests)
|
|
const (
|
|
vk_back = 0x08
|
|
vk_tab = 0x09
|
|
vk_menu = 0x12 // ALT key
|
|
vk_prior = 0x21
|
|
vk_next = 0x22
|
|
vk_end = 0x23
|
|
vk_home = 0x24
|
|
vk_left = 0x25
|
|
vk_up = 0x26
|
|
vk_right = 0x27
|
|
vk_down = 0x28
|
|
vk_insert = 0x2d
|
|
vk_delete = 0x2e
|
|
vk_f1 = 0x70
|
|
vk_f2 = 0x71
|
|
vk_f3 = 0x72
|
|
vk_f4 = 0x73
|
|
vk_f5 = 0x74
|
|
vk_f6 = 0x75
|
|
vk_f7 = 0x76
|
|
vk_f8 = 0x77
|
|
vk_f9 = 0x78
|
|
vk_f10 = 0x79
|
|
vk_f11 = 0x7a
|
|
vk_f12 = 0x7b
|
|
bKey = 0x42
|
|
dKey = 0x44
|
|
fKey = 0x46
|
|
yKey = 0x59
|
|
)
|
|
|
|
const (
|
|
shiftPressed = 0x0010
|
|
leftAltPressed = 0x0002
|
|
leftCtrlPressed = 0x0008
|
|
rightAltPressed = 0x0001
|
|
rightCtrlPressed = 0x0004
|
|
|
|
modKeys = shiftPressed | leftAltPressed | rightAltPressed | leftCtrlPressed | rightCtrlPressed
|
|
)
|
|
|
|
// inputWaiting only returns true if the next call to readNext will return immediately.
|
|
func (s *State) inputWaiting() bool {
|
|
var num uint32
|
|
ok, _, _ := procGetNumberOfConsoleInputEvents.Call(uintptr(s.handle), uintptr(unsafe.Pointer(&num)))
|
|
if ok == 0 {
|
|
// call failed, so we cannot guarantee a non-blocking readNext
|
|
return false
|
|
}
|
|
|
|
// during a "paste" input events are always an odd number, and
|
|
// the last one results in a blocking readNext, so return false
|
|
// when num is 1 or 0.
|
|
return num > 1
|
|
}
|
|
|
|
func (s *State) readNext() (interface{}, error) {
|
|
if s.repeat > 0 {
|
|
s.repeat--
|
|
return s.key, nil
|
|
}
|
|
|
|
var input input_record
|
|
pbuf := uintptr(unsafe.Pointer(&input))
|
|
var rv uint32
|
|
prv := uintptr(unsafe.Pointer(&rv))
|
|
|
|
var surrogate uint16
|
|
|
|
for {
|
|
ok, _, err := procReadConsoleInput.Call(uintptr(s.handle), pbuf, 1, prv)
|
|
|
|
if ok == 0 {
|
|
return nil, err
|
|
}
|
|
|
|
if input.eventType == window_buffer_size_event {
|
|
xy := (*coord)(unsafe.Pointer(&input.blob[0]))
|
|
s.columns = int(xy.x)
|
|
return winch, nil
|
|
}
|
|
if input.eventType != key_event {
|
|
continue
|
|
}
|
|
ke := (*key_event_record)(unsafe.Pointer(&input.blob[0]))
|
|
if ke.KeyDown == 0 {
|
|
if ke.VirtualKeyCode == vk_menu && ke.Char > 0 {
|
|
// paste of unicode (eg. via ALT-numpad)
|
|
if surrogate > 0 {
|
|
return utf16.DecodeRune(rune(surrogate), rune(ke.Char)), nil
|
|
} else if utf16.IsSurrogate(rune(ke.Char)) {
|
|
surrogate = ke.Char
|
|
continue
|
|
} else {
|
|
return rune(ke.Char), nil
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
if ke.VirtualKeyCode == vk_tab && ke.ControlKeyState&modKeys == shiftPressed {
|
|
s.key = shiftTab
|
|
} else if ke.VirtualKeyCode == vk_back && (ke.ControlKeyState&modKeys == leftAltPressed ||
|
|
ke.ControlKeyState&modKeys == rightAltPressed) {
|
|
s.key = altBs
|
|
} else if ke.VirtualKeyCode == bKey && (ke.ControlKeyState&modKeys == leftAltPressed ||
|
|
ke.ControlKeyState&modKeys == rightAltPressed) {
|
|
s.key = altB
|
|
} else if ke.VirtualKeyCode == dKey && (ke.ControlKeyState&modKeys == leftAltPressed ||
|
|
ke.ControlKeyState&modKeys == rightAltPressed) {
|
|
s.key = altD
|
|
} else if ke.VirtualKeyCode == fKey && (ke.ControlKeyState&modKeys == leftAltPressed ||
|
|
ke.ControlKeyState&modKeys == rightAltPressed) {
|
|
s.key = altF
|
|
} else if ke.VirtualKeyCode == yKey && (ke.ControlKeyState&modKeys == leftAltPressed ||
|
|
ke.ControlKeyState&modKeys == rightAltPressed) {
|
|
s.key = altY
|
|
} else if ke.Char > 0 {
|
|
if surrogate > 0 {
|
|
s.key = utf16.DecodeRune(rune(surrogate), rune(ke.Char))
|
|
} else if utf16.IsSurrogate(rune(ke.Char)) {
|
|
surrogate = ke.Char
|
|
continue
|
|
} else {
|
|
s.key = rune(ke.Char)
|
|
}
|
|
} else {
|
|
switch ke.VirtualKeyCode {
|
|
case vk_prior:
|
|
s.key = pageUp
|
|
case vk_next:
|
|
s.key = pageDown
|
|
case vk_end:
|
|
s.key = end
|
|
case vk_home:
|
|
s.key = home
|
|
case vk_left:
|
|
s.key = left
|
|
if ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) != 0 {
|
|
if ke.ControlKeyState&modKeys == ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) {
|
|
s.key = wordLeft
|
|
}
|
|
}
|
|
case vk_right:
|
|
s.key = right
|
|
if ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) != 0 {
|
|
if ke.ControlKeyState&modKeys == ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) {
|
|
s.key = wordRight
|
|
}
|
|
}
|
|
case vk_up:
|
|
s.key = up
|
|
case vk_down:
|
|
s.key = down
|
|
case vk_insert:
|
|
s.key = insert
|
|
case vk_delete:
|
|
s.key = del
|
|
case vk_f1:
|
|
s.key = f1
|
|
case vk_f2:
|
|
s.key = f2
|
|
case vk_f3:
|
|
s.key = f3
|
|
case vk_f4:
|
|
s.key = f4
|
|
case vk_f5:
|
|
s.key = f5
|
|
case vk_f6:
|
|
s.key = f6
|
|
case vk_f7:
|
|
s.key = f7
|
|
case vk_f8:
|
|
s.key = f8
|
|
case vk_f9:
|
|
s.key = f9
|
|
case vk_f10:
|
|
s.key = f10
|
|
case vk_f11:
|
|
s.key = f11
|
|
case vk_f12:
|
|
s.key = f12
|
|
default:
|
|
// Eat modifier keys
|
|
// TODO: return Action(Unknown) if the key isn't a
|
|
// modifier.
|
|
continue
|
|
}
|
|
}
|
|
|
|
if ke.RepeatCount > 1 {
|
|
s.repeat = ke.RepeatCount - 1
|
|
}
|
|
return s.key, nil
|
|
}
|
|
}
|
|
|
|
// Close returns the terminal to its previous mode
|
|
func (s *State) Close() error {
|
|
s.origMode.ApplyMode()
|
|
return nil
|
|
}
|
|
|
|
func (s *State) startPrompt() {
|
|
if m, err := TerminalMode(); err == nil {
|
|
s.defaultMode = m.(inputMode)
|
|
mode := s.defaultMode
|
|
mode &^= enableProcessedInput
|
|
mode.ApplyMode()
|
|
}
|
|
}
|
|
|
|
func (s *State) restartPrompt() {
|
|
}
|
|
|
|
func (s *State) stopPrompt() {
|
|
s.defaultMode.ApplyMode()
|
|
}
|
|
|
|
// TerminalSupported returns true because line editing is always
|
|
// supported on Windows.
|
|
func TerminalSupported() bool {
|
|
return true
|
|
}
|
|
|
|
func (mode inputMode) ApplyMode() error {
|
|
hIn, _, err := procGetStdHandle.Call(uintptr(std_input_handle))
|
|
if hIn == invalid_handle_value || hIn == 0 {
|
|
return err
|
|
}
|
|
ok, _, err := procSetConsoleMode.Call(hIn, uintptr(mode))
|
|
if ok != 0 {
|
|
err = nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// TerminalMode returns the current terminal input mode as an InputModeSetter.
|
|
//
|
|
// This function is provided for convenience, and should
|
|
// not be necessary for most users of liner.
|
|
func TerminalMode() (ModeApplier, error) {
|
|
var mode inputMode
|
|
hIn, _, err := procGetStdHandle.Call(uintptr(std_input_handle))
|
|
if hIn == invalid_handle_value || hIn == 0 {
|
|
return nil, err
|
|
}
|
|
ok, _, err := procGetConsoleMode.Call(hIn, uintptr(unsafe.Pointer(&mode)))
|
|
if ok != 0 {
|
|
err = nil
|
|
}
|
|
return mode, err
|
|
}
|
|
|
|
const cursorColumn = true
|