| // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:async'; | |
| import 'dart:ffi'; | |
| import 'dart:io'; | |
| import 'dart:math' as math; | |
| import 'package:ffi/ffi.dart'; | |
| import 'package:meta/meta.dart'; | |
| /// Handles Win32 keyboard input and translates it to ANSI escape sequences. | |
| /// | |
| /// **Note**: This is not a complete implementation and is only intended for | |
| /// use with the provided cli components from this package. | |
| /// | |
| /// This is used on Windows to work around issues where [stdin] doesn't forward | |
| /// arrow keys or other special keys at all. | |
| class Win32AnsiStdin extends Stream<List<int>> { | |
| static final Map<int, Win32AnsiStdin> _instances = {}; | |
| final int _inputHandle; | |
| final StreamController<List<int>> _controller = StreamController<List<int>>(); | |
| bool _running = false; | |
| /// Creates or retrieves a [Win32AnsiStdin] instance for the given [handle]. | |
| /// | |
| /// If [handle] is omitted, it defaults to the standard input handle | |
| /// (`STD_INPUT_HANDLE`). | |
| /// | |
| /// Instances are cached and reused for the same handle. | |
| factory Win32AnsiStdin([int? handle]) { | |
| final actualHandle = | |
| handle ?? Win32Console.instance.getStdHandle(_stdInputHandle); | |
| return _instances.putIfAbsent( | |
| actualHandle, | |
| () => Win32AnsiStdin._create(actualHandle), | |
| ); | |
| } | |
| Win32AnsiStdin._create(this._inputHandle) { | |
| _controller.onCancel = _close; | |
| } | |
| void _startEventLoop() { | |
| if (_running) return; | |
| _running = true; | |
| _eventLoop(); | |
| } | |
| Future<void> _eventLoop() async { | |
| await using((arena) async { | |
| // Allocate a buffer for up to 10 events at a time. | |
| final pInputRecord = arena<InputRecord>(10); | |
| final pEventsRead = arena<Uint32>(); | |
| final pNumEvents = arena<Uint32>(); | |
| while (_running) { | |
| // Yield to Dart event loop between emitting events. | |
| await Future<void>.value(); | |
| if (!_running) break; | |
| // Check how many events are available, we don't want to block | |
| // waiting to read events if there are none. | |
| final numEventsResult = Win32Console.instance | |
| .getNumberOfConsoleInputEvents(_inputHandle, pNumEvents); | |
| if (numEventsResult == 0 || pNumEvents.value == 0) { | |
| // Error reading events or no events available, yield and try again. | |
| await Future<void>.delayed(const Duration(milliseconds: 50)); | |
| continue; | |
| } | |
| // Read up to 10 events at a time. | |
| final eventsToRead = math.min(10, pNumEvents.value); | |
| final result = Win32Console.instance.readConsoleInputW( | |
| _inputHandle, | |
| pInputRecord, | |
| eventsToRead, | |
| pEventsRead, | |
| ); | |
| if (result != 0 && pEventsRead.value > 0) { | |
| for (var i = 0; i < pEventsRead.value; i++) { | |
| final event = (pInputRecord + i).ref; | |
| if (event.eventType != InputRecordEventType.keyEvent) continue; | |
| final keyEvent = event.event.keyEvent; | |
| if (keyEvent.bKeyDown == 0) continue; | |
| final ansiiBytes = _translateKeyEvent(keyEvent); | |
| if (ansiiBytes != null && ansiiBytes.isNotEmpty) { | |
| _controller.add(ansiiBytes); | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| /// Translate a win32 key event to ANSI escape sequences or characters. | |
| /// | |
| /// Returns `null` if this isn't an event we care about. | |
| List<int>? _translateKeyEvent(KeyEventRecord keyEvent) { | |
| final virtualKeyCode = keyEvent.wVirtualKeyCode; | |
| if (virtualKeyCode == 0x41 && keyEvent.isCtrlPressed) { | |
| return [1]; // Ctrl+A | |
| } | |
| switch (virtualKeyCode) { | |
| case VirtualKeyCodes.up: | |
| return [0x1b, 0x5b, 0x41]; // ESC [ A | |
| case VirtualKeyCodes.down: | |
| return [0x1b, 0x5b, 0x42]; // ESC [ B | |
| case VirtualKeyCodes.right: | |
| return [0x1b, 0x5b, 0x43]; // ESC [ C | |
| case VirtualKeyCodes.left: | |
| return [0x1b, 0x5b, 0x44]; // ESC [ D | |
| case VirtualKeyCodes.home: | |
| return [0x1b, 0x5b, 0x48]; // ESC [ H | |
| case VirtualKeyCodes.end: | |
| return [0x1b, 0x5b, 0x46]; // ESC [ F | |
| case VirtualKeyCodes.pageUp: | |
| return [0x1b, 0x5b, 0x35, 0x7e]; // ESC [ 5 ~ | |
| case VirtualKeyCodes.pageDown: | |
| return [0x1b, 0x5b, 0x36, 0x7e]; // ESC [ 6 ~ | |
| case VirtualKeyCodes.enter: | |
| return [0x0d]; // CR | |
| case VirtualKeyCodes.escape: | |
| return [0x1b]; // ESC | |
| } | |
| final char = keyEvent.uChar; | |
| // Regular printable characters, just forward these along. | |
| if (char >= 32 && char < 127) return [char]; | |
| return null; | |
| } | |
| Future<void> _close() async { | |
| _running = false; | |
| await _controller.close(); | |
| _instances.remove(_inputHandle); | |
| } | |
| @override | |
| StreamSubscription<List<int>> listen( | |
| void Function(List<int> event)? onData, { | |
| Function? onError, | |
| void Function()? onDone, | |
| bool? cancelOnError, | |
| }) { | |
| _startEventLoop(); | |
| return _controller.stream.listen( | |
| onData, | |
| onError: onError, | |
| onDone: onDone, | |
| cancelOnError: cancelOnError, | |
| ); | |
| } | |
| } | |
| // Windows API Constants | |
| const int _stdInputHandle = -10; | |
| /// Virtual key codes | |
| /// | |
| /// https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes | |
| @visibleForTesting | |
| extension VirtualKeyCodes on int { | |
| static const int enter = 0x0D; | |
| static const int escape = 0x1B; | |
| static const int pageUp = 0x21; | |
| static const int pageDown = 0x22; | |
| static const int end = 0x23; | |
| static const int home = 0x24; | |
| static const int left = 0x25; | |
| static const int up = 0x26; | |
| static const int right = 0x27; | |
| static const int down = 0x28; | |
| } | |
| /// Dart enum representing possible event types from input records. | |
| /// | |
| /// https://learn.microsoft.com/en-us/windows/console/input-record-str | |
| enum InputRecordEventType { | |
| keyEvent, | |
| mouseEvent, | |
| windowBufferSizeEvent, | |
| menuEvent, | |
| focusEvent, | |
| unknown; | |
| /// https://learn.microsoft.com/en-us/windows/console/input-record-str#members | |
| factory InputRecordEventType.fromInt(int value) => switch (value) { | |
| 0x0001 => keyEvent, | |
| 0x0002 => mouseEvent, | |
| 0x0004 => windowBufferSizeEvent, | |
| 0x0008 => menuEvent, | |
| 0x0010 => focusEvent, | |
| _ => unknown, | |
| }; | |
| } | |
| /// Windows console input record struct. | |
| /// | |
| /// https://learn.microsoft.com/en-us/windows/console/input-record-str | |
| @visibleForTesting | |
| final class InputRecord extends Struct { | |
| @Uint16() | |
| external int _eventType; | |
| external EventUnion event; | |
| /// Converts [_eventType] to an [InputRecordEventType]. | |
| InputRecordEventType get eventType => | |
| InputRecordEventType.fromInt(_eventType); | |
| set eventType(InputRecordEventType type) { | |
| _eventType = switch (type) { | |
| InputRecordEventType.keyEvent => 0x0001, | |
| InputRecordEventType.mouseEvent => 0x0002, | |
| InputRecordEventType.windowBufferSizeEvent => 0x0004, | |
| InputRecordEventType.menuEvent => 0x0008, | |
| InputRecordEventType.focusEvent => 0x0010, | |
| InputRecordEventType.unknown => 0, | |
| }; | |
| } | |
| } | |
| /// Union of event types for [InputRecord]. | |
| /// | |
| /// https://learn.microsoft.com/en-us/windows/console/input-record-str | |
| @visibleForTesting | |
| final class EventUnion extends Union { | |
| /// Maps to [InputRecord.eventType == 1]. | |
| external KeyEventRecord keyEvent; | |
| } | |
| /// Windows key event record struct. | |
| /// | |
| /// https://learn.microsoft.com/en-us/windows/console/key-event-record-str | |
| @visibleForTesting | |
| final class KeyEventRecord extends Struct { | |
| @Int32() | |
| external int bKeyDown; | |
| @Uint16() | |
| external int wRepeatCount; | |
| @Uint16() | |
| external int wVirtualKeyCode; | |
| @Uint16() | |
| external int wVirtualScanCode; | |
| @Uint16() | |
| external int uChar; | |
| @Uint32() | |
| external int dwControlKeyState; | |
| bool get isCtrlPressed => | |
| (dwControlKeyState & (_leftCtrlPressed | _rightCtrlPressed)) != 0; | |
| } | |
| /// https://learn.microsoft.com/en-us/windows/console/key-event-record-str | |
| const int _leftCtrlPressed = 0x0008; | |
| const int _rightCtrlPressed = 0x0004; | |
| /// FFI Function binding to | |
| /// https://learn.microsoft.com/en-us/windows/console/getstdhandle | |
| typedef GetStdHandleDart = int Function(int nStdHandle); | |
| /// FFI Function binding to | |
| /// https://learn.microsoft.com/en-us/windows/console/readconsoleinput | |
| typedef ReadConsoleInputDart = | |
| int Function( | |
| int hConsoleInput, | |
| Pointer<InputRecord> lpBuffer, | |
| int nLength, | |
| Pointer<Uint32> lpNumberOfEventsRead, | |
| ); | |
| /// FFI Function binding to | |
| /// https://learn.microsoft.com/en-us/windows/console/getnumberofconsoleinputevents | |
| typedef GetNumberOfConsoleInputEventsDart = | |
| int Function(int hConsoleInput, Pointer<Uint32> lpcNumberOfEvents); | |
| /// Lazy loader for Win32 console APIs. | |
| @visibleForTesting | |
| class Win32Console { | |
| static Win32Console? _instance; | |
| static Win32Console get instance { | |
| // Tests may set a mock instance on non-windows. | |
| if (_instance case final instance?) return instance; | |
| if (!Platform.isWindows) { | |
| throw StateError('Win32Console is only available on Windows'); | |
| } | |
| return _instance = Win32Console._(); | |
| } | |
| @visibleForTesting | |
| static set instance(Win32Console? value) => _instance = value; | |
| final GetStdHandleDart getStdHandle; | |
| final ReadConsoleInputDart readConsoleInputW; | |
| final GetNumberOfConsoleInputEventsDart getNumberOfConsoleInputEvents; | |
| factory Win32Console._() { | |
| final kernel32 = DynamicLibrary.open('kernel32.dll'); | |
| return Win32Console.internal( | |
| kernel32.lookupFunction<IntPtr Function(Uint32), GetStdHandleDart>( | |
| 'GetStdHandle', | |
| ), | |
| kernel32.lookupFunction< | |
| Int32 Function(IntPtr, Pointer<InputRecord>, Uint32, Pointer<Uint32>), | |
| ReadConsoleInputDart | |
| >('ReadConsoleInputW'), | |
| kernel32.lookupFunction< | |
| Int32 Function(IntPtr, Pointer<Uint32>), | |
| GetNumberOfConsoleInputEventsDart | |
| >('GetNumberOfConsoleInputEvents'), | |
| ); | |
| } | |
| @visibleForTesting | |
| Win32Console.internal( | |
| this.getStdHandle, | |
| this.readConsoleInputW, | |
| this.getNumberOfConsoleInputEvents, | |
| ); | |
| } |