blob: 9dd660625ecb4b6c82cc77e2abec4413f38add9e [file] [edit]
// 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;
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;
}
/// 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,
);
}