//go:build windows // +build windows package input import ( "errors" "fmt" "strings" "time" "unicode" "unicode/utf16" "unicode/utf8" "github.com/charmbracelet/x/ansi" xwindows "github.com/charmbracelet/x/windows" "github.com/muesli/cancelreader" "golang.org/x/sys/windows" ) // ReadEvents reads input events from the terminal. // // It reads the events available in the input buffer and returns them. func (d *Reader) ReadEvents() ([]Event, error) { events, err := d.handleConInput() if errors.Is(err, errNotConInputReader) { return d.readEvents() } return events, err } var errNotConInputReader = fmt.Errorf("handleConInput: not a conInputReader") func (d *Reader) handleConInput() ([]Event, error) { cc, ok := d.rd.(*conInputReader) if !ok { return nil, errNotConInputReader } var ( events []xwindows.InputRecord err error ) for { // Peek up to 256 events, this is to allow for sequences events reported as // key events. events, err = peekNConsoleInputs(cc.conin, 256) if cc.isCanceled() { return nil, cancelreader.ErrCanceled } if err != nil { return nil, fmt.Errorf("peek coninput events: %w", err) } if len(events) > 0 { break } // Sleep for a bit to avoid busy waiting. time.Sleep(10 * time.Millisecond) } events, err = readNConsoleInputs(cc.conin, uint32(len(events))) if cc.isCanceled() { return nil, cancelreader.ErrCanceled } if err != nil { return nil, fmt.Errorf("read coninput events: %w", err) } var evs []Event for _, event := range events { if e := d.parser.parseConInputEvent(event, &d.keyState); e != nil { if multi, ok := e.(MultiEvent); ok { evs = append(evs, multi...) } else { evs = append(evs, e) } } } return evs, nil } func (p *Parser) parseConInputEvent(event xwindows.InputRecord, keyState *win32InputState) Event { switch event.EventType { case xwindows.KEY_EVENT: kevent := event.KeyEvent() return p.parseWin32InputKeyEvent(keyState, kevent.VirtualKeyCode, kevent.VirtualScanCode, kevent.Char, kevent.KeyDown, kevent.ControlKeyState, kevent.RepeatCount) case xwindows.WINDOW_BUFFER_SIZE_EVENT: wevent := event.WindowBufferSizeEvent() if wevent.Size.X != keyState.lastWinsizeX || wevent.Size.Y != keyState.lastWinsizeY { keyState.lastWinsizeX, keyState.lastWinsizeY = wevent.Size.X, wevent.Size.Y return WindowSizeEvent{ Width: int(wevent.Size.X), Height: int(wevent.Size.Y), } } case xwindows.MOUSE_EVENT: mevent := event.MouseEvent() Event := mouseEvent(keyState.lastMouseBtns, mevent) keyState.lastMouseBtns = mevent.ButtonState return Event case xwindows.FOCUS_EVENT: fevent := event.FocusEvent() if fevent.SetFocus { return FocusEvent{} } return BlurEvent{} case xwindows.MENU_EVENT: // ignore } return nil } func mouseEventButton(p, s uint32) (MouseButton, bool) { var isRelease bool button := MouseNone btn := p ^ s if btn&s == 0 { isRelease = true } if btn == 0 { switch { case s&xwindows.FROM_LEFT_1ST_BUTTON_PRESSED > 0: button = MouseLeft case s&xwindows.FROM_LEFT_2ND_BUTTON_PRESSED > 0: button = MouseMiddle case s&xwindows.RIGHTMOST_BUTTON_PRESSED > 0: button = MouseRight case s&xwindows.FROM_LEFT_3RD_BUTTON_PRESSED > 0: button = MouseBackward case s&xwindows.FROM_LEFT_4TH_BUTTON_PRESSED > 0: button = MouseForward } return button, isRelease } switch btn { case xwindows.FROM_LEFT_1ST_BUTTON_PRESSED: // left button button = MouseLeft case xwindows.RIGHTMOST_BUTTON_PRESSED: // right button button = MouseRight case xwindows.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button button = MouseMiddle case xwindows.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward) button = MouseBackward case xwindows.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward) button = MouseForward } return button, isRelease } func mouseEvent(p uint32, e xwindows.MouseEventRecord) (ev Event) { var mod KeyMod var isRelease bool if e.ControlKeyState&(xwindows.LEFT_ALT_PRESSED|xwindows.RIGHT_ALT_PRESSED) != 0 { mod |= ModAlt } if e.ControlKeyState&(xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_CTRL_PRESSED) != 0 { mod |= ModCtrl } if e.ControlKeyState&(xwindows.SHIFT_PRESSED) != 0 { mod |= ModShift } m := Mouse{ X: int(e.MousePositon.X), Y: int(e.MousePositon.Y), Mod: mod, } wheelDirection := int16(highWord(e.ButtonState)) //nolint:gosec switch e.EventFlags { case 0, xwindows.DOUBLE_CLICK: m.Button, isRelease = mouseEventButton(p, e.ButtonState) case xwindows.MOUSE_WHEELED: if wheelDirection > 0 { m.Button = MouseWheelUp } else { m.Button = MouseWheelDown } case xwindows.MOUSE_HWHEELED: if wheelDirection > 0 { m.Button = MouseWheelRight } else { m.Button = MouseWheelLeft } case xwindows.MOUSE_MOVED: m.Button, _ = mouseEventButton(p, e.ButtonState) return MouseMotionEvent(m) } if isWheel(m.Button) { return MouseWheelEvent(m) } else if isRelease { return MouseReleaseEvent(m) } return MouseClickEvent(m) } func highWord(data uint32) uint16 { return uint16((data & 0xFFFF0000) >> 16) //nolint:gosec } func readNConsoleInputs(console windows.Handle, maxEvents uint32) ([]xwindows.InputRecord, error) { if maxEvents == 0 { return nil, fmt.Errorf("maxEvents cannot be zero") } records := make([]xwindows.InputRecord, maxEvents) n, err := readConsoleInput(console, records) return records[:n], err } func readConsoleInput(console windows.Handle, inputRecords []xwindows.InputRecord) (uint32, error) { if len(inputRecords) == 0 { return 0, fmt.Errorf("size of input record buffer cannot be zero") } var read uint32 err := xwindows.ReadConsoleInput(console, &inputRecords[0], uint32(len(inputRecords)), &read) //nolint:gosec return read, err //nolint:wrapcheck } func peekConsoleInput(console windows.Handle, inputRecords []xwindows.InputRecord) (uint32, error) { if len(inputRecords) == 0 { return 0, fmt.Errorf("size of input record buffer cannot be zero") } var read uint32 err := xwindows.PeekConsoleInput(console, &inputRecords[0], uint32(len(inputRecords)), &read) //nolint:gosec return read, err //nolint:wrapcheck } func peekNConsoleInputs(console windows.Handle, maxEvents uint32) ([]xwindows.InputRecord, error) { if maxEvents == 0 { return nil, fmt.Errorf("maxEvents cannot be zero") } records := make([]xwindows.InputRecord, maxEvents) n, err := peekConsoleInput(console, records) return records[:n], err } // parseWin32InputKeyEvent parses a single key event from either the Windows // Console API or win32-input-mode events. When state is nil, it means this is // an event from win32-input-mode. Otherwise, it's a key event from the Windows // Console API and needs a state to decode ANSI escape sequences and utf16 // runes. func (p *Parser) parseWin32InputKeyEvent(state *win32InputState, vkc uint16, _ uint16, r rune, keyDown bool, cks uint32, repeatCount uint16) (event Event) { defer func() { // Respect the repeat count. if repeatCount > 1 { var multi MultiEvent for i := 0; i < int(repeatCount); i++ { multi = append(multi, event) } event = multi } }() if state != nil { defer func() { state.lastCks = cks }() } var utf8Buf [utf8.UTFMax]byte var key Key if state != nil && state.utf16Half { state.utf16Half = false state.utf16Buf[1] = r codepoint := utf16.DecodeRune(state.utf16Buf[0], state.utf16Buf[1]) rw := utf8.EncodeRune(utf8Buf[:], codepoint) r, _ = utf8.DecodeRune(utf8Buf[:rw]) key.Code = r key.Text = string(r) key.Mod = translateControlKeyState(cks) key = ensureKeyCase(key, cks) if keyDown { return KeyPressEvent(key) } return KeyReleaseEvent(key) } var baseCode rune switch { case vkc == 0: // Zero means this event is either an escape code or a unicode // codepoint. if state != nil && state.ansiIdx == 0 && r != ansi.ESC { // This is a unicode codepoint. baseCode = r break } if state != nil { // Collect ANSI escape code. state.ansiBuf[state.ansiIdx] = byte(r) state.ansiIdx++ if state.ansiIdx <= 2 { // We haven't received enough bytes to determine if this is an // ANSI escape code. return nil } if r == ansi.ESC { // We're expecting a closing String Terminator [ansi.ST]. return nil } n, event := p.parseSequence(state.ansiBuf[:state.ansiIdx]) if n == 0 { return nil } if _, ok := event.(UnknownEvent); ok { return nil } state.ansiIdx = 0 return event } case vkc == xwindows.VK_BACK: baseCode = KeyBackspace case vkc == xwindows.VK_TAB: baseCode = KeyTab case vkc == xwindows.VK_RETURN: baseCode = KeyEnter case vkc == xwindows.VK_SHIFT: //nolint:nestif if cks&xwindows.SHIFT_PRESSED != 0 { if cks&xwindows.ENHANCED_KEY != 0 { baseCode = KeyRightShift } else { baseCode = KeyLeftShift } } else if state != nil { if state.lastCks&xwindows.SHIFT_PRESSED != 0 { if state.lastCks&xwindows.ENHANCED_KEY != 0 { baseCode = KeyRightShift } else { baseCode = KeyLeftShift } } } case vkc == xwindows.VK_CONTROL: if cks&xwindows.LEFT_CTRL_PRESSED != 0 { baseCode = KeyLeftCtrl } else if cks&xwindows.RIGHT_CTRL_PRESSED != 0 { baseCode = KeyRightCtrl } else if state != nil { if state.lastCks&xwindows.LEFT_CTRL_PRESSED != 0 { baseCode = KeyLeftCtrl } else if state.lastCks&xwindows.RIGHT_CTRL_PRESSED != 0 { baseCode = KeyRightCtrl } } case vkc == xwindows.VK_MENU: if cks&xwindows.LEFT_ALT_PRESSED != 0 { baseCode = KeyLeftAlt } else if cks&xwindows.RIGHT_ALT_PRESSED != 0 { baseCode = KeyRightAlt } else if state != nil { if state.lastCks&xwindows.LEFT_ALT_PRESSED != 0 { baseCode = KeyLeftAlt } else if state.lastCks&xwindows.RIGHT_ALT_PRESSED != 0 { baseCode = KeyRightAlt } } case vkc == xwindows.VK_PAUSE: baseCode = KeyPause case vkc == xwindows.VK_CAPITAL: baseCode = KeyCapsLock case vkc == xwindows.VK_ESCAPE: baseCode = KeyEscape case vkc == xwindows.VK_SPACE: baseCode = KeySpace case vkc == xwindows.VK_PRIOR: baseCode = KeyPgUp case vkc == xwindows.VK_NEXT: baseCode = KeyPgDown case vkc == xwindows.VK_END: baseCode = KeyEnd case vkc == xwindows.VK_HOME: baseCode = KeyHome case vkc == xwindows.VK_LEFT: baseCode = KeyLeft case vkc == xwindows.VK_UP: baseCode = KeyUp case vkc == xwindows.VK_RIGHT: baseCode = KeyRight case vkc == xwindows.VK_DOWN: baseCode = KeyDown case vkc == xwindows.VK_SELECT: baseCode = KeySelect case vkc == xwindows.VK_SNAPSHOT: baseCode = KeyPrintScreen case vkc == xwindows.VK_INSERT: baseCode = KeyInsert case vkc == xwindows.VK_DELETE: baseCode = KeyDelete case vkc >= '0' && vkc <= '9': baseCode = rune(vkc) case vkc >= 'A' && vkc <= 'Z': // Convert to lowercase. baseCode = rune(vkc) + 32 case vkc == xwindows.VK_LWIN: baseCode = KeyLeftSuper case vkc == xwindows.VK_RWIN: baseCode = KeyRightSuper case vkc == xwindows.VK_APPS: baseCode = KeyMenu case vkc >= xwindows.VK_NUMPAD0 && vkc <= xwindows.VK_NUMPAD9: baseCode = rune(vkc-xwindows.VK_NUMPAD0) + KeyKp0 case vkc == xwindows.VK_MULTIPLY: baseCode = KeyKpMultiply case vkc == xwindows.VK_ADD: baseCode = KeyKpPlus case vkc == xwindows.VK_SEPARATOR: baseCode = KeyKpComma case vkc == xwindows.VK_SUBTRACT: baseCode = KeyKpMinus case vkc == xwindows.VK_DECIMAL: baseCode = KeyKpDecimal case vkc == xwindows.VK_DIVIDE: baseCode = KeyKpDivide case vkc >= xwindows.VK_F1 && vkc <= xwindows.VK_F24: baseCode = rune(vkc-xwindows.VK_F1) + KeyF1 case vkc == xwindows.VK_NUMLOCK: baseCode = KeyNumLock case vkc == xwindows.VK_SCROLL: baseCode = KeyScrollLock case vkc == xwindows.VK_LSHIFT: baseCode = KeyLeftShift case vkc == xwindows.VK_RSHIFT: baseCode = KeyRightShift case vkc == xwindows.VK_LCONTROL: baseCode = KeyLeftCtrl case vkc == xwindows.VK_RCONTROL: baseCode = KeyRightCtrl case vkc == xwindows.VK_LMENU: baseCode = KeyLeftAlt case vkc == xwindows.VK_RMENU: baseCode = KeyRightAlt case vkc == xwindows.VK_VOLUME_MUTE: baseCode = KeyMute case vkc == xwindows.VK_VOLUME_DOWN: baseCode = KeyLowerVol case vkc == xwindows.VK_VOLUME_UP: baseCode = KeyRaiseVol case vkc == xwindows.VK_MEDIA_NEXT_TRACK: baseCode = KeyMediaNext case vkc == xwindows.VK_MEDIA_PREV_TRACK: baseCode = KeyMediaPrev case vkc == xwindows.VK_MEDIA_STOP: baseCode = KeyMediaStop case vkc == xwindows.VK_MEDIA_PLAY_PAUSE: baseCode = KeyMediaPlayPause case vkc == xwindows.VK_OEM_1, vkc == xwindows.VK_OEM_PLUS, vkc == xwindows.VK_OEM_COMMA, vkc == xwindows.VK_OEM_MINUS, vkc == xwindows.VK_OEM_PERIOD, vkc == xwindows.VK_OEM_2, vkc == xwindows.VK_OEM_3, vkc == xwindows.VK_OEM_4, vkc == xwindows.VK_OEM_5, vkc == xwindows.VK_OEM_6, vkc == xwindows.VK_OEM_7: // Use the actual character provided by Windows for current keyboard layout // instead of hardcoded US layout mappings if !unicode.IsControl(r) && unicode.IsPrint(r) { baseCode = r } else { // Fallback to original hardcoded mappings for non-printable cases switch vkc { case xwindows.VK_OEM_1: baseCode = ';' case xwindows.VK_OEM_PLUS: baseCode = '+' case xwindows.VK_OEM_COMMA: baseCode = ',' case xwindows.VK_OEM_MINUS: baseCode = '-' case xwindows.VK_OEM_PERIOD: baseCode = '.' case xwindows.VK_OEM_2: baseCode = '/' case xwindows.VK_OEM_3: baseCode = '`' case xwindows.VK_OEM_4: baseCode = '[' case xwindows.VK_OEM_5: baseCode = '\\' case xwindows.VK_OEM_6: baseCode = ']' case xwindows.VK_OEM_7: baseCode = '\'' } } } if utf16.IsSurrogate(r) { if state != nil { state.utf16Buf[0] = r state.utf16Half = true } return nil } // AltGr is left ctrl + right alt. On non-US keyboards, this is used to type // special characters and produce printable events. // XXX: Should this be a KeyMod? altGr := cks&(xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_ALT_PRESSED) == xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_ALT_PRESSED // FIXED: Remove numlock and scroll lock states when checking for printable text // These lock states shouldn't affect normal typing cksForTextCheck := cks &^ (xwindows.NUMLOCK_ON | xwindows.SCROLLLOCK_ON) var text string keyCode := baseCode if !unicode.IsControl(r) { rw := utf8.EncodeRune(utf8Buf[:], r) keyCode, _ = utf8.DecodeRune(utf8Buf[:rw]) if unicode.IsPrint(keyCode) && (cksForTextCheck == 0 || cksForTextCheck == xwindows.SHIFT_PRESSED || cksForTextCheck == xwindows.CAPSLOCK_ON || altGr) { // If the control key state is 0, shift is pressed, or caps lock // then the key event is a printable event i.e. [text] is not empty. text = string(keyCode) } } // Special case: numeric keypad divide should produce "/" text on all layouts (fix french keyboard layout) if baseCode == KeyKpDivide { text = "/" } key.Code = keyCode key.Text = text key.Mod = translateControlKeyState(cks) key.BaseCode = baseCode key = ensureKeyCase(key, cks) if keyDown { return KeyPressEvent(key) } return KeyReleaseEvent(key) } // ensureKeyCase ensures that the key's text is in the correct case based on the // control key state. func ensureKeyCase(key Key, cks uint32) Key { if len(key.Text) == 0 { return key } hasShift := cks&xwindows.SHIFT_PRESSED != 0 hasCaps := cks&xwindows.CAPSLOCK_ON != 0 if hasShift || hasCaps { if unicode.IsLower(key.Code) { key.ShiftedCode = unicode.ToUpper(key.Code) key.Text = string(key.ShiftedCode) } } else { if unicode.IsUpper(key.Code) { key.ShiftedCode = unicode.ToLower(key.Code) key.Text = string(key.ShiftedCode) } } return key } // translateControlKeyState translates the control key state from the Windows // Console API into a Mod bitmask. func translateControlKeyState(cks uint32) (m KeyMod) { if cks&xwindows.LEFT_CTRL_PRESSED != 0 || cks&xwindows.RIGHT_CTRL_PRESSED != 0 { m |= ModCtrl } if cks&xwindows.LEFT_ALT_PRESSED != 0 || cks&xwindows.RIGHT_ALT_PRESSED != 0 { m |= ModAlt } if cks&xwindows.SHIFT_PRESSED != 0 { m |= ModShift } if cks&xwindows.CAPSLOCK_ON != 0 { m |= ModCapsLock } if cks&xwindows.NUMLOCK_ON != 0 { m |= ModNumLock } if cks&xwindows.SCROLLLOCK_ON != 0 { m |= ModScrollLock } return } //nolint:unused func keyEventString(vkc, sc uint16, r rune, keyDown bool, cks uint32, repeatCount uint16) string { var s strings.Builder s.WriteString("vkc: ") s.WriteString(fmt.Sprintf("%d, 0x%02x", vkc, vkc)) s.WriteString(", sc: ") s.WriteString(fmt.Sprintf("%d, 0x%02x", sc, sc)) s.WriteString(", r: ") s.WriteString(fmt.Sprintf("%q", r)) s.WriteString(", down: ") s.WriteString(fmt.Sprintf("%v", keyDown)) s.WriteString(", cks: [") if cks&xwindows.LEFT_ALT_PRESSED != 0 { s.WriteString("left alt, ") } if cks&xwindows.RIGHT_ALT_PRESSED != 0 { s.WriteString("right alt, ") } if cks&xwindows.LEFT_CTRL_PRESSED != 0 { s.WriteString("left ctrl, ") } if cks&xwindows.RIGHT_CTRL_PRESSED != 0 { s.WriteString("right ctrl, ") } if cks&xwindows.SHIFT_PRESSED != 0 { s.WriteString("shift, ") } if cks&xwindows.CAPSLOCK_ON != 0 { s.WriteString("caps lock, ") } if cks&xwindows.NUMLOCK_ON != 0 { s.WriteString("num lock, ") } if cks&xwindows.SCROLLLOCK_ON != 0 { s.WriteString("scroll lock, ") } if cks&xwindows.ENHANCED_KEY != 0 { s.WriteString("enhanced key, ") } s.WriteString("], repeat count: ") s.WriteString(fmt.Sprintf("%d", repeatCount)) return s.String() }