package liner import ( "bufio" "os" "syscall" "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 int16 ControlKeyState uint32 } // These names are from the Win32 api, so they use underscores (contrary to // what golint suggests) const ( vk_tab = 0x09 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)) 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) if s.columns > 1 { s.columns-- } return winch, nil } if input.eventType != key_event { continue } ke := (*key_event_record)(unsafe.Pointer(&input.blob[0])) if ke.KeyDown == 0 { continue } if ke.VirtualKeyCode == vk_tab && ke.ControlKeyState&modKeys == shiftPressed { s.key = shiftTab } 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 { 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 }