From 210f8e426c6188130fee1434a70d41f34fbe5201 Mon Sep 17 00:00:00 2001 From: Haoxiang Fei Date: Tue, 9 Jun 2026 17:10:37 +0800 Subject: [PATCH 01/12] fix(input): parse Windows console input records --- docs/plan.md | 1 + .../2026-06-09-windows-input-record-enum.md | 122 ++++ win32_input.c | 50 +- win32_input.mbt | 549 ++++++++++++++---- win32_input_wbtest.mbt | 227 +++++++- 5 files changed, 783 insertions(+), 166 deletions(-) create mode 100644 docs/plans/2026-06-09-windows-input-record-enum.md diff --git a/docs/plan.md b/docs/plan.md index 1156ca4..4095df9 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -56,6 +56,7 @@ This board tracks implementation direction for `moonbit-community/tty`. Use | TTY-19 | done | OSC dynamic color queries | `docs/plans/2026-05-27-osc-dynamic-colors.md`, `color/`, `internal/vt/`, `internal/input/`, root package | root `Tty` can query default foreground, default background, and cursor colors while preserving interleaved input | `moon fmt`, `moon test color`, `moon test internal/vt`, `moon test internal/input`, `moon test .`, `moon test`, `moon check`, `moon info`, `git diff --check` | | TTY-20 | done | DEC auto wrap mode commands | `docs/plans/2026-05-29-decawm-auto-wrap.md`, `internal/vt/`, root package | root `Tty` can enable and disable DECAWM without adding terminal state or a capability query | `moon fmt`, `moon test internal/vt`, `moon test .`, `moon check`, `moon info`, `git diff --check` | | TTY-21 | done | Windows console input polling | `docs/plans/2026-06-08-windows-console-input-polling.md`, root package | Windows `Tty::read_event` reads console input records directly and reports resize events without public API changes | `moon fmt`, `moon check`, `moon test .`, `moon test internal/input`, `moon test`, `moon info`, `git diff --check`, Windows manual resize smoke pending | +| TTY-22 | done | Windows input record enum | `docs/plans/2026-06-09-windows-input-record-enum.md`, root package | Windows `INPUT_RECORD` bytes parse into private typed variants, including native mouse records | `moon fmt`, targeted Windows wbtests, `moon test .`, `moon test`, `moon check`, `moon info`, `git diff --check` | | MVP-2 | done | queued input and shell commands in agent demo | `docs/plans/2026-05-21-agent-queued-shell.md`, `examples/agent` | `Tab` queues input for delayed submission and `!cmd` runs through `moonbitlang/async/process` without background tasks writing directly to tty | `moon fmt`, `moon check examples/agent`, `moon test examples/agent`, `moon check`, `moon test`, `moon info`, `git diff --check` | ## Current Rules diff --git a/docs/plans/2026-06-09-windows-input-record-enum.md b/docs/plans/2026-06-09-windows-input-record-enum.md new file mode 100644 index 0000000..8aed8f6 --- /dev/null +++ b/docs/plans/2026-06-09-windows-input-record-enum.md @@ -0,0 +1,122 @@ +# Windows Input Record Enum + +## Goal + +Parse Windows console `INPUT_RECORD` values on the MoonBit side as a private +record enum, and handle native mouse records produced by `ReadConsoleInputW`. + +## Target Files + +- `win32_input.c`: copy one Windows `INPUT_RECORD` into a caller-provided byte + buffer instead of flattening selected fields into many FFI refs. +- `win32_input.mbt`: parse the raw Windows record layout into a private enum + and dispatch key, mouse, focus, and resize records through typed variants. +- `win32_input_wbtest.mbt`: update helpers and add record-layout and mouse + regression coverage. +- `docs/plan.md`: mark the task active while this implementation is in flight. + +## Accepted Design + +- Keep the implementation Windows-only and private to the root package. +- Treat the Windows SDK `INPUT_RECORD` layout as the private on-Windows ABI + boundary. +- The C FFI reads with `PeekConsoleInputW` / `ReadConsoleInputW` and copies the + resulting `INPUT_RECORD` bytes into a MoonBit-owned buffer. +- MoonBit parses that buffer into: + - `Win32InputRecord::Key(Win32KeyRecord)` + - `Win32InputRecord::Mouse(Win32MouseRecord)` + - `Win32InputRecord::WindowBufferSize(Win32Coord)` + - `Win32InputRecord::Focus(Bool)` + - `Win32InputRecord::Unsupported(Int)` +- Existing key-record behavior is preserved while moving from the old flat + record struct to the `Key` payload. +- Native `MOUSE_EVENT_RECORD` values map to existing public + `@input.MouseEvent` values and are queued as `Input(Mouse(...))`. +- Keep SGR/VT mouse byte decoding unchanged for terminals that report mouse + input as VT sequences. +- The direct-event vs decoded-byte select lives inside + `Win32ConsoleInputSource::read_decoded_input_event`. +- `Win32ConsoleInputSource::read_event` starts the record producer and delegates + event selection to `read_decoded_input_event`. + +## Public API / Interface Diff + +- No public MoonBit API change. +- `pkg.generated.mbti` should remain unchanged after `moon info`. +- New enum, record payload types, byte-layout helpers, and FFI functions remain + private to the root package. + +## Invariants + +- C does not expose raw `INPUT_RECORD` memory beyond copying it into a buffer + supplied by MoonBit. +- MoonBit layout parsing is guarded by buffer-size checks and stays under + `#cfg(platform="windows")`. +- Unsupported record types are still consumed and ignored so they cannot stall + polling. +- Key repeats, AltGr text, keypad metadata, Ctrl-space, Ctrl-left-bracket, + key-up dropping, and query-source preservation remain unchanged. +- This task does not change the existing pipe-based byte delivery or attempt to + solve cancellation after `ReadConsoleInputW` consumes a record. + +## Acceptance Criteria + +- Existing Windows key/focus/resize white-box tests continue to pass. +- New tests prove the raw Windows record bytes parse into typed enum variants. +- New tests prove native mouse move/button records become public mouse events. +- `examples/input` should no longer lose native mouse records through the + unsupported-record branch on Windows console input. + +## Validation Plan + +- `moon fmt` +- Targeted Windows white-box tests for input-record parsing and mouse mapping. +- Existing targeted Windows input-source regression tests. +- `moon test .` +- `moon test` +- `moon check` +- `moon check --target all` +- `moon info` +- Review `.mbti` diff. +- `git diff --check` + +## Result + +- Implemented the private Windows record split: + - C now copies the SDK `INPUT_RECORD` bytes into a MoonBit-owned buffer. + - MoonBit parses those bytes into private `Key`, `Mouse`, + `WindowBufferSize`, `Focus`, and `Unsupported` variants. + - Existing key handling now consumes `Win32KeyRecord`. + - Native `MOUSE_EVENT_RECORD` values update a mutable + `Win32MouseButtonState` and queue existing public `MouseEvent` values. +- Added white-box coverage for raw key, mouse, and focus record parsing. +- Added white-box coverage for native mouse move, press, drag, release, and + wheel mapping. +- Moved the direct-event vs decoded-input select into + `Win32ConsoleInputSource::read_decoded_input_event`, without changing the + existing pipe-based byte delivery. + +## Validation Results + +- Passed: `moon fmt` +- Passed: `moon test . --filter "win32*" --target-dir .moon-test-win32-build` +- Passed: `moon check --target-dir .moon-check-build` +- Passed: `moon test . --target-dir .moon-test-dot-build` +- Passed: `moon test --target-dir .moon-test-all-build` +- Passed: `moon check --target all --target-dir .moon-check-all-build` +- Passed: `moon info --target-dir .moon-info-build` +- Passed: `clang.exe -fsyntax-only -I C:\Users\mbt\.moon\include win32_input.c` +- Passed: `git diff --check` +- Note: full native test runs emit warnings from upstream async/runtime C stubs + on Windows; no warnings remain from this package's MoonBit changes. +- Re-ran after the select-only follow-up: + - Passed: `moon fmt` + - Passed: `moon test . --filter "win32*" --target-dir .moon-test-win32-build` + - Passed: `moon check --target-dir .moon-check-build` + - Note: `moon check` currently reports private unused-field warnings for the + Windows `INPUT_RECORD` enum payloads. + +## Public API Audit + +- No public MoonBit API change intended. +- `moon info` produced no intended `.mbti` public API diff. diff --git a/win32_input.c b/win32_input.c index aeae044..4653c51 100644 --- a/win32_input.c +++ b/win32_input.c @@ -1,5 +1,6 @@ #include #include +#include #ifdef _WIN32 #include @@ -7,6 +8,16 @@ #include #endif +MOONBIT_FFI_EXPORT +int32_t +moonbit_tty_get_sizeof_input_record(void) { +#ifdef _WIN32 + return (int32_t)sizeof(INPUT_RECORD); +#else + return 0; +#endif +} + MOONBIT_FFI_EXPORT int32_t moonbit_tty_read_console_input_record( @@ -15,17 +26,9 @@ moonbit_tty_read_console_input_record( #else intptr_t fd, #endif - int32_t *event_type, - int32_t *key_down, - int32_t *repeat_count, - int32_t *virtual_key_code, - int32_t *unicode_char, - int32_t *control_key_state, - int32_t *focus_set + void *output_record ) { - if (event_type == NULL || key_down == NULL || repeat_count == NULL || - virtual_key_code == NULL || unicode_char == NULL || - control_key_state == NULL || focus_set == NULL) { + if (output_record == NULL) { #ifdef _WIN32 SetLastError(ERROR_INVALID_PARAMETER); #else @@ -34,15 +37,9 @@ moonbit_tty_read_console_input_record( return -1; } - *event_type = 0; - *key_down = 0; - *repeat_count = 0; - *virtual_key_code = 0; - *unicode_char = 0; - *control_key_state = 0; - *focus_set = 0; - #ifdef _WIN32 + memset(output_record, 0, sizeof(INPUT_RECORD)); + INPUT_RECORD record; DWORD count = 0; if (!PeekConsoleInputW(fd, &record, 1, &count)) { @@ -58,24 +55,11 @@ moonbit_tty_read_console_input_record( return 0; } - *event_type = (int32_t)record.EventType; - switch (record.EventType) { - case KEY_EVENT: - *key_down = record.Event.KeyEvent.bKeyDown ? 1 : 0; - *repeat_count = (int32_t)record.Event.KeyEvent.wRepeatCount; - *virtual_key_code = (int32_t)record.Event.KeyEvent.wVirtualKeyCode; - *unicode_char = (int32_t)record.Event.KeyEvent.uChar.UnicodeChar; - *control_key_state = (int32_t)record.Event.KeyEvent.dwControlKeyState; - break; - case FOCUS_EVENT: - *focus_set = record.Event.FocusEvent.bSetFocus ? 1 : 0; - break; - default: - break; - } + memcpy(output_record, &record, sizeof(INPUT_RECORD)); return 1; #else (void)fd; + (void)output_record; errno = ENOSYS; return -1; #endif diff --git a/win32_input.mbt b/win32_input.mbt index af2730f..3211dcd 100644 --- a/win32_input.mbt +++ b/win32_input.mbt @@ -2,6 +2,10 @@ #cfg(platform="windows") const Win32RecordKey : Int = 0x0001 +///| +#cfg(platform="windows") +const Win32RecordMouse : Int = 0x0002 + ///| #cfg(platform="windows") const Win32RecordWindowBufferSize : Int = 0x0004 @@ -44,14 +48,82 @@ const Win32EnhancedKey : Int = 0x0100 ///| #cfg(platform="windows") -priv struct Win32InputRecord { - event_type : Int +const Win32MouseMoved : Int = 0x0001 + +///| +#cfg(platform="windows") +const Win32MouseDoubleClick : Int = 0x0002 + +///| +#cfg(platform="windows") +const Win32MouseWheeled : Int = 0x0004 + +///| +#cfg(platform="windows") +const Win32MouseHorizontalWheeled : Int = 0x0008 + +///| +#cfg(platform="windows") +const Win32MouseLeftButton : Int = 0x0001 + +///| +#cfg(platform="windows") +const Win32MouseRightButton : Int = 0x0002 + +///| +#cfg(platform="windows") +const Win32MouseMiddleButton : Int = 0x0004 + +///| +#cfg(platform="windows") +const Win32MouseButtonMask : Int = 0x0007 + +///| +#cfg(platform="windows") +priv struct Win32RawInputRecord(Bytes) + +///| +#cfg(platform="windows") +priv enum Win32InputRecord { + Key(Win32KeyRecord) + Mouse(Win32MouseRecord) + WindowBufferSize(Win32Coord) + Focus(Bool) + Unsupported(Int) +} + +///| +#cfg(platform="windows") +priv struct Win32Coord { + x : Int + y : Int +} + +///| +#cfg(platform="windows") +priv struct Win32KeyRecord { key_down : Int repeat_count : Int virtual_key_code : Int + virtual_scan_code : Int unicode_char : Int control_key_state : Int - focus_set : Int +} + +///| +#cfg(platform="windows") +priv struct Win32MouseRecord { + x : Int + y : Int + button_state : Int + control_key_state : Int + event_flags : Int +} + +///| +#cfg(platform="windows") +priv struct Win32MouseButtonState { + mut buttons : Int } ///| @@ -64,6 +136,7 @@ priv struct Win32ConsoleInputSource { reader : @input.EventReader events : @async.Queue[Event] pending_surrogate : Ref[Int] + mouse_button_state : Win32MouseButtonState } ///| @@ -81,6 +154,7 @@ fn Win32ConsoleInputSource::new( reader: @input.EventReader::new(byte_read as &@async/io.Reader), events: Queue(kind=Unbounded), pending_surrogate: Ref(0), + mouse_button_state: { buttons: 0 }, } } @@ -91,6 +165,7 @@ fn Win32ConsoleInputSource::close(self : Win32ConsoleInputSource) -> Unit { ignore(self.output_fd) ignore(self.reader) ignore(self.pending_surrogate.val) + ignore(self.mouse_button_state.buttons) self.byte_read.close() self.byte_write.close() self.events.close(clear=true) @@ -152,14 +227,23 @@ async fn Win32ConsoleInputSource::read_event( self : Win32ConsoleInputSource, esc_timeout_ms : Int, ) -> Event { - match self.try_get_event() { - Some(event) => return event - None => () - } @async.with_task_group() <| group => { group.spawn_bg(no_wait=true) <| () => { self.read_records() } + self.read_decoded_input_event(esc_timeout_ms) + } +} + +///| +#cfg(platform="windows") +async fn Win32ConsoleInputSource::read_decoded_input_event( + self : Win32ConsoleInputSource, + esc_timeout_ms : Int, +) -> Event { + @async.with_task_group() <| group => { group.spawn_bg(no_wait=true) <| () => { - group.return_immediately(self.read_decoded_input_event(esc_timeout_ms)) + group.return_immediately( + self.read_input_event_from_decoder(esc_timeout_ms), + ) } self.events.get() } @@ -167,7 +251,7 @@ async fn Win32ConsoleInputSource::read_event( ///| #cfg(platform="windows") -async fn Win32ConsoleInputSource::read_decoded_input_event( +async fn Win32ConsoleInputSource::read_input_event_from_decoder( self : Win32ConsoleInputSource, esc_timeout_ms : Int, ) -> Event { @@ -191,18 +275,6 @@ async fn Win32ConsoleInputSource::read_internal_event( self.reader.read_event(esc_timeout_ms~) } -///| -#cfg(platform="windows") -fn Win32ConsoleInputSource::try_get_event( - self : Win32ConsoleInputSource, -) -> Event? { - try self.events.try_get() catch { - _ => None - } noraise { - event => event - } -} - ///| #cfg(platform="windows") async fn Win32ConsoleInputSource::read_records( @@ -222,27 +294,24 @@ async fn Win32ConsoleInputSource::read_record( self : Win32ConsoleInputSource, record : Win32InputRecord, ) -> Unit { - match record.event_type { - Win32RecordKey => self.read_key_record(record) - Win32RecordWindowBufferSize => { + match record { + Key(record) => self.read_key_record(record) + Mouse(record) => self.read_mouse_record(record) + WindowBufferSize(_) => { self.pending_surrogate.val = 0 - match self.window_size() { - Some(size) => self.events.put(Resize(size)) - None => () + if self.window_size() is Some(size) { + self.events.put(Resize(size)) } } - Win32RecordFocus => { + Focus(focus_set) => { self.pending_surrogate.val = 0 - if record.focus_set != 0 { + if focus_set { self.events.put(Input(FocusIn)) } else { self.events.put(Input(FocusOut)) } } - _ => { - self.pending_surrogate.val = 0 - () - } + Unsupported(_) => self.pending_surrogate.val = 0 } } @@ -250,8 +319,9 @@ async fn Win32ConsoleInputSource::read_record( #cfg(platform="windows") async fn Win32ConsoleInputSource::read_key_record( self : Win32ConsoleInputSource, - record : Win32InputRecord, + record : Win32KeyRecord, ) -> Unit { + ignore(record.virtual_scan_code) if record.key_down == 0 { return } @@ -271,7 +341,7 @@ async fn Win32ConsoleInputSource::read_key_record( #cfg(platform="windows") async fn Win32ConsoleInputSource::queue_key_input( self : Win32ConsoleInputSource, - record : Win32InputRecord, + record : Win32KeyRecord, ) -> Unit { match record.win32_key_input(self.pending_surrogate) { Some(input) => @@ -282,6 +352,31 @@ async fn Win32ConsoleInputSource::queue_key_input( } } +///| +#cfg(platform="windows") +async fn Win32ConsoleInputSource::read_mouse_record( + self : Win32ConsoleInputSource, + record : Win32MouseRecord, +) -> Unit { + self.pending_surrogate.val = 0 + match self.mouse_button_state.event_kind(record) { + Some(kind) => + self.events.put( + Input( + Mouse( + @public_input.MouseEvent::new( + kind, + record.y + 1, + record.x + 1, + modifiers=record.win32_modifiers(), + ), + ), + ), + ) + None => () + } +} + ///| #cfg(platform="windows") fn Win32ConsoleInputSource::window_size( @@ -298,7 +393,147 @@ fn Win32ConsoleInputSource::window_size( ///| #cfg(platform="windows") -fn Win32InputRecord::win32_repeat_count(self : Win32InputRecord) -> Int { +fn Win32MouseButtonState::event_kind( + self : Win32MouseButtonState, + record : Win32MouseRecord, +) -> @public_input.MouseEventKind? { + let buttons = record.button_state & Win32MouseButtonMask + match record.event_flags { + 0 => { + let previous = self.buttons + self.buttons = buttons + match win32_newly_pressed_mouse_button(previous, buttons) { + Some(button) => Some(Press(button)) + None => + win32_newly_released_mouse_button(previous, buttons).map(button => { + Release(button) + }) + } + } + Win32MouseMoved => { + self.buttons = buttons + match win32_mouse_button_from_state(buttons) { + Some(button) => Some(Drag(button)) + None => Some(Move) + } + } + Win32MouseDoubleClick => { + self.buttons = buttons + win32_mouse_button_from_state(buttons).map(button => Press(button)) + } + Win32MouseWheeled => { + self.buttons = buttons + let delta = win32_signed_high_word(record.button_state) + if delta > 0 { + Some(Scroll(Up)) + } else if delta < 0 { + Some(Scroll(Down)) + } else { + None + } + } + Win32MouseHorizontalWheeled => { + self.buttons = buttons + let delta = win32_signed_high_word(record.button_state) + if delta > 0 { + Some(Scroll(Right)) + } else if delta < 0 { + Some(Scroll(Left)) + } else { + None + } + } + _ => { + self.buttons = buttons + None + } + } +} + +///| +#cfg(platform="windows") +fn Win32MouseRecord::win32_modifiers( + self : Win32MouseRecord, +) -> @public_input.KeyModifiers { + win32_modifiers_from_control_key_state(self.control_key_state) +} + +///| +#cfg(platform="windows") +fn win32_newly_pressed_mouse_button( + previous : Int, + buttons : Int, +) -> @public_input.MouseButton? { + if (buttons & Win32MouseLeftButton) != 0 && + (previous & Win32MouseLeftButton) == 0 { + Some(Left) + } else if (buttons & Win32MouseMiddleButton) != 0 && + (previous & Win32MouseMiddleButton) == 0 { + Some(Middle) + } else if (buttons & Win32MouseRightButton) != 0 && + (previous & Win32MouseRightButton) == 0 { + Some(Right) + } else { + None + } +} + +///| +#cfg(platform="windows") +fn win32_newly_released_mouse_button( + previous : Int, + buttons : Int, +) -> @public_input.MouseButton? { + if (previous & Win32MouseLeftButton) != 0 && + (buttons & Win32MouseLeftButton) == 0 { + Some(Left) + } else if (previous & Win32MouseMiddleButton) != 0 && + (buttons & Win32MouseMiddleButton) == 0 { + Some(Middle) + } else if (previous & Win32MouseRightButton) != 0 && + (buttons & Win32MouseRightButton) == 0 { + Some(Right) + } else { + None + } +} + +///| +#cfg(platform="windows") +fn win32_mouse_button_from_state(state : Int) -> @public_input.MouseButton? { + if (state & Win32MouseLeftButton) != 0 { + Some(Left) + } else if (state & Win32MouseMiddleButton) != 0 { + Some(Middle) + } else if (state & Win32MouseRightButton) != 0 { + Some(Right) + } else { + None + } +} + +///| +#cfg(platform="windows") +fn win32_signed_high_word(value : Int) -> Int { + let low_word = value & 0xffff + (value - low_word) / 0x10000 +} + +///| +#cfg(platform="windows") +fn win32_modifiers_from_control_key_state( + state : Int, +) -> @public_input.KeyModifiers { + @public_input.KeyModifiers::new( + shift=win32_has_state(state, Win32ShiftPressed), + alt=win32_has_state(state, Win32LeftAltPressed | Win32RightAltPressed), + ctrl=win32_has_state(state, Win32LeftCtrlPressed | Win32RightCtrlPressed), + ) +} + +///| +#cfg(platform="windows") +fn Win32KeyRecord::win32_repeat_count(self : Win32KeyRecord) -> Int { if self.repeat_count > 0 { self.repeat_count } else { @@ -308,26 +543,20 @@ fn Win32InputRecord::win32_repeat_count(self : Win32InputRecord) -> Int { ///| #cfg(platform="windows") -fn Win32InputRecord::win32_key_text_uses_decoder( - self : Win32InputRecord, -) -> Bool { +fn Win32KeyRecord::win32_key_text_uses_decoder(self : Win32KeyRecord) -> Bool { self.key_down != 0 && !self.win32_has_direct_key_metadata() } ///| #cfg(platform="windows") -fn Win32InputRecord::win32_has_direct_key_metadata( - self : Win32InputRecord, -) -> Bool { +fn Win32KeyRecord::win32_has_direct_key_metadata(self : Win32KeyRecord) -> Bool { (self.win32_has_keyboard_modifier() && !self.win32_is_alt_gr_text()) || self.win32_is_keypad() } ///| #cfg(platform="windows") -fn Win32InputRecord::win32_has_keyboard_modifier( - self : Win32InputRecord, -) -> Bool { +fn Win32KeyRecord::win32_has_keyboard_modifier(self : Win32KeyRecord) -> Bool { win32_has_state( self.control_key_state, Win32ShiftPressed | @@ -340,7 +569,7 @@ fn Win32InputRecord::win32_has_keyboard_modifier( ///| #cfg(platform="windows") -fn Win32InputRecord::win32_is_alt_gr_text(self : Win32InputRecord) -> Bool { +fn Win32KeyRecord::win32_is_alt_gr_text(self : Win32KeyRecord) -> Bool { let alt_gr_state = Win32RightAltPressed | Win32LeftCtrlPressed self.key_down != 0 && self.unicode_char > 0x1f && @@ -353,8 +582,8 @@ fn Win32InputRecord::win32_is_alt_gr_text(self : Win32InputRecord) -> Bool { ///| #cfg(platform="windows") -fn Win32InputRecord::win32_key_text( - self : Win32InputRecord, +fn Win32KeyRecord::win32_key_text( + self : Win32KeyRecord, pending_surrogate : Ref[Int], ) -> String? { if self.key_down == 0 { @@ -382,8 +611,8 @@ fn Win32InputRecord::win32_key_text( ///| #cfg(platform="windows") -fn Win32InputRecord::win32_key_input( - self : Win32InputRecord, +fn Win32KeyRecord::win32_key_input( + self : Win32KeyRecord, pending_surrogate : Ref[Int], ) -> @public_input.InputEvent? { guard self.win32_key_event(pending_surrogate) is Some(event) else { @@ -394,8 +623,8 @@ fn Win32InputRecord::win32_key_input( ///| #cfg(platform="windows") -fn Win32InputRecord::win32_key_kind( - self : Win32InputRecord, +fn Win32KeyRecord::win32_key_kind( + self : Win32KeyRecord, ) -> @public_input.KeyEventKind { if self.key_down == 0 { Release @@ -408,26 +637,16 @@ fn Win32InputRecord::win32_key_kind( ///| #cfg(platform="windows") -fn Win32InputRecord::win32_modifiers( - self : Win32InputRecord, +fn Win32KeyRecord::win32_modifiers( + self : Win32KeyRecord, ) -> @public_input.KeyModifiers { - @public_input.KeyModifiers::new( - shift=win32_has_state(self.control_key_state, Win32ShiftPressed), - alt=win32_has_state( - self.control_key_state, - Win32LeftAltPressed | Win32RightAltPressed, - ), - ctrl=win32_has_state( - self.control_key_state, - Win32LeftCtrlPressed | Win32RightCtrlPressed, - ), - ) + win32_modifiers_from_control_key_state(self.control_key_state) } ///| #cfg(platform="windows") -fn Win32InputRecord::win32_key_state( - self : Win32InputRecord, +fn Win32KeyRecord::win32_key_state( + self : Win32KeyRecord, ) -> @public_input.KeyEventState { @public_input.KeyEventState::new( keypad=self.win32_is_keypad(), @@ -438,8 +657,8 @@ fn Win32InputRecord::win32_key_state( ///| #cfg(platform="windows") -fn Win32InputRecord::win32_key_event( - self : Win32InputRecord, +fn Win32KeyRecord::win32_key_event( + self : Win32KeyRecord, pending_surrogate : Ref[Int], ) -> @public_input.KeyEvent? { let kind = self.win32_key_kind() @@ -483,8 +702,8 @@ fn Win32InputRecord::win32_key_event( ///| #cfg(platform="windows") -fn Win32InputRecord::win32_key_code_from_virtual_key( - self : Win32InputRecord, +fn Win32KeyRecord::win32_key_code_from_virtual_key( + self : Win32KeyRecord, ) -> @public_input.KeyCode? { if self.win32_is_keypad_enter() { Some(KeypadEnter) @@ -495,7 +714,7 @@ fn Win32InputRecord::win32_key_code_from_virtual_key( ///| #cfg(platform="windows") -fn Win32InputRecord::win32_is_keypad(self : Win32InputRecord) -> Bool { +fn Win32KeyRecord::win32_is_keypad(self : Win32KeyRecord) -> Bool { win32_virtual_key_is_keypad(self.virtual_key_code) || self.win32_is_keypad_enter() || self.win32_is_keypad_navigation() @@ -503,24 +722,22 @@ fn Win32InputRecord::win32_is_keypad(self : Win32InputRecord) -> Bool { ///| #cfg(platform="windows") -fn Win32InputRecord::win32_is_keypad_enter(self : Win32InputRecord) -> Bool { +fn Win32KeyRecord::win32_is_keypad_enter(self : Win32KeyRecord) -> Bool { self.virtual_key_code == 0x0d && win32_has_state(self.control_key_state, Win32EnhancedKey) } ///| #cfg(platform="windows") -fn Win32InputRecord::win32_is_keypad_navigation( - self : Win32InputRecord, -) -> Bool { +fn Win32KeyRecord::win32_is_keypad_navigation(self : Win32KeyRecord) -> Bool { win32_virtual_key_is_navigation(self.virtual_key_code) && !win32_has_state(self.control_key_state, Win32EnhancedKey) } ///| #cfg(platform="windows") -fn Win32InputRecord::win32_char_key_event( - self : Win32InputRecord, +fn Win32KeyRecord::win32_char_key_event( + self : Win32KeyRecord, char : Char, modifiers : @public_input.KeyModifiers, kind : @public_input.KeyEventKind, @@ -535,7 +752,7 @@ fn Win32InputRecord::win32_char_key_event( ///| #cfg(platform="windows") -fn win32_text_for_key(record : Win32InputRecord, char : Char) -> String? { +fn win32_text_for_key(record : Win32KeyRecord, char : Char) -> String? { if record.key_down == 0 { None } else { @@ -699,17 +916,152 @@ fn win32_decode_surrogate_pair(high : Int, low : Int) -> Char { } ///| -#borrow(event_type, key_down, repeat_count, virtual_key_code, unicode_char, control_key_state, focus_set) +#cfg(platform="windows") +fn Win32RawInputRecord::new() -> Win32RawInputRecord { + Win32RawInputRecord(Bytes::make(sizeof_win32_input_record, 0)) +} + +///| +#cfg(platform="windows") +fn Win32RawInputRecord::parse(self : Win32RawInputRecord) -> Win32InputRecord { + let bytes = self.0[:] + guard win32_u16_at(bytes, 0) is Some(event_type) else { + return Unsupported(0) + } + match event_type { + Win32RecordKey => win32_parse_key_record(bytes) + Win32RecordMouse => win32_parse_mouse_record(bytes) + Win32RecordWindowBufferSize => win32_parse_window_buffer_size_record(bytes) + Win32RecordFocus => win32_parse_focus_record(bytes) + _ => Unsupported(event_type) + } +} + +///| +#cfg(platform="windows") +fn win32_parse_key_record(bytes : BytesView) -> Win32InputRecord { + guard win32_bool_at(bytes, 4) is Some(key_down) else { + return Unsupported(Win32RecordKey) + } + guard win32_u16_at(bytes, 8) is Some(repeat_count) else { + return Unsupported(Win32RecordKey) + } + guard win32_u16_at(bytes, 10) is Some(virtual_key_code) else { + return Unsupported(Win32RecordKey) + } + guard win32_u16_at(bytes, 12) is Some(virtual_scan_code) else { + return Unsupported(Win32RecordKey) + } + guard win32_u16_at(bytes, 14) is Some(unicode_char) else { + return Unsupported(Win32RecordKey) + } + guard win32_dword_at(bytes, 16) is Some(control_key_state) else { + return Unsupported(Win32RecordKey) + } + Key({ + key_down: if key_down { + 1 + } else { + 0 + }, + repeat_count, + virtual_key_code, + virtual_scan_code, + unicode_char, + control_key_state, + }) +} + +///| +#cfg(platform="windows") +fn win32_parse_mouse_record(bytes : BytesView) -> Win32InputRecord { + guard win32_i16_at(bytes, 4) is Some(x) else { + return Unsupported(Win32RecordMouse) + } + guard win32_i16_at(bytes, 6) is Some(y) else { + return Unsupported(Win32RecordMouse) + } + guard win32_dword_at(bytes, 8) is Some(button_state) else { + return Unsupported(Win32RecordMouse) + } + guard win32_dword_at(bytes, 12) is Some(control_key_state) else { + return Unsupported(Win32RecordMouse) + } + guard win32_dword_at(bytes, 16) is Some(event_flags) else { + return Unsupported(Win32RecordMouse) + } + Mouse({ x, y, button_state, control_key_state, event_flags }) +} + +///| +#cfg(platform="windows") +fn win32_parse_window_buffer_size_record(bytes : BytesView) -> Win32InputRecord { + guard win32_i16_at(bytes, 4) is Some(x) else { + return Unsupported(Win32RecordWindowBufferSize) + } + guard win32_i16_at(bytes, 6) is Some(y) else { + return Unsupported(Win32RecordWindowBufferSize) + } + WindowBufferSize({ x, y }) +} + +///| +#cfg(platform="windows") +fn win32_parse_focus_record(bytes : BytesView) -> Win32InputRecord { + guard win32_bool_at(bytes, 4) is Some(focus_set) else { + return Unsupported(Win32RecordFocus) + } + Focus(focus_set) +} + +///| +#cfg(platform="windows") +fn win32_bool_at(bytes : BytesView, offset : Int) -> Bool? { + win32_dword_at(bytes, offset).map(value => value != 0) +} + +///| +#cfg(platform="windows") +fn win32_i16_at(bytes : BytesView, offset : Int) -> Int? { + guard win32_u16_at(bytes, offset) is Some(value) else { return None } + if value >= 0x8000 { + Some(value - 0x10000) + } else { + Some(value) + } +} + +///| +#cfg(platform="windows") +fn win32_u16_at(bytes : BytesView, offset : Int) -> Int? { + if offset < 0 || offset + 1 >= bytes.length() { + return None + } + Some(bytes[offset].to_int() + bytes[offset + 1].to_int() * 0x100) +} + +///| +#cfg(platform="windows") +fn win32_dword_at(bytes : BytesView, offset : Int) -> Int? { + guard win32_u16_at(bytes, offset) is Some(low) else { return None } + guard win32_i16_at(bytes, offset + 2) is Some(high) else { return None } + Some(low + high * 0x10000) +} + +///| +#cfg(platform="windows") +extern "c" fn win32_sizeof_input_record() -> Int = "moonbit_tty_get_sizeof_input_record" + +///| +#cfg(platform="windows") +let sizeof_win32_input_record : Int = win32_sizeof_input_record() + +///| +#borrow(record) #cfg(platform="windows") extern "c" fn win32_read_console_input_record_ffi( fd : @async/types.Fd, - event_type : Ref[Int], - key_down : Ref[Int], - repeat_count : Ref[Int], - virtual_key_code : Ref[Int], - unicode_char : Ref[Int], - control_key_state : Ref[Int], - focus_set : Ref[Int], + record : Win32RawInputRecord, ) -> Int = "moonbit_tty_read_console_input_record" ///| @@ -717,31 +1069,14 @@ extern "c" fn win32_read_console_input_record_ffi( fn win32_read_console_input_record( fd : @async/types.Fd, ) -> Win32InputRecord? raise { - let event_type = Ref(0) - let key_down = Ref(0) - let repeat_count = Ref(0) - let virtual_key_code = Ref(0) - let unicode_char = Ref(0) - let control_key_state = Ref(0) - let focus_set = Ref(0) - let rc = win32_read_console_input_record_ffi( - fd, event_type, key_down, repeat_count, virtual_key_code, unicode_char, control_key_state, - focus_set, - ) + let record = Win32RawInputRecord::new() + let rc = win32_read_console_input_record_ffi(fd, record) if rc < 0 { @os_error.check_errno("ReadConsoleInputW") } if rc == 0 { None } else { - Some({ - event_type: event_type.val, - key_down: key_down.val, - repeat_count: repeat_count.val, - virtual_key_code: virtual_key_code.val, - unicode_char: unicode_char.val, - control_key_state: control_key_state.val, - focus_set: focus_set.val, - }) + Some(record.parse()) } } diff --git a/win32_input_wbtest.mbt b/win32_input_wbtest.mbt index 42a5c94..f40f626 100644 --- a/win32_input_wbtest.mbt +++ b/win32_input_wbtest.mbt @@ -5,15 +5,14 @@ fn win32_test_key_record( virtual_key_code? : Int = 0, control_key_state? : Int = 0, key_down? : Int = 1, -) -> Win32InputRecord { +) -> Win32KeyRecord { { - event_type: Win32RecordKey, key_down, repeat_count: 1, virtual_key_code, + virtual_scan_code: 0, unicode_char: char.to_int(), control_key_state, - focus_set: 0, } } @@ -24,15 +23,129 @@ fn win32_test_key_record_charcode( virtual_key_code? : Int = 0, control_key_state? : Int = 0, key_down? : Int = 1, -) -> Win32InputRecord { +) -> Win32KeyRecord { { - event_type: Win32RecordKey, key_down, repeat_count: 1, virtual_key_code, + virtual_scan_code: 0, unicode_char: charcode, control_key_state, - focus_set: 0, + } +} + +///| +#cfg(platform="windows") +fn win32_test_mouse_record( + x? : Int = 0, + y? : Int = 0, + button_state? : Int = 0, + control_key_state? : Int = 0, + event_flags? : Int = 0, +) -> Win32MouseRecord { + { x, y, button_state, control_key_state, event_flags } +} + +///| +#cfg(platform="windows") +fn win32_test_try_get_event(source : Win32ConsoleInputSource) -> Event? { + try source.events.try_get() catch { + _ => None + } noraise { + event => event + } +} + +///| +#cfg(platform="windows") +fn win32_test_raw_record(event_type : Int) -> FixedArray[Byte] { + let buf = FixedArray::make(sizeof_win32_input_record, b'\x00') + win32_test_set_u16_le(buf, 0, event_type) + buf +} + +///| +#cfg(platform="windows") +fn win32_test_raw_input_record(buf : FixedArray[Byte]) -> Win32RawInputRecord { + Win32RawInputRecord(buf.unsafe_reinterpret_as_bytes()) +} + +///| +#cfg(platform="windows") +fn win32_test_set_u16_le( + buf : FixedArray[Byte], + offset : Int, + value : Int, +) -> Unit { + buf[offset] = (value & 0xff).to_byte() + buf[offset + 1] = ((value / 0x100) & 0xff).to_byte() +} + +///| +#cfg(platform="windows") +fn win32_test_set_u32_le( + buf : FixedArray[Byte], + offset : Int, + value : Int, +) -> Unit { + buf[offset] = (value & 0xff).to_byte() + buf[offset + 1] = ((value / 0x100) & 0xff).to_byte() + buf[offset + 2] = ((value / 0x10000) & 0xff).to_byte() + buf[offset + 3] = ((value / 0x1000000) & 0xff).to_byte() +} + +///| +#cfg(platform="windows") +test "win32 raw input record parses key records" { + let buf = win32_test_raw_record(Win32RecordKey) + win32_test_set_u32_le(buf, 4, 1) + win32_test_set_u16_le(buf, 8, 2) + win32_test_set_u16_le(buf, 10, 0x41) + win32_test_set_u16_le(buf, 12, 0x1e) + win32_test_set_u16_le(buf, 14, 'A'.to_int()) + win32_test_set_u32_le(buf, 16, Win32ShiftPressed) + match win32_test_raw_input_record(buf).parse() { + Key(record) => { + @debug.assert_eq(record.key_down, 1) + @debug.assert_eq(record.repeat_count, 2) + @debug.assert_eq(record.virtual_key_code, 0x41) + @debug.assert_eq(record.virtual_scan_code, 0x1e) + @debug.assert_eq(record.unicode_char, 'A'.to_int()) + @debug.assert_eq(record.control_key_state, Win32ShiftPressed) + } + _ => fail("expected key record") + } +} + +///| +#cfg(platform="windows") +test "win32 raw input record parses mouse records" { + let buf = win32_test_raw_record(Win32RecordMouse) + win32_test_set_u16_le(buf, 4, 30) + win32_test_set_u16_le(buf, 6, 2) + win32_test_set_u32_le(buf, 8, Win32MouseLeftButton) + win32_test_set_u32_le(buf, 12, Win32ShiftPressed) + win32_test_set_u32_le(buf, 16, Win32MouseMoved) + match win32_test_raw_input_record(buf).parse() { + Mouse(record) => { + @debug.assert_eq(record.x, 30) + @debug.assert_eq(record.y, 2) + @debug.assert_eq(record.button_state, Win32MouseLeftButton) + @debug.assert_eq(record.control_key_state, Win32ShiftPressed) + @debug.assert_eq(record.event_flags, Win32MouseMoved) + } + _ => fail("expected mouse record") + } +} + +///| +#cfg(platform="windows") +test "win32 raw input record parses focus records" { + let buf = win32_test_raw_record(Win32RecordFocus) + win32_test_set_u32_le(buf, 4, 1) + match win32_test_raw_input_record(buf).parse() { + Focus(true) => () + _ => fail("expected focus record") } } @@ -45,7 +158,7 @@ async test "win32 console source ignores key-up records" { let source = Win32ConsoleInputSource::new(dummy_input.fd(), dummy_output.fd()) defer source.close() source.read_key_record(win32_test_key_record('a', key_down=0)) - match source.try_get_event() { + match win32_test_try_get_event(source) { None => () Some(_) => fail("expected key-up record to be ignored") } @@ -82,15 +195,7 @@ async test "win32 console source preserves native events during internal reads" defer source.close() @async.with_task_group() <| group => { group.spawn_bg() <| () => { - source.read_record({ - event_type: Win32RecordFocus, - key_down: 0, - repeat_count: 0, - virtual_key_code: 0, - unicode_char: 0, - control_key_state: 0, - focus_set: 1, - }) + source.read_record(Focus(true)) for char in "\u{1B}[7;9R" { source.read_key_record(win32_test_key_record(char)) } @@ -102,13 +207,83 @@ async test "win32 console source preserves native events during internal reads" } _ => fail("expected cursor position event") } - match source.try_get_event() { + match win32_test_try_get_event(source) { Some(Input(FocusIn)) => () _ => fail("expected native focus event to remain queued") } } } +///| +#cfg(platform="windows") +async test "win32 console source maps native mouse records" { + let (dummy_input, dummy_output) = @async/pipe.pipe() + defer dummy_input.close() + defer dummy_output.close() + let source = Win32ConsoleInputSource::new(dummy_input.fd(), dummy_output.fd()) + defer source.close() + source.read_mouse_record( + win32_test_mouse_record(x=30, y=2, event_flags=Win32MouseMoved), + ) + match win32_test_try_get_event(source) { + Some(Input(Mouse(event))) => + @debug.assert_eq(event, @public_input.MouseEvent::new(Move, 3, 31)) + _ => fail("expected mouse move event") + } + source.read_mouse_record( + win32_test_mouse_record(x=30, y=2, button_state=Win32MouseLeftButton), + ) + match win32_test_try_get_event(source) { + Some(Input(Mouse(event))) => + @debug.assert_eq(event, @public_input.MouseEvent::new(Press(Left), 3, 31)) + _ => fail("expected left mouse press event") + } + source.read_mouse_record( + win32_test_mouse_record( + x=31, + y=2, + button_state=Win32MouseLeftButton, + control_key_state=Win32ShiftPressed, + event_flags=Win32MouseMoved, + ), + ) + match win32_test_try_get_event(source) { + Some(Input(Mouse(event))) => + @debug.assert_eq( + event, + @public_input.MouseEvent::new( + Drag(Left), + 3, + 32, + modifiers=@public_input.KeyModifiers::new(shift=true), + ), + ) + _ => fail("expected left mouse drag event") + } + source.read_mouse_record(win32_test_mouse_record(x=31, y=2)) + match win32_test_try_get_event(source) { + Some(Input(Mouse(event))) => + @debug.assert_eq( + event, + @public_input.MouseEvent::new(Release(Left), 3, 32), + ) + _ => fail("expected left mouse release event") + } + source.read_mouse_record( + win32_test_mouse_record( + x=31, + y=2, + button_state=120 * 0x10000, + event_flags=Win32MouseWheeled, + ), + ) + match win32_test_try_get_event(source) { + Some(Input(Mouse(event))) => + @debug.assert_eq(event, @public_input.MouseEvent::new(Scroll(Up), 3, 32)) + _ => fail("expected mouse wheel up event") + } +} + ///| #cfg(platform="windows") async test "win32 console source preserves printable key modifiers" { @@ -120,7 +295,7 @@ async test "win32 console source preserves printable key modifiers" { source.read_key_record( win32_test_key_record('a', control_key_state=Win32LeftAltPressed), ) - match source.try_get_event() { + match win32_test_try_get_event(source) { Some(Input(Key(event))) => @debug.assert_eq( event, @@ -138,7 +313,7 @@ async test "win32 console source preserves printable key modifiers" { control_key_state=Win32ShiftPressed | Win32RightAltPressed, ), ) - match source.try_get_event() { + match win32_test_try_get_event(source) { Some(Input(Key(event))) => @debug.assert_eq( event, @@ -167,7 +342,7 @@ async test "win32 console source preserves ctrl left bracket" { control_key_state=Win32LeftCtrlPressed, ), ) - match source.try_get_event() { + match win32_test_try_get_event(source) { Some(Input(Key(event))) => @debug.assert_eq( event, @@ -195,7 +370,7 @@ async test "win32 console source preserves ctrl space" { control_key_state=Win32LeftCtrlPressed, ), ) - match source.try_get_event() { + match win32_test_try_get_event(source) { Some(Input(Key(event))) => @debug.assert_eq( event, @@ -251,7 +426,7 @@ async test "win32 console source preserves keypad key metadata" { control_key_state=Win32NumLockOn, ), ) - match source.try_get_event() { + match win32_test_try_get_event(source) { Some(Input(Key(event))) => @debug.assert_eq( event, @@ -263,7 +438,7 @@ async test "win32 console source preserves keypad key metadata" { _ => fail("expected keypad 1 key event") } source.read_key_record(win32_test_key_record('*', virtual_key_code=0x6a)) - match source.try_get_event() { + match win32_test_try_get_event(source) { Some(Input(Key(event))) => @debug.assert_eq( event, @@ -291,7 +466,7 @@ async test "win32 console source preserves keypad enter metadata" { control_key_state=Win32EnhancedKey, ), ) - match source.try_get_event() { + match win32_test_try_get_event(source) { Some(Input(Key(event))) => @debug.assert_eq( event, @@ -315,7 +490,7 @@ async test "win32 console source preserves keypad navigation metadata" { source.read_key_record( win32_test_key_record_charcode(0, virtual_key_code=0x23), ) - match source.try_get_event() { + match win32_test_try_get_event(source) { Some(Input(Key(event))) => @debug.assert_eq( event, @@ -333,7 +508,7 @@ async test "win32 console source preserves keypad navigation metadata" { control_key_state=Win32EnhancedKey, ), ) - match source.try_get_event() { + match win32_test_try_get_event(source) { Some(Input(Key(event))) => @debug.assert_eq(event, @public_input.KeyEvent::new(End)) _ => fail("expected enhanced End key event") From ab3bafa44e6f07b5e57178ec370af067bcba2d56 Mon Sep 17 00:00:00 2001 From: Haoxiang Fei Date: Tue, 9 Jun 2026 17:34:44 +0800 Subject: [PATCH 02/12] refactor(input): flatten Windows input backend --- .../2026-06-09-windows-input-record-enum.md | 57 +++ tty.mbt | 13 +- win32_input.mbt | 335 +++++++++--------- win32_input_wbtest.mbt | 75 ++-- 4 files changed, 259 insertions(+), 221 deletions(-) diff --git a/docs/plans/2026-06-09-windows-input-record-enum.md b/docs/plans/2026-06-09-windows-input-record-enum.md index 8aed8f6..9321f27 100644 --- a/docs/plans/2026-06-09-windows-input-record-enum.md +++ b/docs/plans/2026-06-09-windows-input-record-enum.md @@ -120,3 +120,60 @@ record enum, and handle native mouse records produced by `ReadConsoleInputW`. - No public MoonBit API change intended. - `moon info` produced no intended `.mbti` public API diff. + +## Follow-up: Flatten Windows Input Backend + +### Goal + +Make `Tty.reader` the single byte-stream `EventReader` owner on Windows by +flattening the Windows console input source state into a private Windows input +backend stored by `Tty`. + +### Accepted Design + +- Keep the change Windows-only and private to the root package. +- Replace `Win32ConsoleInputSource` with a private `WindowsInput` enum: + - `ByteStream` for non-console Windows handles that should read directly from + the original input byte stream. + - `Console(...)` for Win32 console handles that use `ReadConsoleInputW`, + pipe text bytes into `Tty.reader`, and queue direct events. +- Keep `Tty.reader` as the only `@input.EventReader` field. +- In the console case, construct `Tty.reader` from the console byte pipe read + end; in the fallback case, construct it from the original input reader. +- Keep existing pipe-based byte delivery and current cancellation behavior; do + not introduce a queue-backed byte reader in this refactor. +- Do not add Windows prefixes to `Tty` field names solely because the struct is + under `#cfg(platform="windows")`. + +### Public API / Interface Diff + +- No public MoonBit API change intended. +- `pkg.generated.mbti` should remain unchanged after `moon info`. + +### Validation Plan + +- `moon fmt` +- `moon test . --filter "win32*" --target-dir .moon-test-win32-flatten-build` +- `moon check --target-dir .moon-check-flatten-build` +- `moon info --target-dir .moon-info-flatten-build` +- Review `.mbti` diff. + +### Result + +- Replaced `Win32ConsoleInputSource` with the private Windows-only + `WindowsInput` backend enum. +- `Tty` now owns the active `EventReader` on Windows: + - `ByteStream` uses the original input byte reader. + - `Console` uses the console byte pipe read end. +- Moved Windows console record reading and record dispatch helpers onto `Tty`. +- Kept existing pipe-based text-byte delivery and direct event queue behavior. + +### Validation Results + +- Passed: `moon fmt` +- Passed: `moon check --target-dir .moon-check-flatten-build` +- Passed: `moon test . --filter "win32*" --target-dir .moon-test-win32-flatten-build` +- Passed: `moon info --target-dir .moon-info-flatten-build` +- Note: `moon info` still rewrites the pre-existing `Fd::fd` generated mbti + spelling from `Int` to `@types.Fd`; this flatten refactor does not intend to + include that generated public-interface churn. diff --git a/tty.mbt b/tty.mbt index b52ad57..cec6449 100644 --- a/tty.mbt +++ b/tty.mbt @@ -24,7 +24,7 @@ struct Tty { input : &Reader output : &Writer reader : @input.EventReader - win32_console_input : Win32ConsoleInputSource? + input_backend : WindowsInput pending_input : Array[@public_input.InputEvent] } @@ -43,12 +43,12 @@ struct Tty { pub fn[I : Reader, O : Writer] Tty::new(input : I, output : O) -> Tty { let input = input as &Reader let output = output as &Writer - let win32_console_input = win32_console_input_source(input.fd(), output.fd()) + let input_backend = WindowsInput::new(input.fd()) { input, output, - reader: @input.EventReader::new(input as &@async/io.Reader), - win32_console_input, + reader: input_backend.event_reader(input as &@async/io.Reader), + input_backend, pending_input: [], } } @@ -79,10 +79,7 @@ pub fn Tty::stdio() -> Tty { /// Closing stdio-backed handles is a no-op. #cfg(platform="windows") pub fn Tty::close(self : Self) -> Unit { - match self.win32_console_input { - Some(source) => source.close() - None => () - } + self.input_backend.close() self.input.close() self.output.close() } diff --git a/win32_input.mbt b/win32_input.mbt index 3211dcd..8267cba 100644 --- a/win32_input.mbt +++ b/win32_input.mbt @@ -128,59 +128,75 @@ priv struct Win32MouseButtonState { ///| #cfg(platform="windows") -priv struct Win32ConsoleInputSource { - input_fd : @async/types.Fd - output_fd : @async/types.Fd - byte_read : @async/io.PipeRead - byte_write : @async/io.PipeWrite - reader : @input.EventReader - events : @async.Queue[Event] - pending_surrogate : Ref[Int] - mouse_button_state : Win32MouseButtonState +priv enum WindowsInput { + ByteStream + Console( + input_fd~ : @async/types.Fd, + byte_read~ : @async/io.PipeRead, + byte_write~ : @async/io.PipeWrite, + events~ : @async.Queue[Event], + pending_surrogate~ : Ref[Int], + mouse_button_state~ : Win32MouseButtonState + ) } ///| #cfg(platform="windows") -fn Win32ConsoleInputSource::new( - input_fd : @async/types.Fd, - output_fd : @async/types.Fd, -) -> Win32ConsoleInputSource { - let (byte_read, byte_write) = @async/io.pipe() - { - input_fd, - output_fd, - byte_read, - byte_write, - reader: @input.EventReader::new(byte_read as &@async/io.Reader), - events: Queue(kind=Unbounded), - pending_surrogate: Ref(0), - mouse_button_state: { buttons: 0 }, +fn WindowsInput::new(input_fd : @async/types.Fd) -> WindowsInput { + if tty_isatty(input_fd) > 0 { + WindowsInput::console(input_fd) + } else { + ByteStream } } ///| #cfg(platform="windows") -fn Win32ConsoleInputSource::close(self : Win32ConsoleInputSource) -> Unit { - ignore(self.input_fd) - ignore(self.output_fd) - ignore(self.reader) - ignore(self.pending_surrogate.val) - ignore(self.mouse_button_state.buttons) - self.byte_read.close() - self.byte_write.close() - self.events.close(clear=true) +fn WindowsInput::console(input_fd : @async/types.Fd) -> WindowsInput { + let (byte_read, byte_write) = @async/io.pipe() + Console( + input_fd~, + byte_read~, + byte_write~, + events=Queue(kind=Unbounded), + pending_surrogate=Ref(0), + mouse_button_state={ buttons: 0 }, + ) } ///| #cfg(platform="windows") -fn win32_console_input_source( - input_fd : @async/types.Fd, - output_fd : @async/types.Fd, -) -> Win32ConsoleInputSource? { - if tty_isatty(input_fd) > 0 { - Some(Win32ConsoleInputSource::new(input_fd, output_fd)) - } else { - None +fn WindowsInput::event_reader( + self : WindowsInput, + fallback : &@async/io.Reader, +) -> @input.EventReader { + match self { + ByteStream => @input.EventReader::new(fallback) + Console(byte_read~, ..) => + @input.EventReader::new(byte_read as &@async/io.Reader) + } +} + +///| +#cfg(platform="windows") +fn WindowsInput::close(self : WindowsInput) -> Unit { + match self { + ByteStream => () + Console( + input_fd~, + byte_read~, + byte_write~, + events~, + pending_surrogate~, + mouse_button_state~ + ) => { + ignore(input_fd) + ignore(pending_surrogate.val) + ignore(mouse_button_state.buttons) + byte_read.close() + byte_write.close() + events.close(clear=true) + } } } @@ -190,9 +206,9 @@ async fn Tty::read_win32_console_event( self : Self, esc_timeout_ms : Int, ) -> Event { - match self.win32_console_input { - Some(source) => source.read_event(esc_timeout_ms) - None => self.read_decoded_input_event(esc_timeout_ms) + match self.input_backend { + Console(..) => self.read_console_event(esc_timeout_ms) + ByteStream => self.read_decoded_input_event(esc_timeout_ms) } } @@ -202,13 +218,13 @@ async fn Tty::read_internal_event_from_sources( self : Self, esc_timeout_ms : Int, ) -> @input.Event { - match self.win32_console_input { - Some(source) => + match self.input_backend { + Console(..) => @async.with_task_group() <| group => { - group.spawn_bg(no_wait=true) <| () => { source.read_records() } - source.read_internal_event(esc_timeout_ms) + group.spawn_bg(no_wait=true) <| () => { self.read_records() } + self.reader.read_event(esc_timeout_ms~) } - None => self.reader.read_event(esc_timeout_ms~) + ByteStream => self.reader.read_event(esc_timeout_ms~) } } @@ -223,171 +239,142 @@ async fn Tty::read_internal_event_from_sources( ///| #cfg(platform="windows") -async fn Win32ConsoleInputSource::read_event( - self : Win32ConsoleInputSource, - esc_timeout_ms : Int, -) -> Event { +async fn Tty::read_console_event(self : Self, esc_timeout_ms : Int) -> Event { @async.with_task_group() <| group => { group.spawn_bg(no_wait=true) <| () => { self.read_records() } - self.read_decoded_input_event(esc_timeout_ms) + self.read_console_input_event(esc_timeout_ms) } } ///| #cfg(platform="windows") -async fn Win32ConsoleInputSource::read_decoded_input_event( - self : Win32ConsoleInputSource, +async fn Tty::read_console_input_event( + self : Self, esc_timeout_ms : Int, ) -> Event { - @async.with_task_group() <| group => { - group.spawn_bg(no_wait=true) <| () => { - group.return_immediately( - self.read_input_event_from_decoder(esc_timeout_ms), - ) - } - self.events.get() + match self.input_backend { + Console(events~, ..) => + @async.with_task_group() <| group => { + group.spawn_bg(no_wait=true) <| () => { + group.return_immediately( + self.read_decoded_input_event(esc_timeout_ms), + ) + } + events.get() + } + ByteStream => self.read_decoded_input_event(esc_timeout_ms) } } ///| #cfg(platform="windows") -async fn Win32ConsoleInputSource::read_input_event_from_decoder( - self : Win32ConsoleInputSource, - esc_timeout_ms : Int, -) -> Event { - for ;; { - match self.read_internal_event(esc_timeout_ms) { - Input(event) => return Input(event) - CursorPosition(..) => () - KeyboardEnhancementFlags(_) => () - PrimaryDeviceAttributes(_) => () - DynamicColor(..) => () - } +async fn Tty::read_records(self : Self) -> Unit { + match self.input_backend { + Console(input_fd~, ..) => + for ;; { + match win32_read_console_input_record(input_fd) { + None => @async.sleep(10) + Some(record) => self.read_record(record) + } + } + ByteStream => () } } ///| #cfg(platform="windows") -async fn Win32ConsoleInputSource::read_internal_event( - self : Win32ConsoleInputSource, - esc_timeout_ms : Int, -) -> @input.Event { - self.reader.read_event(esc_timeout_ms~) -} - -///| -#cfg(platform="windows") -async fn Win32ConsoleInputSource::read_records( - self : Win32ConsoleInputSource, -) -> Unit { - for ;; { - match win32_read_console_input_record(self.input_fd) { - None => @async.sleep(10) - Some(record) => self.read_record(record) - } +async fn Tty::read_record(self : Self, record : Win32InputRecord) -> Unit { + match self.input_backend { + Console(events~, pending_surrogate~, ..) => + match record { + Key(record) => self.read_key_record(record) + Mouse(record) => self.read_mouse_record(record) + WindowBufferSize(_) => { + pending_surrogate.val = 0 + try self.window_size() catch { + _ => () + } noraise { + size => events.put(Resize(size)) + } + } + Focus(focus_set) => { + pending_surrogate.val = 0 + if focus_set { + events.put(Input(FocusIn)) + } else { + events.put(Input(FocusOut)) + } + } + Unsupported(_) => pending_surrogate.val = 0 + } + ByteStream => () } } ///| #cfg(platform="windows") -async fn Win32ConsoleInputSource::read_record( - self : Win32ConsoleInputSource, - record : Win32InputRecord, -) -> Unit { - match record { - Key(record) => self.read_key_record(record) - Mouse(record) => self.read_mouse_record(record) - WindowBufferSize(_) => { - self.pending_surrogate.val = 0 - if self.window_size() is Some(size) { - self.events.put(Resize(size)) +async fn Tty::read_key_record(self : Self, record : Win32KeyRecord) -> Unit { + match self.input_backend { + Console(byte_write~, pending_surrogate~, ..) => { + ignore(record.virtual_scan_code) + if record.key_down == 0 { + return } - } - Focus(focus_set) => { - self.pending_surrogate.val = 0 - if focus_set { - self.events.put(Input(FocusIn)) - } else { - self.events.put(Input(FocusOut)) + if !record.win32_key_text_uses_decoder() { + return self.queue_key_input(record) } - } - Unsupported(_) => self.pending_surrogate.val = 0 - } -} - -///| -#cfg(platform="windows") -async fn Win32ConsoleInputSource::read_key_record( - self : Win32ConsoleInputSource, - record : Win32KeyRecord, -) -> Unit { - ignore(record.virtual_scan_code) - if record.key_down == 0 { - return - } - if !record.win32_key_text_uses_decoder() { - return self.queue_key_input(record) - } - match record.win32_key_text(self.pending_surrogate) { - Some(text) => - for _ in 0.. + for _ in 0.. self.queue_key_input(record) } - None => self.queue_key_input(record) + } + ByteStream => () } } ///| #cfg(platform="windows") -async fn Win32ConsoleInputSource::queue_key_input( - self : Win32ConsoleInputSource, - record : Win32KeyRecord, -) -> Unit { - match record.win32_key_input(self.pending_surrogate) { - Some(input) => - for _ in 0.. Unit { + match self.input_backend { + Console(events~, pending_surrogate~, ..) => + match record.win32_key_input(pending_surrogate) { + Some(input) => + for _ in 0.. () } - None => () + ByteStream => () } } ///| #cfg(platform="windows") -async fn Win32ConsoleInputSource::read_mouse_record( - self : Win32ConsoleInputSource, - record : Win32MouseRecord, -) -> Unit { - self.pending_surrogate.val = 0 - match self.mouse_button_state.event_kind(record) { - Some(kind) => - self.events.put( - Input( - Mouse( - @public_input.MouseEvent::new( - kind, - record.y + 1, - record.x + 1, - modifiers=record.win32_modifiers(), +async fn Tty::read_mouse_record(self : Self, record : Win32MouseRecord) -> Unit { + match self.input_backend { + Console(events~, pending_surrogate~, mouse_button_state~, ..) => { + pending_surrogate.val = 0 + match mouse_button_state.event_kind(record) { + Some(kind) => + events.put( + Input( + Mouse( + @public_input.MouseEvent::new( + kind, + record.y + 1, + record.x + 1, + modifiers=record.win32_modifiers(), + ), + ), ), - ), - ), - ) - None => () - } -} - -///| -#cfg(platform="windows") -fn Win32ConsoleInputSource::window_size( - self : Win32ConsoleInputSource, -) -> WindowSize? { - let rows = Ref(0) - let cols = Ref(0) - if tty_get_window_size(self.output_fd, rows, cols) < 0 { - None - } else { - Some({ rows: rows.val, cols: cols.val }) + ) + None => () + } + } + ByteStream => () } } diff --git a/win32_input_wbtest.mbt b/win32_input_wbtest.mbt index f40f626..b983b02 100644 --- a/win32_input_wbtest.mbt +++ b/win32_input_wbtest.mbt @@ -48,11 +48,30 @@ fn win32_test_mouse_record( ///| #cfg(platform="windows") -fn win32_test_try_get_event(source : Win32ConsoleInputSource) -> Event? { - try source.events.try_get() catch { - _ => None - } noraise { - event => event +fn[I : Reader, O : Writer] win32_test_console_tty(input : I, output : O) -> Tty { + let input = input as &Reader + let output = output as &Writer + let input_backend = WindowsInput::console(input.fd()) + { + input, + output, + reader: input_backend.event_reader(input as &@async/io.Reader), + input_backend, + pending_input: [], + } +} + +///| +#cfg(platform="windows") +fn win32_test_try_get_event(tty : Tty) -> Event? { + match tty.input_backend { + Console(events~, ..) => + try events.try_get() catch { + _ => None + } noraise { + event => event + } + ByteStream => None } } @@ -153,9 +172,7 @@ test "win32 raw input record parses focus records" { #cfg(platform="windows") async test "win32 console source ignores key-up records" { let (dummy_input, dummy_output) = @async/pipe.pipe() - defer dummy_input.close() - defer dummy_output.close() - let source = Win32ConsoleInputSource::new(dummy_input.fd(), dummy_output.fd()) + let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() source.read_key_record(win32_test_key_record('a', key_down=0)) match win32_test_try_get_event(source) { @@ -168,9 +185,7 @@ async test "win32 console source ignores key-up records" { #cfg(platform="windows") async test "win32 console source decodes bracketed paste key records" { let (dummy_input, dummy_output) = @async/pipe.pipe() - defer dummy_input.close() - defer dummy_output.close() - let source = Win32ConsoleInputSource::new(dummy_input.fd(), dummy_output.fd()) + let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() @async.with_task_group() <| group => { group.spawn_bg() <| () => { @@ -189,9 +204,7 @@ async test "win32 console source decodes bracketed paste key records" { #cfg(platform="windows") async test "win32 console source preserves native events during internal reads" { let (dummy_input, dummy_output) = @async/pipe.pipe() - defer dummy_input.close() - defer dummy_output.close() - let source = Win32ConsoleInputSource::new(dummy_input.fd(), dummy_output.fd()) + let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() @async.with_task_group() <| group => { group.spawn_bg() <| () => { @@ -200,7 +213,7 @@ async test "win32 console source preserves native events during internal reads" source.read_key_record(win32_test_key_record(char)) } } - match source.read_internal_event(50) { + match source.reader.read_event(esc_timeout_ms=50) { CursorPosition(row~, col~) => { @debug.assert_eq(row, 7) @debug.assert_eq(col, 9) @@ -218,9 +231,7 @@ async test "win32 console source preserves native events during internal reads" #cfg(platform="windows") async test "win32 console source maps native mouse records" { let (dummy_input, dummy_output) = @async/pipe.pipe() - defer dummy_input.close() - defer dummy_output.close() - let source = Win32ConsoleInputSource::new(dummy_input.fd(), dummy_output.fd()) + let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() source.read_mouse_record( win32_test_mouse_record(x=30, y=2, event_flags=Win32MouseMoved), @@ -288,9 +299,7 @@ async test "win32 console source maps native mouse records" { #cfg(platform="windows") async test "win32 console source preserves printable key modifiers" { let (dummy_input, dummy_output) = @async/pipe.pipe() - defer dummy_input.close() - defer dummy_output.close() - let source = Win32ConsoleInputSource::new(dummy_input.fd(), dummy_output.fd()) + let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() source.read_key_record( win32_test_key_record('a', control_key_state=Win32LeftAltPressed), @@ -331,9 +340,7 @@ async test "win32 console source preserves printable key modifiers" { #cfg(platform="windows") async test "win32 console source preserves ctrl left bracket" { let (dummy_input, dummy_output) = @async/pipe.pipe() - defer dummy_input.close() - defer dummy_output.close() - let source = Win32ConsoleInputSource::new(dummy_input.fd(), dummy_output.fd()) + let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() source.read_key_record( win32_test_key_record( @@ -359,9 +366,7 @@ async test "win32 console source preserves ctrl left bracket" { #cfg(platform="windows") async test "win32 console source preserves ctrl space" { let (dummy_input, dummy_output) = @async/pipe.pipe() - defer dummy_input.close() - defer dummy_output.close() - let source = Win32ConsoleInputSource::new(dummy_input.fd(), dummy_output.fd()) + let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() source.read_key_record( win32_test_key_record_charcode( @@ -387,9 +392,7 @@ async test "win32 console source preserves ctrl space" { #cfg(platform="windows") async test "win32 console source preserves AltGr text input" { let (dummy_input, dummy_output) = @async/pipe.pipe() - defer dummy_input.close() - defer dummy_output.close() - let source = Win32ConsoleInputSource::new(dummy_input.fd(), dummy_output.fd()) + let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() @async.with_task_group() <| group => { group.spawn_bg() <| () => { @@ -415,9 +418,7 @@ async test "win32 console source preserves AltGr text input" { #cfg(platform="windows") async test "win32 console source preserves keypad key metadata" { let (dummy_input, dummy_output) = @async/pipe.pipe() - defer dummy_input.close() - defer dummy_output.close() - let source = Win32ConsoleInputSource::new(dummy_input.fd(), dummy_output.fd()) + let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() source.read_key_record( win32_test_key_record( @@ -455,9 +456,7 @@ async test "win32 console source preserves keypad key metadata" { #cfg(platform="windows") async test "win32 console source preserves keypad enter metadata" { let (dummy_input, dummy_output) = @async/pipe.pipe() - defer dummy_input.close() - defer dummy_output.close() - let source = Win32ConsoleInputSource::new(dummy_input.fd(), dummy_output.fd()) + let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() source.read_key_record( win32_test_key_record( @@ -483,9 +482,7 @@ async test "win32 console source preserves keypad enter metadata" { #cfg(platform="windows") async test "win32 console source preserves keypad navigation metadata" { let (dummy_input, dummy_output) = @async/pipe.pipe() - defer dummy_input.close() - defer dummy_output.close() - let source = Win32ConsoleInputSource::new(dummy_input.fd(), dummy_output.fd()) + let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() source.read_key_record( win32_test_key_record_charcode(0, virtual_key_code=0x23), From 74e14f1efd8ba0a82fa85722b3f219287384a454 Mon Sep 17 00:00:00 2001 From: Haoxiang Fei Date: Tue, 9 Jun 2026 17:46:55 +0800 Subject: [PATCH 03/12] refactor(input): split Win32 parser package --- .../2026-06-09-windows-input-record-enum.md | 70 ++ internal/win32/key.mbt | 372 ++++++++ internal/win32/moon.pkg | 7 + internal/win32/mouse.mbt | 155 ++++ internal/win32/pkg.generated.mbti | 101 +++ internal/win32/record.mbt | 292 ++++++ internal/win32/record_wbtest.mbt | 84 ++ moon.pkg | 1 + win32_input.mbt | 847 +----------------- win32_input_wbtest.mbt | 167 +--- 10 files changed, 1143 insertions(+), 953 deletions(-) create mode 100644 internal/win32/key.mbt create mode 100644 internal/win32/moon.pkg create mode 100644 internal/win32/mouse.mbt create mode 100644 internal/win32/pkg.generated.mbti create mode 100644 internal/win32/record.mbt create mode 100644 internal/win32/record_wbtest.mbt diff --git a/docs/plans/2026-06-09-windows-input-record-enum.md b/docs/plans/2026-06-09-windows-input-record-enum.md index 9321f27..ea63a92 100644 --- a/docs/plans/2026-06-09-windows-input-record-enum.md +++ b/docs/plans/2026-06-09-windows-input-record-enum.md @@ -177,3 +177,73 @@ backend stored by `Tty`. - Note: `moon info` still rewrites the pre-existing `Fd::fd` generated mbti spelling from `Int` to `@types.Fd`; this flatten refactor does not intend to include that generated public-interface churn. + +## Follow-up: Split Win32 Input Parser Package + +### Goal + +Move the pure Windows `INPUT_RECORD` parsing and native key/mouse mapping code +out of the root tty package into `internal/win32`, while keeping root `Tty` +responsible for terminal handles, backend selection, async record reading, and +platform FFI. + +### Accepted Design + +- Add a new internal package at `internal/win32`. +- Move raw input record modeling, byte-layout parsing, key mapping, mouse + mapping, modifier mapping, and surrogate helpers into that package. +- Use package-local names without redundant `Win32` prefixes where the package + path already supplies the Win32 context: + - `RawInputRecord` + - `InputRecord` + - `KeyRecord` + - `MouseRecord` + - `MouseButtonState` +- Keep `ReadConsoleInputW` and `sizeof(INPUT_RECORD)` FFI wrappers in the root + tty package. Root passes an `@win32.RawInputRecord` buffer to C and then asks + the internal package to parse it. +- Keep the current pipe-based text-byte delivery and direct event queue + behavior unchanged. +- Keep root tests for `Tty`/backend behavior in the root white-box test file, + and move raw record layout tests to `internal/win32`. + +### Public API / Interface Diff + +- No root public MoonBit API change intended. +- Root `pkg.generated.mbti` should remain unchanged after `moon info`. +- The new `internal/win32` package will expose an internal-only interface for + root tty code and white-box tests. + +### Validation Plan + +- `moon fmt` +- `moon test internal/win32 --target-dir .moon-test-internal-win32-build` +- `moon test . --filter "win32*" --target-dir .moon-test-win32-split-build` +- `moon check --target-dir .moon-check-win32-split-build` +- `moon info --target-dir .moon-info-win32-split-build` +- Review `.mbti` diff. +- `git diff --check` + +### Result + +- Added `internal/win32` for pure Windows `INPUT_RECORD` modeling, parsing, + key mapping, mouse mapping, and modifier/surrogate helpers. +- Root `win32_input.mbt` now keeps only the Windows input backend, `Tty` + dispatch helpers, and the `ReadConsoleInputW` / `sizeof(INPUT_RECORD)` FFI + wrapper. +- Root passes an `@win32.RawInputRecord` buffer to C and parses it through the + internal package. +- Moved raw record layout white-box tests into `internal/win32`. +- Kept root white-box tests focused on `Tty` backend behavior. + +### Validation Results + +- Passed: `moon fmt` +- Passed: `moon test internal/win32 --target-dir .moon-test-internal-win32-build` +- Passed: `moon test . --filter "win32*" --target-dir .moon-test-win32-split-build` +- Passed: `moon check --target-dir .moon-check-win32-split-build` +- Passed: `moon info --target-dir .moon-info-win32-split-build` +- Passed: `git diff --check` +- Note: `moon info` still rewrites the pre-existing root `Fd::fd` generated + mbti spelling from `Int` to `@types.Fd`; this split restored the root + generated interface to avoid unrelated public API churn. diff --git a/internal/win32/key.mbt b/internal/win32/key.mbt new file mode 100644 index 0000000..f96e2dc --- /dev/null +++ b/internal/win32/key.mbt @@ -0,0 +1,372 @@ +///| +#cfg(platform="windows") +pub fn KeyRecord::is_down(self : KeyRecord) -> Bool { + self.key_down != 0 +} + +///| +#cfg(platform="windows") +pub fn KeyRecord::repeat_count(self : KeyRecord) -> Int { + if self.repeat_count > 0 { + self.repeat_count + } else { + 1 + } +} + +///| +#cfg(platform="windows") +pub fn KeyRecord::text_uses_decoder(self : KeyRecord) -> Bool { + self.key_down != 0 && !self.has_direct_key_metadata() +} + +///| +#cfg(platform="windows") +fn KeyRecord::has_direct_key_metadata(self : KeyRecord) -> Bool { + (self.has_keyboard_modifier() && !self.is_alt_gr_text()) || self.is_keypad() +} + +///| +#cfg(platform="windows") +fn KeyRecord::has_keyboard_modifier(self : KeyRecord) -> Bool { + has_state( + self.control_key_state, + ShiftPressed | + LeftAltPressed | + RightAltPressed | + LeftCtrlPressed | + RightCtrlPressed, + ) +} + +///| +#cfg(platform="windows") +fn KeyRecord::is_alt_gr_text(self : KeyRecord) -> Bool { + let alt_gr_state = RightAltPressed | LeftCtrlPressed + self.key_down != 0 && + self.unicode_char > 0x1f && + (self.control_key_state & alt_gr_state) == alt_gr_state && + !has_state(self.control_key_state, LeftAltPressed | RightCtrlPressed) +} + +///| +#cfg(platform="windows") +pub fn KeyRecord::text( + self : KeyRecord, + pending_surrogate : Ref[Int], +) -> String? { + if self.key_down == 0 { + return None + } + let charcode = self.unicode_char + if charcode == 0 { + pending_surrogate.val = 0 + return None + } + if is_high_surrogate(charcode) { + pending_surrogate.val = charcode + return None + } + if is_low_surrogate(charcode) { + let high = pending_surrogate.val + pending_surrogate.val = 0 + guard high != 0 else { return None } + let char = decode_surrogate_pair(high, charcode) + return Some([char]) + } + pending_surrogate.val = 0 + Some([charcode.unsafe_to_char()]) +} + +///| +#cfg(platform="windows") +pub fn KeyRecord::input( + self : KeyRecord, + pending_surrogate : Ref[Int], +) -> @public_input.InputEvent? { + guard self.key_event(pending_surrogate) is Some(event) else { return None } + Some(Key(event)) +} + +///| +#cfg(platform="windows") +fn KeyRecord::key_kind(self : KeyRecord) -> @public_input.KeyEventKind { + if self.key_down == 0 { + Release + } else if self.repeat_count > 1 { + Repeat + } else { + Press + } +} + +///| +#cfg(platform="windows") +fn KeyRecord::modifiers(self : KeyRecord) -> @public_input.KeyModifiers { + modifiers_from_control_key_state(self.control_key_state) +} + +///| +#cfg(platform="windows") +fn KeyRecord::key_state(self : KeyRecord) -> @public_input.KeyEventState { + @public_input.KeyEventState::new( + keypad=self.is_keypad(), + caps_lock=has_state(self.control_key_state, CapsLockOn), + num_lock=has_state(self.control_key_state, NumLockOn), + ) +} + +///| +#cfg(platform="windows") +fn KeyRecord::key_event( + self : KeyRecord, + pending_surrogate : Ref[Int], +) -> @public_input.KeyEvent? { + let kind = self.key_kind() + let modifiers = self.modifiers() + let state = self.key_state() + match self.key_code_from_virtual_key() { + Some(code) => { + pending_surrogate.val = 0 + return Some(@public_input.KeyEvent::new(code, modifiers~, kind~, state~)) + } + None => () + } + let charcode = self.unicode_char + if charcode == 0 { + pending_surrogate.val = 0 + return control_code_from_virtual_key(self.virtual_key_code).map(code => { + @public_input.KeyEvent::new(code, modifiers~, kind~, state~) + }) + } + if charcode >= 0x00 && charcode <= 0x1f { + pending_surrogate.val = 0 + return control_code_from_char(charcode).map(code => { + @public_input.KeyEvent::new(code, modifiers~, kind~, state~) + }) + } + if is_high_surrogate(charcode) { + pending_surrogate.val = charcode + return None + } + if is_low_surrogate(charcode) { + let high = pending_surrogate.val + pending_surrogate.val = 0 + guard high != 0 else { return None } + let char = decode_surrogate_pair(high, charcode) + return Some(self.char_key_event(char, modifiers, kind, state)) + } + pending_surrogate.val = 0 + let char = charcode.unsafe_to_char() + Some(self.char_key_event(char, modifiers, kind, state)) +} + +///| +#cfg(platform="windows") +fn KeyRecord::key_code_from_virtual_key( + self : KeyRecord, +) -> @public_input.KeyCode? { + if self.is_keypad_enter() { + Some(KeypadEnter) + } else { + key_code_from_virtual_key(self.virtual_key_code) + } +} + +///| +#cfg(platform="windows") +fn KeyRecord::is_keypad(self : KeyRecord) -> Bool { + virtual_key_is_keypad(self.virtual_key_code) || + self.is_keypad_enter() || + self.is_keypad_navigation() +} + +///| +#cfg(platform="windows") +fn KeyRecord::is_keypad_enter(self : KeyRecord) -> Bool { + self.virtual_key_code == 0x0d && + has_state(self.control_key_state, EnhancedKey) +} + +///| +#cfg(platform="windows") +fn KeyRecord::is_keypad_navigation(self : KeyRecord) -> Bool { + virtual_key_is_navigation(self.virtual_key_code) && + !has_state(self.control_key_state, EnhancedKey) +} + +///| +#cfg(platform="windows") +fn KeyRecord::char_key_event( + self : KeyRecord, + char : Char, + modifiers : @public_input.KeyModifiers, + kind : @public_input.KeyEventKind, + state : @public_input.KeyEventState, +) -> @public_input.KeyEvent { + match text_for_key(self, char) { + Some(text) => + @public_input.KeyEvent::new(Char(char), modifiers~, kind~, state~, text~) + None => @public_input.KeyEvent::new(Char(char), modifiers~, kind~, state~) + } +} + +///| +#cfg(platform="windows") +fn text_for_key(record : KeyRecord, char : Char) -> String? { + if record.key_down == 0 { + None + } else { + Some([char]) + } +} + +///| +#cfg(platform="windows") +fn key_code_from_virtual_key(vk : Int) -> @public_input.KeyCode? { + if vk >= 0x70 && vk <= 0x87 { + return function_key(vk - 0x6f) + } + match vk { + 0x08 => Some(Backspace) + 0x09 => Some(Tab) + 0x0d => Some(Enter) + 0x13 => Some(Pause) + 0x1b => Some(Escape) + 0x21 => Some(PageUp) + 0x22 => Some(PageDown) + 0x23 => Some(End) + 0x24 => Some(Home) + 0x25 => Some(Left) + 0x26 => Some(Up) + 0x27 => Some(Right) + 0x28 => Some(Down) + 0x2c => Some(PrintScreen) + 0x2d => Some(Insert) + 0x2e => Some(Delete) + 0x5d => Some(Menu) + 0x6a => Some(KeypadMultiply) + 0x6b => Some(KeypadPlus) + 0x6c => Some(KeypadSeparator) + 0x6d => Some(KeypadMinus) + 0x6e => Some(KeypadDecimal) + 0x6f => Some(KeypadDivide) + 0x90 => Some(NumLock) + 0x91 => Some(ScrollLock) + _ => if vk >= 0x60 && vk <= 0x69 { keypad_digit(vk - 0x60) } else { None } + } +} + +///| +#cfg(platform="windows") +fn control_code_from_char(charcode : Int) -> @public_input.KeyCode? { + match charcode { + 0x00 => Some(Char(' ')) + 0x01..=0x1a => Some(Char((charcode - 1 + 'a'.to_int()).unsafe_to_char())) + 0x1b => Some(Escape) + 0x1c..=0x1f => Some(Char((charcode - 0x1c + '4'.to_int()).unsafe_to_char())) + _ => None + } +} + +///| +#cfg(platform="windows") +fn control_code_from_virtual_key(vk : Int) -> @public_input.KeyCode? { + if vk == 0x20 { + Some(Char(' ')) + } else if vk >= 0x41 && vk <= 0x5a { + Some(Char((vk - 0x41 + 'a'.to_int()).unsafe_to_char())) + } else { + None + } +} + +///| +#cfg(platform="windows") +fn function_key(number : Int) -> @public_input.KeyCode? { + match number { + 1 => Some(F1) + 2 => Some(F2) + 3 => Some(F3) + 4 => Some(F4) + 5 => Some(F5) + 6 => Some(F6) + 7 => Some(F7) + 8 => Some(F8) + 9 => Some(F9) + 10 => Some(F10) + 11 => Some(F11) + 12 => Some(F12) + 13 => Some(F13) + 14 => Some(F14) + 15 => Some(F15) + 16 => Some(F16) + 17 => Some(F17) + 18 => Some(F18) + 19 => Some(F19) + 20 => Some(F20) + 21 => Some(F21) + 22 => Some(F22) + 23 => Some(F23) + 24 => Some(F24) + _ => None + } +} + +///| +#cfg(platform="windows") +fn keypad_digit(number : Int) -> @public_input.KeyCode? { + match number { + 0 => Some(Keypad0) + 1 => Some(Keypad1) + 2 => Some(Keypad2) + 3 => Some(Keypad3) + 4 => Some(Keypad4) + 5 => Some(Keypad5) + 6 => Some(Keypad6) + 7 => Some(Keypad7) + 8 => Some(Keypad8) + 9 => Some(Keypad9) + _ => None + } +} + +///| +#cfg(platform="windows") +fn virtual_key_is_keypad(vk : Int) -> Bool { + vk >= 0x60 && vk <= 0x6f +} + +///| +#cfg(platform="windows") +fn virtual_key_is_navigation(vk : Int) -> Bool { + match vk { + 0x21 | 0x22 | 0x23 | 0x24 | 0x25 | 0x26 | 0x27 | 0x28 | 0x2d | 0x2e => true + _ => false + } +} + +///| +#cfg(platform="windows") +fn has_state(state : Int, mask : Int) -> Bool { + (state & mask) != 0 +} + +///| +#cfg(platform="windows") +fn is_high_surrogate(code : Int) -> Bool { + code >= 0xd800 && code <= 0xdbff +} + +///| +#cfg(platform="windows") +fn is_low_surrogate(code : Int) -> Bool { + code >= 0xdc00 && code <= 0xdfff +} + +///| +#cfg(platform="windows") +fn decode_surrogate_pair(high : Int, low : Int) -> Char { + let code = 0x10000 + (high - 0xd800) * 0x400 + (low - 0xdc00) + code.unsafe_to_char() +} diff --git a/internal/win32/moon.pkg b/internal/win32/moon.pkg new file mode 100644 index 0000000..fb29af8 --- /dev/null +++ b/internal/win32/moon.pkg @@ -0,0 +1,7 @@ +import { + "moonbit-community/tty/input" @public_input, +} + +import { + "moonbitlang/core/debug", +} for "wbtest" diff --git a/internal/win32/mouse.mbt b/internal/win32/mouse.mbt new file mode 100644 index 0000000..cab7509 --- /dev/null +++ b/internal/win32/mouse.mbt @@ -0,0 +1,155 @@ +///| +#cfg(platform="windows") +pub fn MouseButtonState::input_event( + self : MouseButtonState, + record : MouseRecord, +) -> @public_input.InputEvent? { + match self.event_kind(record) { + Some(kind) => + Some( + Mouse( + @public_input.MouseEvent::new( + kind, + record.y + 1, + record.x + 1, + modifiers=record.modifiers(), + ), + ), + ) + None => None + } +} + +///| +#cfg(platform="windows") +fn MouseButtonState::event_kind( + self : MouseButtonState, + record : MouseRecord, +) -> @public_input.MouseEventKind? { + let buttons = record.button_state & MouseButtonMask + match record.event_flags { + 0 => { + let previous = self.buttons + self.buttons = buttons + match newly_pressed_mouse_button(previous, buttons) { + Some(button) => Some(Press(button)) + None => + newly_released_mouse_button(previous, buttons).map(button => { + Release(button) + }) + } + } + MouseMoved => { + self.buttons = buttons + match mouse_button_from_state(buttons) { + Some(button) => Some(Drag(button)) + None => Some(Move) + } + } + MouseDoubleClick => { + self.buttons = buttons + mouse_button_from_state(buttons).map(button => Press(button)) + } + MouseWheeled => { + self.buttons = buttons + let delta = signed_high_word(record.button_state) + if delta > 0 { + Some(Scroll(Up)) + } else if delta < 0 { + Some(Scroll(Down)) + } else { + None + } + } + MouseHorizontalWheeled => { + self.buttons = buttons + let delta = signed_high_word(record.button_state) + if delta > 0 { + Some(Scroll(Right)) + } else if delta < 0 { + Some(Scroll(Left)) + } else { + None + } + } + _ => { + self.buttons = buttons + None + } + } +} + +///| +#cfg(platform="windows") +fn MouseRecord::modifiers(self : MouseRecord) -> @public_input.KeyModifiers { + modifiers_from_control_key_state(self.control_key_state) +} + +///| +#cfg(platform="windows") +fn newly_pressed_mouse_button( + previous : Int, + buttons : Int, +) -> @public_input.MouseButton? { + if (buttons & MouseLeftButton) != 0 && (previous & MouseLeftButton) == 0 { + Some(Left) + } else if (buttons & MouseMiddleButton) != 0 && + (previous & MouseMiddleButton) == 0 { + Some(Middle) + } else if (buttons & MouseRightButton) != 0 && + (previous & MouseRightButton) == 0 { + Some(Right) + } else { + None + } +} + +///| +#cfg(platform="windows") +fn newly_released_mouse_button( + previous : Int, + buttons : Int, +) -> @public_input.MouseButton? { + if (previous & MouseLeftButton) != 0 && (buttons & MouseLeftButton) == 0 { + Some(Left) + } else if (previous & MouseMiddleButton) != 0 && + (buttons & MouseMiddleButton) == 0 { + Some(Middle) + } else if (previous & MouseRightButton) != 0 && + (buttons & MouseRightButton) == 0 { + Some(Right) + } else { + None + } +} + +///| +#cfg(platform="windows") +fn mouse_button_from_state(state : Int) -> @public_input.MouseButton? { + if (state & MouseLeftButton) != 0 { + Some(Left) + } else if (state & MouseMiddleButton) != 0 { + Some(Middle) + } else if (state & MouseRightButton) != 0 { + Some(Right) + } else { + None + } +} + +///| +#cfg(platform="windows") +fn signed_high_word(value : Int) -> Int { + let low_word = value & 0xffff + (value - low_word) / 0x10000 +} + +///| +#cfg(platform="windows") +fn modifiers_from_control_key_state(state : Int) -> @public_input.KeyModifiers { + @public_input.KeyModifiers::new( + shift=has_state(state, ShiftPressed), + alt=has_state(state, LeftAltPressed | RightAltPressed), + ctrl=has_state(state, LeftCtrlPressed | RightCtrlPressed), + ) +} diff --git a/internal/win32/pkg.generated.mbti b/internal/win32/pkg.generated.mbti new file mode 100644 index 0000000..6815646 --- /dev/null +++ b/internal/win32/pkg.generated.mbti @@ -0,0 +1,101 @@ +// Generated using `moon info`, DON'T EDIT IT +package "moonbit-community/tty/internal/win32" + +import { + "moonbit-community/tty/input", + "moonbitlang/core/ref", +} + +// Values +pub const CapsLockOn : Int = 0x0080 + +pub const EnhancedKey : Int = 0x0100 + +pub const LeftAltPressed : Int = 0x0002 + +pub const LeftCtrlPressed : Int = 0x0008 + +pub const MouseDoubleClick : Int = 0x0002 + +pub const MouseHorizontalWheeled : Int = 0x0008 + +pub const MouseLeftButton : Int = 0x0001 + +pub const MouseMiddleButton : Int = 0x0004 + +pub const MouseMoved : Int = 0x0001 + +pub const MouseRightButton : Int = 0x0002 + +pub const MouseWheeled : Int = 0x0004 + +pub const NumLockOn : Int = 0x0020 + +pub const RecordFocus : Int = 0x0010 + +pub const RecordKey : Int = 0x0001 + +pub const RecordMouse : Int = 0x0002 + +pub const RecordWindowBufferSize : Int = 0x0004 + +pub const RightAltPressed : Int = 0x0001 + +pub const RightCtrlPressed : Int = 0x0004 + +pub const ShiftPressed : Int = 0x0010 + +// Errors + +// Types and methods +pub struct Coord { + x : Int + y : Int +} + +pub(all) enum InputRecord { + Key(KeyRecord) + Mouse(MouseRecord) + WindowBufferSize(Coord) + Focus(Bool) + Unsupported(Int) +} + +pub struct KeyRecord { + key_down : Int + repeat_count : Int + virtual_key_code : Int + virtual_scan_code : Int + unicode_char : Int + control_key_state : Int +} +pub fn KeyRecord::input(Self, @ref.Ref[Int]) -> @input.InputEvent? +pub fn KeyRecord::is_down(Self) -> Bool +pub fn KeyRecord::new(key_down? : Int, repeat_count? : Int, virtual_key_code? : Int, virtual_scan_code? : Int, unicode_char? : Int, control_key_state? : Int) -> Self +pub fn KeyRecord::repeat_count(Self) -> Int +pub fn KeyRecord::text(Self, @ref.Ref[Int]) -> String? +pub fn KeyRecord::text_uses_decoder(Self) -> Bool + +pub struct MouseButtonState { + mut buttons : Int +} +pub fn MouseButtonState::input_event(Self, MouseRecord) -> @input.InputEvent? +pub fn MouseButtonState::new() -> Self + +pub struct MouseRecord { + x : Int + y : Int + button_state : Int + control_key_state : Int + event_flags : Int +} +pub fn MouseRecord::new(x? : Int, y? : Int, button_state? : Int, control_key_state? : Int, event_flags? : Int) -> Self + +pub(all) struct RawInputRecord(Bytes) +pub fn RawInputRecord::new(Int) -> Self +pub fn RawInputRecord::parse(Self) -> InputRecord + +// Type aliases + +// Traits + diff --git a/internal/win32/record.mbt b/internal/win32/record.mbt new file mode 100644 index 0000000..54c7aaf --- /dev/null +++ b/internal/win32/record.mbt @@ -0,0 +1,292 @@ +///| +#cfg(platform="windows") +pub const RecordKey : Int = 0x0001 + +///| +#cfg(platform="windows") +pub const RecordMouse : Int = 0x0002 + +///| +#cfg(platform="windows") +pub const RecordWindowBufferSize : Int = 0x0004 + +///| +#cfg(platform="windows") +pub const RecordFocus : Int = 0x0010 + +///| +#cfg(platform="windows") +pub const RightAltPressed : Int = 0x0001 + +///| +#cfg(platform="windows") +pub const LeftAltPressed : Int = 0x0002 + +///| +#cfg(platform="windows") +pub const RightCtrlPressed : Int = 0x0004 + +///| +#cfg(platform="windows") +pub const LeftCtrlPressed : Int = 0x0008 + +///| +#cfg(platform="windows") +pub const ShiftPressed : Int = 0x0010 + +///| +#cfg(platform="windows") +pub const NumLockOn : Int = 0x0020 + +///| +#cfg(platform="windows") +pub const CapsLockOn : Int = 0x0080 + +///| +#cfg(platform="windows") +pub const EnhancedKey : Int = 0x0100 + +///| +#cfg(platform="windows") +pub const MouseMoved : Int = 0x0001 + +///| +#cfg(platform="windows") +pub const MouseDoubleClick : Int = 0x0002 + +///| +#cfg(platform="windows") +pub const MouseWheeled : Int = 0x0004 + +///| +#cfg(platform="windows") +pub const MouseHorizontalWheeled : Int = 0x0008 + +///| +#cfg(platform="windows") +pub const MouseLeftButton : Int = 0x0001 + +///| +#cfg(platform="windows") +pub const MouseRightButton : Int = 0x0002 + +///| +#cfg(platform="windows") +pub const MouseMiddleButton : Int = 0x0004 + +///| +#cfg(platform="windows") +const MouseButtonMask : Int = 0x0007 + +///| +#cfg(platform="windows") +pub(all) struct RawInputRecord(Bytes) + +///| +#cfg(platform="windows") +pub(all) enum InputRecord { + Key(KeyRecord) + Mouse(MouseRecord) + WindowBufferSize(Coord) + Focus(Bool) + Unsupported(Int) +} + +///| +#cfg(platform="windows") +pub struct Coord { + x : Int + y : Int +} + +///| +#cfg(platform="windows") +pub struct KeyRecord { + key_down : Int + repeat_count : Int + virtual_key_code : Int + virtual_scan_code : Int + unicode_char : Int + control_key_state : Int +} + +///| +#cfg(platform="windows") +pub struct MouseRecord { + x : Int + y : Int + button_state : Int + control_key_state : Int + event_flags : Int +} + +///| +#cfg(platform="windows") +pub struct MouseButtonState { + mut buttons : Int +} + +///| +#cfg(platform="windows") +pub fn KeyRecord::new( + key_down? : Int = 1, + repeat_count? : Int = 1, + virtual_key_code? : Int = 0, + virtual_scan_code? : Int = 0, + unicode_char? : Int = 0, + control_key_state? : Int = 0, +) -> KeyRecord { + { + key_down, + repeat_count, + virtual_key_code, + virtual_scan_code, + unicode_char, + control_key_state, + } +} + +///| +#cfg(platform="windows") +pub fn MouseRecord::new( + x? : Int = 0, + y? : Int = 0, + button_state? : Int = 0, + control_key_state? : Int = 0, + event_flags? : Int = 0, +) -> MouseRecord { + { x, y, button_state, control_key_state, event_flags } +} + +///| +#cfg(platform="windows") +pub fn MouseButtonState::new() -> MouseButtonState { + { buttons: 0 } +} + +///| +#cfg(platform="windows") +pub fn RawInputRecord::new(size : Int) -> RawInputRecord { + RawInputRecord(Bytes::make(size, 0)) +} + +///| +#cfg(platform="windows") +pub fn RawInputRecord::parse(self : RawInputRecord) -> InputRecord { + let bytes = self.0[:] + guard u16_at(bytes, 0) is Some(event_type) else { return Unsupported(0) } + match event_type { + RecordKey => parse_key_record(bytes) + RecordMouse => parse_mouse_record(bytes) + RecordWindowBufferSize => parse_window_buffer_size_record(bytes) + RecordFocus => parse_focus_record(bytes) + _ => Unsupported(event_type) + } +} + +///| +#cfg(platform="windows") +fn parse_key_record(bytes : BytesView) -> InputRecord { + guard bool_at(bytes, 4) is Some(key_down) else { + return Unsupported(RecordKey) + } + guard u16_at(bytes, 8) is Some(repeat_count) else { + return Unsupported(RecordKey) + } + guard u16_at(bytes, 10) is Some(virtual_key_code) else { + return Unsupported(RecordKey) + } + guard u16_at(bytes, 12) is Some(virtual_scan_code) else { + return Unsupported(RecordKey) + } + guard u16_at(bytes, 14) is Some(unicode_char) else { + return Unsupported(RecordKey) + } + guard dword_at(bytes, 16) is Some(control_key_state) else { + return Unsupported(RecordKey) + } + Key( + KeyRecord::new( + key_down=if key_down { 1 } else { 0 }, + repeat_count~, + virtual_key_code~, + virtual_scan_code~, + unicode_char~, + control_key_state~, + ), + ) +} + +///| +#cfg(platform="windows") +fn parse_mouse_record(bytes : BytesView) -> InputRecord { + guard i16_at(bytes, 4) is Some(x) else { return Unsupported(RecordMouse) } + guard i16_at(bytes, 6) is Some(y) else { return Unsupported(RecordMouse) } + guard dword_at(bytes, 8) is Some(button_state) else { + return Unsupported(RecordMouse) + } + guard dword_at(bytes, 12) is Some(control_key_state) else { + return Unsupported(RecordMouse) + } + guard dword_at(bytes, 16) is Some(event_flags) else { + return Unsupported(RecordMouse) + } + Mouse( + MouseRecord::new(x~, y~, button_state~, control_key_state~, event_flags~), + ) +} + +///| +#cfg(platform="windows") +fn parse_window_buffer_size_record(bytes : BytesView) -> InputRecord { + guard i16_at(bytes, 4) is Some(x) else { + return Unsupported(RecordWindowBufferSize) + } + guard i16_at(bytes, 6) is Some(y) else { + return Unsupported(RecordWindowBufferSize) + } + WindowBufferSize({ x, y }) +} + +///| +#cfg(platform="windows") +fn parse_focus_record(bytes : BytesView) -> InputRecord { + guard bool_at(bytes, 4) is Some(focus_set) else { + return Unsupported(RecordFocus) + } + Focus(focus_set) +} + +///| +#cfg(platform="windows") +fn bool_at(bytes : BytesView, offset : Int) -> Bool? { + dword_at(bytes, offset).map(value => value != 0) +} + +///| +#cfg(platform="windows") +fn i16_at(bytes : BytesView, offset : Int) -> Int? { + guard u16_at(bytes, offset) is Some(value) else { return None } + if value >= 0x8000 { + Some(value - 0x10000) + } else { + Some(value) + } +} + +///| +#cfg(platform="windows") +fn u16_at(bytes : BytesView, offset : Int) -> Int? { + if offset < 0 || offset + 1 >= bytes.length() { + return None + } + Some(bytes[offset].to_int() + bytes[offset + 1].to_int() * 0x100) +} + +///| +#cfg(platform="windows") +fn dword_at(bytes : BytesView, offset : Int) -> Int? { + guard u16_at(bytes, offset) is Some(low) else { return None } + guard i16_at(bytes, offset + 2) is Some(high) else { return None } + Some(low + high * 0x10000) +} diff --git a/internal/win32/record_wbtest.mbt b/internal/win32/record_wbtest.mbt new file mode 100644 index 0000000..40f7a47 --- /dev/null +++ b/internal/win32/record_wbtest.mbt @@ -0,0 +1,84 @@ +///| +#cfg(platform="windows") +fn test_raw_record(event_type : Int) -> FixedArray[Byte] { + let buf = FixedArray::make(20, b'\x00') + test_set_u16_le(buf, 0, event_type) + buf +} + +///| +#cfg(platform="windows") +fn test_raw_input_record(buf : FixedArray[Byte]) -> RawInputRecord { + RawInputRecord(buf.unsafe_reinterpret_as_bytes()) +} + +///| +#cfg(platform="windows") +fn test_set_u16_le(buf : FixedArray[Byte], offset : Int, value : Int) -> Unit { + buf[offset] = (value & 0xff).to_byte() + buf[offset + 1] = ((value / 0x100) & 0xff).to_byte() +} + +///| +#cfg(platform="windows") +fn test_set_u32_le(buf : FixedArray[Byte], offset : Int, value : Int) -> Unit { + buf[offset] = (value & 0xff).to_byte() + buf[offset + 1] = ((value / 0x100) & 0xff).to_byte() + buf[offset + 2] = ((value / 0x10000) & 0xff).to_byte() + buf[offset + 3] = ((value / 0x1000000) & 0xff).to_byte() +} + +///| +#cfg(platform="windows") +test "win32 raw input record parses key records" { + let buf = test_raw_record(RecordKey) + test_set_u32_le(buf, 4, 1) + test_set_u16_le(buf, 8, 2) + test_set_u16_le(buf, 10, 0x41) + test_set_u16_le(buf, 12, 0x1e) + test_set_u16_le(buf, 14, 'A'.to_int()) + test_set_u32_le(buf, 16, ShiftPressed) + match test_raw_input_record(buf).parse() { + Key(record) => { + @debug.assert_eq(record.key_down, 1) + @debug.assert_eq(record.repeat_count, 2) + @debug.assert_eq(record.virtual_key_code, 0x41) + @debug.assert_eq(record.virtual_scan_code, 0x1e) + @debug.assert_eq(record.unicode_char, 'A'.to_int()) + @debug.assert_eq(record.control_key_state, ShiftPressed) + } + _ => fail("expected key record") + } +} + +///| +#cfg(platform="windows") +test "win32 raw input record parses mouse records" { + let buf = test_raw_record(RecordMouse) + test_set_u16_le(buf, 4, 30) + test_set_u16_le(buf, 6, 2) + test_set_u32_le(buf, 8, MouseLeftButton) + test_set_u32_le(buf, 12, ShiftPressed) + test_set_u32_le(buf, 16, MouseMoved) + match test_raw_input_record(buf).parse() { + Mouse(record) => { + @debug.assert_eq(record.x, 30) + @debug.assert_eq(record.y, 2) + @debug.assert_eq(record.button_state, MouseLeftButton) + @debug.assert_eq(record.control_key_state, ShiftPressed) + @debug.assert_eq(record.event_flags, MouseMoved) + } + _ => fail("expected mouse record") + } +} + +///| +#cfg(platform="windows") +test "win32 raw input record parses focus records" { + let buf = test_raw_record(RecordFocus) + test_set_u32_le(buf, 4, 1) + match test_raw_input_record(buf).parse() { + Focus(true) => () + _ => fail("expected focus record") + } +} diff --git a/moon.pkg b/moon.pkg index ed4587c..da52b3f 100644 --- a/moon.pkg +++ b/moon.pkg @@ -10,6 +10,7 @@ import { "moonbit-community/tty/color", "moonbit-community/tty/input" @public_input, "moonbit-community/tty/internal/input", + "moonbit-community/tty/internal/win32", "moonbit-community/tty/internal/vt", } diff --git a/win32_input.mbt b/win32_input.mbt index 8267cba..da04535 100644 --- a/win32_input.mbt +++ b/win32_input.mbt @@ -1,131 +1,3 @@ -///| -#cfg(platform="windows") -const Win32RecordKey : Int = 0x0001 - -///| -#cfg(platform="windows") -const Win32RecordMouse : Int = 0x0002 - -///| -#cfg(platform="windows") -const Win32RecordWindowBufferSize : Int = 0x0004 - -///| -#cfg(platform="windows") -const Win32RecordFocus : Int = 0x0010 - -///| -#cfg(platform="windows") -const Win32RightAltPressed : Int = 0x0001 - -///| -#cfg(platform="windows") -const Win32LeftAltPressed : Int = 0x0002 - -///| -#cfg(platform="windows") -const Win32RightCtrlPressed : Int = 0x0004 - -///| -#cfg(platform="windows") -const Win32LeftCtrlPressed : Int = 0x0008 - -///| -#cfg(platform="windows") -const Win32ShiftPressed : Int = 0x0010 - -///| -#cfg(platform="windows") -const Win32NumLockOn : Int = 0x0020 - -///| -#cfg(platform="windows") -const Win32CapsLockOn : Int = 0x0080 - -///| -#cfg(platform="windows") -const Win32EnhancedKey : Int = 0x0100 - -///| -#cfg(platform="windows") -const Win32MouseMoved : Int = 0x0001 - -///| -#cfg(platform="windows") -const Win32MouseDoubleClick : Int = 0x0002 - -///| -#cfg(platform="windows") -const Win32MouseWheeled : Int = 0x0004 - -///| -#cfg(platform="windows") -const Win32MouseHorizontalWheeled : Int = 0x0008 - -///| -#cfg(platform="windows") -const Win32MouseLeftButton : Int = 0x0001 - -///| -#cfg(platform="windows") -const Win32MouseRightButton : Int = 0x0002 - -///| -#cfg(platform="windows") -const Win32MouseMiddleButton : Int = 0x0004 - -///| -#cfg(platform="windows") -const Win32MouseButtonMask : Int = 0x0007 - -///| -#cfg(platform="windows") -priv struct Win32RawInputRecord(Bytes) - -///| -#cfg(platform="windows") -priv enum Win32InputRecord { - Key(Win32KeyRecord) - Mouse(Win32MouseRecord) - WindowBufferSize(Win32Coord) - Focus(Bool) - Unsupported(Int) -} - -///| -#cfg(platform="windows") -priv struct Win32Coord { - x : Int - y : Int -} - -///| -#cfg(platform="windows") -priv struct Win32KeyRecord { - key_down : Int - repeat_count : Int - virtual_key_code : Int - virtual_scan_code : Int - unicode_char : Int - control_key_state : Int -} - -///| -#cfg(platform="windows") -priv struct Win32MouseRecord { - x : Int - y : Int - button_state : Int - control_key_state : Int - event_flags : Int -} - -///| -#cfg(platform="windows") -priv struct Win32MouseButtonState { - mut buttons : Int -} - ///| #cfg(platform="windows") priv enum WindowsInput { @@ -136,7 +8,7 @@ priv enum WindowsInput { byte_write~ : @async/io.PipeWrite, events~ : @async.Queue[Event], pending_surrogate~ : Ref[Int], - mouse_button_state~ : Win32MouseButtonState + mouse_button_state~ : @win32.MouseButtonState ) } @@ -160,7 +32,7 @@ fn WindowsInput::console(input_fd : @async/types.Fd) -> WindowsInput { byte_write~, events=Queue(kind=Unbounded), pending_surrogate=Ref(0), - mouse_button_state={ buttons: 0 }, + mouse_button_state=@win32.MouseButtonState::new(), ) } @@ -182,17 +54,7 @@ fn WindowsInput::event_reader( fn WindowsInput::close(self : WindowsInput) -> Unit { match self { ByteStream => () - Console( - input_fd~, - byte_read~, - byte_write~, - events~, - pending_surrogate~, - mouse_button_state~ - ) => { - ignore(input_fd) - ignore(pending_surrogate.val) - ignore(mouse_button_state.buttons) + Console(byte_read~, byte_write~, events~, ..) => { byte_read.close() byte_write.close() events.close(clear=true) @@ -283,7 +145,7 @@ async fn Tty::read_records(self : Self) -> Unit { ///| #cfg(platform="windows") -async fn Tty::read_record(self : Self, record : Win32InputRecord) -> Unit { +async fn Tty::read_record(self : Self, record : @win32.InputRecord) -> Unit { match self.input_backend { Console(events~, pending_surrogate~, ..) => match record { @@ -313,19 +175,18 @@ async fn Tty::read_record(self : Self, record : Win32InputRecord) -> Unit { ///| #cfg(platform="windows") -async fn Tty::read_key_record(self : Self, record : Win32KeyRecord) -> Unit { +async fn Tty::read_key_record(self : Self, record : @win32.KeyRecord) -> Unit { match self.input_backend { Console(byte_write~, pending_surrogate~, ..) => { - ignore(record.virtual_scan_code) - if record.key_down == 0 { + if !record.is_down() { return } - if !record.win32_key_text_uses_decoder() { + if !record.text_uses_decoder() { return self.queue_key_input(record) } - match record.win32_key_text(pending_surrogate) { + match record.text(pending_surrogate) { Some(text) => - for _ in 0.. self.queue_key_input(record) @@ -337,12 +198,12 @@ async fn Tty::read_key_record(self : Self, record : Win32KeyRecord) -> Unit { ///| #cfg(platform="windows") -async fn Tty::queue_key_input(self : Self, record : Win32KeyRecord) -> Unit { +async fn Tty::queue_key_input(self : Self, record : @win32.KeyRecord) -> Unit { match self.input_backend { Console(events~, pending_surrogate~, ..) => - match record.win32_key_input(pending_surrogate) { + match record.input(pending_surrogate) { Some(input) => - for _ in 0.. () @@ -353,24 +214,15 @@ async fn Tty::queue_key_input(self : Self, record : Win32KeyRecord) -> Unit { ///| #cfg(platform="windows") -async fn Tty::read_mouse_record(self : Self, record : Win32MouseRecord) -> Unit { +async fn Tty::read_mouse_record( + self : Self, + record : @win32.MouseRecord, +) -> Unit { match self.input_backend { Console(events~, pending_surrogate~, mouse_button_state~, ..) => { pending_surrogate.val = 0 - match mouse_button_state.event_kind(record) { - Some(kind) => - events.put( - Input( - Mouse( - @public_input.MouseEvent::new( - kind, - record.y + 1, - record.x + 1, - modifiers=record.win32_modifiers(), - ), - ), - ), - ) + match mouse_button_state.input_event(record) { + Some(input) => events.put(Input(input)) None => () } } @@ -378,663 +230,6 @@ async fn Tty::read_mouse_record(self : Self, record : Win32MouseRecord) -> Unit } } -///| -#cfg(platform="windows") -fn Win32MouseButtonState::event_kind( - self : Win32MouseButtonState, - record : Win32MouseRecord, -) -> @public_input.MouseEventKind? { - let buttons = record.button_state & Win32MouseButtonMask - match record.event_flags { - 0 => { - let previous = self.buttons - self.buttons = buttons - match win32_newly_pressed_mouse_button(previous, buttons) { - Some(button) => Some(Press(button)) - None => - win32_newly_released_mouse_button(previous, buttons).map(button => { - Release(button) - }) - } - } - Win32MouseMoved => { - self.buttons = buttons - match win32_mouse_button_from_state(buttons) { - Some(button) => Some(Drag(button)) - None => Some(Move) - } - } - Win32MouseDoubleClick => { - self.buttons = buttons - win32_mouse_button_from_state(buttons).map(button => Press(button)) - } - Win32MouseWheeled => { - self.buttons = buttons - let delta = win32_signed_high_word(record.button_state) - if delta > 0 { - Some(Scroll(Up)) - } else if delta < 0 { - Some(Scroll(Down)) - } else { - None - } - } - Win32MouseHorizontalWheeled => { - self.buttons = buttons - let delta = win32_signed_high_word(record.button_state) - if delta > 0 { - Some(Scroll(Right)) - } else if delta < 0 { - Some(Scroll(Left)) - } else { - None - } - } - _ => { - self.buttons = buttons - None - } - } -} - -///| -#cfg(platform="windows") -fn Win32MouseRecord::win32_modifiers( - self : Win32MouseRecord, -) -> @public_input.KeyModifiers { - win32_modifiers_from_control_key_state(self.control_key_state) -} - -///| -#cfg(platform="windows") -fn win32_newly_pressed_mouse_button( - previous : Int, - buttons : Int, -) -> @public_input.MouseButton? { - if (buttons & Win32MouseLeftButton) != 0 && - (previous & Win32MouseLeftButton) == 0 { - Some(Left) - } else if (buttons & Win32MouseMiddleButton) != 0 && - (previous & Win32MouseMiddleButton) == 0 { - Some(Middle) - } else if (buttons & Win32MouseRightButton) != 0 && - (previous & Win32MouseRightButton) == 0 { - Some(Right) - } else { - None - } -} - -///| -#cfg(platform="windows") -fn win32_newly_released_mouse_button( - previous : Int, - buttons : Int, -) -> @public_input.MouseButton? { - if (previous & Win32MouseLeftButton) != 0 && - (buttons & Win32MouseLeftButton) == 0 { - Some(Left) - } else if (previous & Win32MouseMiddleButton) != 0 && - (buttons & Win32MouseMiddleButton) == 0 { - Some(Middle) - } else if (previous & Win32MouseRightButton) != 0 && - (buttons & Win32MouseRightButton) == 0 { - Some(Right) - } else { - None - } -} - -///| -#cfg(platform="windows") -fn win32_mouse_button_from_state(state : Int) -> @public_input.MouseButton? { - if (state & Win32MouseLeftButton) != 0 { - Some(Left) - } else if (state & Win32MouseMiddleButton) != 0 { - Some(Middle) - } else if (state & Win32MouseRightButton) != 0 { - Some(Right) - } else { - None - } -} - -///| -#cfg(platform="windows") -fn win32_signed_high_word(value : Int) -> Int { - let low_word = value & 0xffff - (value - low_word) / 0x10000 -} - -///| -#cfg(platform="windows") -fn win32_modifiers_from_control_key_state( - state : Int, -) -> @public_input.KeyModifiers { - @public_input.KeyModifiers::new( - shift=win32_has_state(state, Win32ShiftPressed), - alt=win32_has_state(state, Win32LeftAltPressed | Win32RightAltPressed), - ctrl=win32_has_state(state, Win32LeftCtrlPressed | Win32RightCtrlPressed), - ) -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_repeat_count(self : Win32KeyRecord) -> Int { - if self.repeat_count > 0 { - self.repeat_count - } else { - 1 - } -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_key_text_uses_decoder(self : Win32KeyRecord) -> Bool { - self.key_down != 0 && !self.win32_has_direct_key_metadata() -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_has_direct_key_metadata(self : Win32KeyRecord) -> Bool { - (self.win32_has_keyboard_modifier() && !self.win32_is_alt_gr_text()) || - self.win32_is_keypad() -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_has_keyboard_modifier(self : Win32KeyRecord) -> Bool { - win32_has_state( - self.control_key_state, - Win32ShiftPressed | - Win32LeftAltPressed | - Win32RightAltPressed | - Win32LeftCtrlPressed | - Win32RightCtrlPressed, - ) -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_is_alt_gr_text(self : Win32KeyRecord) -> Bool { - let alt_gr_state = Win32RightAltPressed | Win32LeftCtrlPressed - self.key_down != 0 && - self.unicode_char > 0x1f && - (self.control_key_state & alt_gr_state) == alt_gr_state && - !win32_has_state( - self.control_key_state, - Win32LeftAltPressed | Win32RightCtrlPressed, - ) -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_key_text( - self : Win32KeyRecord, - pending_surrogate : Ref[Int], -) -> String? { - if self.key_down == 0 { - return None - } - let charcode = self.unicode_char - if charcode == 0 { - pending_surrogate.val = 0 - return None - } - if win32_is_high_surrogate(charcode) { - pending_surrogate.val = charcode - return None - } - if win32_is_low_surrogate(charcode) { - let high = pending_surrogate.val - pending_surrogate.val = 0 - guard high != 0 else { return None } - let char = win32_decode_surrogate_pair(high, charcode) - return Some([char]) - } - pending_surrogate.val = 0 - Some([charcode.unsafe_to_char()]) -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_key_input( - self : Win32KeyRecord, - pending_surrogate : Ref[Int], -) -> @public_input.InputEvent? { - guard self.win32_key_event(pending_surrogate) is Some(event) else { - return None - } - Some(Key(event)) -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_key_kind( - self : Win32KeyRecord, -) -> @public_input.KeyEventKind { - if self.key_down == 0 { - Release - } else if self.repeat_count > 1 { - Repeat - } else { - Press - } -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_modifiers( - self : Win32KeyRecord, -) -> @public_input.KeyModifiers { - win32_modifiers_from_control_key_state(self.control_key_state) -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_key_state( - self : Win32KeyRecord, -) -> @public_input.KeyEventState { - @public_input.KeyEventState::new( - keypad=self.win32_is_keypad(), - caps_lock=win32_has_state(self.control_key_state, Win32CapsLockOn), - num_lock=win32_has_state(self.control_key_state, Win32NumLockOn), - ) -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_key_event( - self : Win32KeyRecord, - pending_surrogate : Ref[Int], -) -> @public_input.KeyEvent? { - let kind = self.win32_key_kind() - let modifiers = self.win32_modifiers() - let state = self.win32_key_state() - match self.win32_key_code_from_virtual_key() { - Some(code) => { - pending_surrogate.val = 0 - return Some(@public_input.KeyEvent::new(code, modifiers~, kind~, state~)) - } - None => () - } - let charcode = self.unicode_char - if charcode == 0 { - pending_surrogate.val = 0 - return win32_control_code_from_virtual_key(self.virtual_key_code).map(code => { - @public_input.KeyEvent::new(code, modifiers~, kind~, state~) - }) - } - if charcode >= 0x00 && charcode <= 0x1f { - pending_surrogate.val = 0 - return win32_control_code_from_char(charcode).map(code => { - @public_input.KeyEvent::new(code, modifiers~, kind~, state~) - }) - } - if win32_is_high_surrogate(charcode) { - pending_surrogate.val = charcode - return None - } - if win32_is_low_surrogate(charcode) { - let high = pending_surrogate.val - pending_surrogate.val = 0 - guard high != 0 else { return None } - let char = win32_decode_surrogate_pair(high, charcode) - return Some(self.win32_char_key_event(char, modifiers, kind, state)) - } - pending_surrogate.val = 0 - let char = charcode.unsafe_to_char() - Some(self.win32_char_key_event(char, modifiers, kind, state)) -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_key_code_from_virtual_key( - self : Win32KeyRecord, -) -> @public_input.KeyCode? { - if self.win32_is_keypad_enter() { - Some(KeypadEnter) - } else { - win32_key_code_from_virtual_key(self.virtual_key_code) - } -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_is_keypad(self : Win32KeyRecord) -> Bool { - win32_virtual_key_is_keypad(self.virtual_key_code) || - self.win32_is_keypad_enter() || - self.win32_is_keypad_navigation() -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_is_keypad_enter(self : Win32KeyRecord) -> Bool { - self.virtual_key_code == 0x0d && - win32_has_state(self.control_key_state, Win32EnhancedKey) -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_is_keypad_navigation(self : Win32KeyRecord) -> Bool { - win32_virtual_key_is_navigation(self.virtual_key_code) && - !win32_has_state(self.control_key_state, Win32EnhancedKey) -} - -///| -#cfg(platform="windows") -fn Win32KeyRecord::win32_char_key_event( - self : Win32KeyRecord, - char : Char, - modifiers : @public_input.KeyModifiers, - kind : @public_input.KeyEventKind, - state : @public_input.KeyEventState, -) -> @public_input.KeyEvent { - match win32_text_for_key(self, char) { - Some(text) => - @public_input.KeyEvent::new(Char(char), modifiers~, kind~, state~, text~) - None => @public_input.KeyEvent::new(Char(char), modifiers~, kind~, state~) - } -} - -///| -#cfg(platform="windows") -fn win32_text_for_key(record : Win32KeyRecord, char : Char) -> String? { - if record.key_down == 0 { - None - } else { - Some([char]) - } -} - -///| -#cfg(platform="windows") -fn win32_key_code_from_virtual_key(vk : Int) -> @public_input.KeyCode? { - if vk >= 0x70 && vk <= 0x87 { - return win32_function_key(vk - 0x6f) - } - match vk { - 0x08 => Some(Backspace) - 0x09 => Some(Tab) - 0x0d => Some(Enter) - 0x13 => Some(Pause) - 0x1b => Some(Escape) - 0x21 => Some(PageUp) - 0x22 => Some(PageDown) - 0x23 => Some(End) - 0x24 => Some(Home) - 0x25 => Some(Left) - 0x26 => Some(Up) - 0x27 => Some(Right) - 0x28 => Some(Down) - 0x2c => Some(PrintScreen) - 0x2d => Some(Insert) - 0x2e => Some(Delete) - 0x5d => Some(Menu) - 0x6a => Some(KeypadMultiply) - 0x6b => Some(KeypadPlus) - 0x6c => Some(KeypadSeparator) - 0x6d => Some(KeypadMinus) - 0x6e => Some(KeypadDecimal) - 0x6f => Some(KeypadDivide) - 0x90 => Some(NumLock) - 0x91 => Some(ScrollLock) - _ => - if vk >= 0x60 && vk <= 0x69 { - win32_keypad_digit(vk - 0x60) - } else { - None - } - } -} - -///| -#cfg(platform="windows") -fn win32_control_code_from_char(charcode : Int) -> @public_input.KeyCode? { - match charcode { - 0x00 => Some(Char(' ')) - 0x01..=0x1a => Some(Char((charcode - 1 + 'a'.to_int()).unsafe_to_char())) - 0x1b => Some(Escape) - 0x1c..=0x1f => Some(Char((charcode - 0x1c + '4'.to_int()).unsafe_to_char())) - _ => None - } -} - -///| -#cfg(platform="windows") -fn win32_control_code_from_virtual_key(vk : Int) -> @public_input.KeyCode? { - if vk == 0x20 { - Some(Char(' ')) - } else if vk >= 0x41 && vk <= 0x5a { - Some(Char((vk - 0x41 + 'a'.to_int()).unsafe_to_char())) - } else { - None - } -} - -///| -#cfg(platform="windows") -fn win32_function_key(number : Int) -> @public_input.KeyCode? { - match number { - 1 => Some(F1) - 2 => Some(F2) - 3 => Some(F3) - 4 => Some(F4) - 5 => Some(F5) - 6 => Some(F6) - 7 => Some(F7) - 8 => Some(F8) - 9 => Some(F9) - 10 => Some(F10) - 11 => Some(F11) - 12 => Some(F12) - 13 => Some(F13) - 14 => Some(F14) - 15 => Some(F15) - 16 => Some(F16) - 17 => Some(F17) - 18 => Some(F18) - 19 => Some(F19) - 20 => Some(F20) - 21 => Some(F21) - 22 => Some(F22) - 23 => Some(F23) - 24 => Some(F24) - _ => None - } -} - -///| -#cfg(platform="windows") -fn win32_keypad_digit(number : Int) -> @public_input.KeyCode? { - match number { - 0 => Some(Keypad0) - 1 => Some(Keypad1) - 2 => Some(Keypad2) - 3 => Some(Keypad3) - 4 => Some(Keypad4) - 5 => Some(Keypad5) - 6 => Some(Keypad6) - 7 => Some(Keypad7) - 8 => Some(Keypad8) - 9 => Some(Keypad9) - _ => None - } -} - -///| -#cfg(platform="windows") -fn win32_virtual_key_is_keypad(vk : Int) -> Bool { - vk >= 0x60 && vk <= 0x6f -} - -///| -#cfg(platform="windows") -fn win32_virtual_key_is_navigation(vk : Int) -> Bool { - match vk { - 0x21 | 0x22 | 0x23 | 0x24 | 0x25 | 0x26 | 0x27 | 0x28 | 0x2d | 0x2e => true - _ => false - } -} - -///| -#cfg(platform="windows") -fn win32_has_state(state : Int, mask : Int) -> Bool { - (state & mask) != 0 -} - -///| -#cfg(platform="windows") -fn win32_is_high_surrogate(code : Int) -> Bool { - code >= 0xd800 && code <= 0xdbff -} - -///| -#cfg(platform="windows") -fn win32_is_low_surrogate(code : Int) -> Bool { - code >= 0xdc00 && code <= 0xdfff -} - -///| -#cfg(platform="windows") -fn win32_decode_surrogate_pair(high : Int, low : Int) -> Char { - let code = 0x10000 + (high - 0xd800) * 0x400 + (low - 0xdc00) - code.unsafe_to_char() -} - -///| -#cfg(platform="windows") -fn Win32RawInputRecord::new() -> Win32RawInputRecord { - Win32RawInputRecord(Bytes::make(sizeof_win32_input_record, 0)) -} - -///| -#cfg(platform="windows") -fn Win32RawInputRecord::parse(self : Win32RawInputRecord) -> Win32InputRecord { - let bytes = self.0[:] - guard win32_u16_at(bytes, 0) is Some(event_type) else { - return Unsupported(0) - } - match event_type { - Win32RecordKey => win32_parse_key_record(bytes) - Win32RecordMouse => win32_parse_mouse_record(bytes) - Win32RecordWindowBufferSize => win32_parse_window_buffer_size_record(bytes) - Win32RecordFocus => win32_parse_focus_record(bytes) - _ => Unsupported(event_type) - } -} - -///| -#cfg(platform="windows") -fn win32_parse_key_record(bytes : BytesView) -> Win32InputRecord { - guard win32_bool_at(bytes, 4) is Some(key_down) else { - return Unsupported(Win32RecordKey) - } - guard win32_u16_at(bytes, 8) is Some(repeat_count) else { - return Unsupported(Win32RecordKey) - } - guard win32_u16_at(bytes, 10) is Some(virtual_key_code) else { - return Unsupported(Win32RecordKey) - } - guard win32_u16_at(bytes, 12) is Some(virtual_scan_code) else { - return Unsupported(Win32RecordKey) - } - guard win32_u16_at(bytes, 14) is Some(unicode_char) else { - return Unsupported(Win32RecordKey) - } - guard win32_dword_at(bytes, 16) is Some(control_key_state) else { - return Unsupported(Win32RecordKey) - } - Key({ - key_down: if key_down { - 1 - } else { - 0 - }, - repeat_count, - virtual_key_code, - virtual_scan_code, - unicode_char, - control_key_state, - }) -} - -///| -#cfg(platform="windows") -fn win32_parse_mouse_record(bytes : BytesView) -> Win32InputRecord { - guard win32_i16_at(bytes, 4) is Some(x) else { - return Unsupported(Win32RecordMouse) - } - guard win32_i16_at(bytes, 6) is Some(y) else { - return Unsupported(Win32RecordMouse) - } - guard win32_dword_at(bytes, 8) is Some(button_state) else { - return Unsupported(Win32RecordMouse) - } - guard win32_dword_at(bytes, 12) is Some(control_key_state) else { - return Unsupported(Win32RecordMouse) - } - guard win32_dword_at(bytes, 16) is Some(event_flags) else { - return Unsupported(Win32RecordMouse) - } - Mouse({ x, y, button_state, control_key_state, event_flags }) -} - -///| -#cfg(platform="windows") -fn win32_parse_window_buffer_size_record(bytes : BytesView) -> Win32InputRecord { - guard win32_i16_at(bytes, 4) is Some(x) else { - return Unsupported(Win32RecordWindowBufferSize) - } - guard win32_i16_at(bytes, 6) is Some(y) else { - return Unsupported(Win32RecordWindowBufferSize) - } - WindowBufferSize({ x, y }) -} - -///| -#cfg(platform="windows") -fn win32_parse_focus_record(bytes : BytesView) -> Win32InputRecord { - guard win32_bool_at(bytes, 4) is Some(focus_set) else { - return Unsupported(Win32RecordFocus) - } - Focus(focus_set) -} - -///| -#cfg(platform="windows") -fn win32_bool_at(bytes : BytesView, offset : Int) -> Bool? { - win32_dword_at(bytes, offset).map(value => value != 0) -} - -///| -#cfg(platform="windows") -fn win32_i16_at(bytes : BytesView, offset : Int) -> Int? { - guard win32_u16_at(bytes, offset) is Some(value) else { return None } - if value >= 0x8000 { - Some(value - 0x10000) - } else { - Some(value) - } -} - -///| -#cfg(platform="windows") -fn win32_u16_at(bytes : BytesView, offset : Int) -> Int? { - if offset < 0 || offset + 1 >= bytes.length() { - return None - } - Some(bytes[offset].to_int() + bytes[offset + 1].to_int() * 0x100) -} - -///| -#cfg(platform="windows") -fn win32_dword_at(bytes : BytesView, offset : Int) -> Int? { - guard win32_u16_at(bytes, offset) is Some(low) else { return None } - guard win32_i16_at(bytes, offset + 2) is Some(high) else { return None } - Some(low + high * 0x10000) -} - ///| #cfg(platform="windows") extern "c" fn win32_sizeof_input_record() -> Int = "moonbit_tty_get_sizeof_input_record" @@ -1048,15 +243,15 @@ let sizeof_win32_input_record : Int = win32_sizeof_input_record() #cfg(platform="windows") extern "c" fn win32_read_console_input_record_ffi( fd : @async/types.Fd, - record : Win32RawInputRecord, + record : @win32.RawInputRecord, ) -> Int = "moonbit_tty_read_console_input_record" ///| #cfg(platform="windows") fn win32_read_console_input_record( fd : @async/types.Fd, -) -> Win32InputRecord? raise { - let record = Win32RawInputRecord::new() +) -> @win32.InputRecord? raise { + let record = @win32.RawInputRecord::new(sizeof_win32_input_record) let rc = win32_read_console_input_record_ffi(fd, record) if rc < 0 { @os_error.check_errno("ReadConsoleInputW") diff --git a/win32_input_wbtest.mbt b/win32_input_wbtest.mbt index b983b02..029c336 100644 --- a/win32_input_wbtest.mbt +++ b/win32_input_wbtest.mbt @@ -5,15 +5,15 @@ fn win32_test_key_record( virtual_key_code? : Int = 0, control_key_state? : Int = 0, key_down? : Int = 1, -) -> Win32KeyRecord { - { - key_down, - repeat_count: 1, - virtual_key_code, - virtual_scan_code: 0, - unicode_char: char.to_int(), - control_key_state, - } +) -> @win32.KeyRecord { + @win32.KeyRecord::new( + key_down~, + repeat_count=1, + virtual_key_code~, + virtual_scan_code=0, + unicode_char=char.to_int(), + control_key_state~, + ) } ///| @@ -23,15 +23,15 @@ fn win32_test_key_record_charcode( virtual_key_code? : Int = 0, control_key_state? : Int = 0, key_down? : Int = 1, -) -> Win32KeyRecord { - { - key_down, - repeat_count: 1, - virtual_key_code, - virtual_scan_code: 0, - unicode_char: charcode, - control_key_state, - } +) -> @win32.KeyRecord { + @win32.KeyRecord::new( + key_down~, + repeat_count=1, + virtual_key_code~, + virtual_scan_code=0, + unicode_char=charcode, + control_key_state~, + ) } ///| @@ -42,8 +42,14 @@ fn win32_test_mouse_record( button_state? : Int = 0, control_key_state? : Int = 0, event_flags? : Int = 0, -) -> Win32MouseRecord { - { x, y, button_state, control_key_state, event_flags } +) -> @win32.MouseRecord { + @win32.MouseRecord::new( + x~, + y~, + button_state~, + control_key_state~, + event_flags~, + ) } ///| @@ -75,99 +81,6 @@ fn win32_test_try_get_event(tty : Tty) -> Event? { } } -///| -#cfg(platform="windows") -fn win32_test_raw_record(event_type : Int) -> FixedArray[Byte] { - let buf = FixedArray::make(sizeof_win32_input_record, b'\x00') - win32_test_set_u16_le(buf, 0, event_type) - buf -} - -///| -#cfg(platform="windows") -fn win32_test_raw_input_record(buf : FixedArray[Byte]) -> Win32RawInputRecord { - Win32RawInputRecord(buf.unsafe_reinterpret_as_bytes()) -} - -///| -#cfg(platform="windows") -fn win32_test_set_u16_le( - buf : FixedArray[Byte], - offset : Int, - value : Int, -) -> Unit { - buf[offset] = (value & 0xff).to_byte() - buf[offset + 1] = ((value / 0x100) & 0xff).to_byte() -} - -///| -#cfg(platform="windows") -fn win32_test_set_u32_le( - buf : FixedArray[Byte], - offset : Int, - value : Int, -) -> Unit { - buf[offset] = (value & 0xff).to_byte() - buf[offset + 1] = ((value / 0x100) & 0xff).to_byte() - buf[offset + 2] = ((value / 0x10000) & 0xff).to_byte() - buf[offset + 3] = ((value / 0x1000000) & 0xff).to_byte() -} - -///| -#cfg(platform="windows") -test "win32 raw input record parses key records" { - let buf = win32_test_raw_record(Win32RecordKey) - win32_test_set_u32_le(buf, 4, 1) - win32_test_set_u16_le(buf, 8, 2) - win32_test_set_u16_le(buf, 10, 0x41) - win32_test_set_u16_le(buf, 12, 0x1e) - win32_test_set_u16_le(buf, 14, 'A'.to_int()) - win32_test_set_u32_le(buf, 16, Win32ShiftPressed) - match win32_test_raw_input_record(buf).parse() { - Key(record) => { - @debug.assert_eq(record.key_down, 1) - @debug.assert_eq(record.repeat_count, 2) - @debug.assert_eq(record.virtual_key_code, 0x41) - @debug.assert_eq(record.virtual_scan_code, 0x1e) - @debug.assert_eq(record.unicode_char, 'A'.to_int()) - @debug.assert_eq(record.control_key_state, Win32ShiftPressed) - } - _ => fail("expected key record") - } -} - -///| -#cfg(platform="windows") -test "win32 raw input record parses mouse records" { - let buf = win32_test_raw_record(Win32RecordMouse) - win32_test_set_u16_le(buf, 4, 30) - win32_test_set_u16_le(buf, 6, 2) - win32_test_set_u32_le(buf, 8, Win32MouseLeftButton) - win32_test_set_u32_le(buf, 12, Win32ShiftPressed) - win32_test_set_u32_le(buf, 16, Win32MouseMoved) - match win32_test_raw_input_record(buf).parse() { - Mouse(record) => { - @debug.assert_eq(record.x, 30) - @debug.assert_eq(record.y, 2) - @debug.assert_eq(record.button_state, Win32MouseLeftButton) - @debug.assert_eq(record.control_key_state, Win32ShiftPressed) - @debug.assert_eq(record.event_flags, Win32MouseMoved) - } - _ => fail("expected mouse record") - } -} - -///| -#cfg(platform="windows") -test "win32 raw input record parses focus records" { - let buf = win32_test_raw_record(Win32RecordFocus) - win32_test_set_u32_le(buf, 4, 1) - match win32_test_raw_input_record(buf).parse() { - Focus(true) => () - _ => fail("expected focus record") - } -} - ///| #cfg(platform="windows") async test "win32 console source ignores key-up records" { @@ -234,7 +147,7 @@ async test "win32 console source maps native mouse records" { let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() source.read_mouse_record( - win32_test_mouse_record(x=30, y=2, event_flags=Win32MouseMoved), + win32_test_mouse_record(x=30, y=2, event_flags=@win32.MouseMoved), ) match win32_test_try_get_event(source) { Some(Input(Mouse(event))) => @@ -242,7 +155,7 @@ async test "win32 console source maps native mouse records" { _ => fail("expected mouse move event") } source.read_mouse_record( - win32_test_mouse_record(x=30, y=2, button_state=Win32MouseLeftButton), + win32_test_mouse_record(x=30, y=2, button_state=@win32.MouseLeftButton), ) match win32_test_try_get_event(source) { Some(Input(Mouse(event))) => @@ -253,9 +166,9 @@ async test "win32 console source maps native mouse records" { win32_test_mouse_record( x=31, y=2, - button_state=Win32MouseLeftButton, - control_key_state=Win32ShiftPressed, - event_flags=Win32MouseMoved, + button_state=@win32.MouseLeftButton, + control_key_state=@win32.ShiftPressed, + event_flags=@win32.MouseMoved, ), ) match win32_test_try_get_event(source) { @@ -285,7 +198,7 @@ async test "win32 console source maps native mouse records" { x=31, y=2, button_state=120 * 0x10000, - event_flags=Win32MouseWheeled, + event_flags=@win32.MouseWheeled, ), ) match win32_test_try_get_event(source) { @@ -302,7 +215,7 @@ async test "win32 console source preserves printable key modifiers" { let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() source.read_key_record( - win32_test_key_record('a', control_key_state=Win32LeftAltPressed), + win32_test_key_record('a', control_key_state=@win32.LeftAltPressed), ) match win32_test_try_get_event(source) { Some(Input(Key(event))) => @@ -319,7 +232,7 @@ async test "win32 console source preserves printable key modifiers" { source.read_key_record( win32_test_key_record( 'X', - control_key_state=Win32ShiftPressed | Win32RightAltPressed, + control_key_state=@win32.ShiftPressed | @win32.RightAltPressed, ), ) match win32_test_try_get_event(source) { @@ -346,7 +259,7 @@ async test "win32 console source preserves ctrl left bracket" { win32_test_key_record( '\u{1B}', virtual_key_code=0xdb, - control_key_state=Win32LeftCtrlPressed, + control_key_state=@win32.LeftCtrlPressed, ), ) match win32_test_try_get_event(source) { @@ -372,7 +285,7 @@ async test "win32 console source preserves ctrl space" { win32_test_key_record_charcode( 0, virtual_key_code=0x20, - control_key_state=Win32LeftCtrlPressed, + control_key_state=@win32.LeftCtrlPressed, ), ) match win32_test_try_get_event(source) { @@ -399,7 +312,7 @@ async test "win32 console source preserves AltGr text input" { source.read_key_record( win32_test_key_record( '@', - control_key_state=Win32RightAltPressed | Win32LeftCtrlPressed, + control_key_state=@win32.RightAltPressed | @win32.LeftCtrlPressed, ), ) } @@ -424,7 +337,7 @@ async test "win32 console source preserves keypad key metadata" { win32_test_key_record( '1', virtual_key_code=0x61, - control_key_state=Win32NumLockOn, + control_key_state=@win32.NumLockOn, ), ) match win32_test_try_get_event(source) { @@ -462,7 +375,7 @@ async test "win32 console source preserves keypad enter metadata" { win32_test_key_record( '\r', virtual_key_code=0x0d, - control_key_state=Win32EnhancedKey, + control_key_state=@win32.EnhancedKey, ), ) match win32_test_try_get_event(source) { @@ -502,7 +415,7 @@ async test "win32 console source preserves keypad navigation metadata" { win32_test_key_record_charcode( 0, virtual_key_code=0x23, - control_key_state=Win32EnhancedKey, + control_key_state=@win32.EnhancedKey, ), ) match win32_test_try_get_event(source) { From 51c4cab1552a09a7f3d3d5af6c04822fa7390668 Mon Sep 17 00:00:00 2001 From: Haoxiang Fei Date: Tue, 9 Jun 2026 18:03:54 +0800 Subject: [PATCH 04/12] refactor(input): parse Win32 records from bytes --- .../2026-06-09-windows-input-record-enum.md | 16 ++++++++++++++++ internal/win32/pkg.generated.mbti | 5 +---- internal/win32/record.mbt | 14 ++------------ internal/win32/record_wbtest.mbt | 10 +++++----- win32_input.mbt | 6 +++--- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/docs/plans/2026-06-09-windows-input-record-enum.md b/docs/plans/2026-06-09-windows-input-record-enum.md index ea63a92..6dab546 100644 --- a/docs/plans/2026-06-09-windows-input-record-enum.md +++ b/docs/plans/2026-06-09-windows-input-record-enum.md @@ -247,3 +247,19 @@ platform FFI. - Note: `moon info` still rewrites the pre-existing root `Fd::fd` generated mbti spelling from `Int` to `@types.Fd`; this split restored the root generated interface to avoid unrelated public API churn. + +### Follow-up Result + +- Removed the internal `RawInputRecord` wrapper. +- Root now allocates a `Bytes` buffer for `ReadConsoleInputW`, passes that + buffer directly through the FFI borrow boundary, and calls + `@win32.InputRecord::parse(bytes)`. +- `internal/win32` now exposes `InputRecord::parse(Bytes) -> InputRecord`. + +### Follow-up Validation Results + +- Passed: `moon fmt` +- Passed: `moon check --target-dir .moon-check-win32-parse-bytes-build` +- Passed: `moon test internal/win32 --target-dir .moon-test-internal-win32-parse-bytes-build` +- Passed: `moon test . --filter "win32*" --target-dir .moon-test-win32-parse-bytes-build` +- Passed: `moon info --target-dir .moon-info-win32-parse-bytes-build` diff --git a/internal/win32/pkg.generated.mbti b/internal/win32/pkg.generated.mbti index 6815646..5d0f5e9 100644 --- a/internal/win32/pkg.generated.mbti +++ b/internal/win32/pkg.generated.mbti @@ -60,6 +60,7 @@ pub(all) enum InputRecord { Focus(Bool) Unsupported(Int) } +pub fn InputRecord::parse(Bytes) -> Self pub struct KeyRecord { key_down : Int @@ -91,10 +92,6 @@ pub struct MouseRecord { } pub fn MouseRecord::new(x? : Int, y? : Int, button_state? : Int, control_key_state? : Int, event_flags? : Int) -> Self -pub(all) struct RawInputRecord(Bytes) -pub fn RawInputRecord::new(Int) -> Self -pub fn RawInputRecord::parse(Self) -> InputRecord - // Type aliases // Traits diff --git a/internal/win32/record.mbt b/internal/win32/record.mbt index 54c7aaf..fba69fe 100644 --- a/internal/win32/record.mbt +++ b/internal/win32/record.mbt @@ -78,10 +78,6 @@ pub const MouseMiddleButton : Int = 0x0004 #cfg(platform="windows") const MouseButtonMask : Int = 0x0007 -///| -#cfg(platform="windows") -pub(all) struct RawInputRecord(Bytes) - ///| #cfg(platform="windows") pub(all) enum InputRecord { @@ -166,14 +162,8 @@ pub fn MouseButtonState::new() -> MouseButtonState { ///| #cfg(platform="windows") -pub fn RawInputRecord::new(size : Int) -> RawInputRecord { - RawInputRecord(Bytes::make(size, 0)) -} - -///| -#cfg(platform="windows") -pub fn RawInputRecord::parse(self : RawInputRecord) -> InputRecord { - let bytes = self.0[:] +pub fn InputRecord::parse(record : Bytes) -> InputRecord { + let bytes = record[:] guard u16_at(bytes, 0) is Some(event_type) else { return Unsupported(0) } match event_type { RecordKey => parse_key_record(bytes) diff --git a/internal/win32/record_wbtest.mbt b/internal/win32/record_wbtest.mbt index 40f7a47..25d5fd7 100644 --- a/internal/win32/record_wbtest.mbt +++ b/internal/win32/record_wbtest.mbt @@ -8,8 +8,8 @@ fn test_raw_record(event_type : Int) -> FixedArray[Byte] { ///| #cfg(platform="windows") -fn test_raw_input_record(buf : FixedArray[Byte]) -> RawInputRecord { - RawInputRecord(buf.unsafe_reinterpret_as_bytes()) +fn test_input_record(buf : FixedArray[Byte]) -> InputRecord { + InputRecord::parse(buf.unsafe_reinterpret_as_bytes()) } ///| @@ -38,7 +38,7 @@ test "win32 raw input record parses key records" { test_set_u16_le(buf, 12, 0x1e) test_set_u16_le(buf, 14, 'A'.to_int()) test_set_u32_le(buf, 16, ShiftPressed) - match test_raw_input_record(buf).parse() { + match test_input_record(buf) { Key(record) => { @debug.assert_eq(record.key_down, 1) @debug.assert_eq(record.repeat_count, 2) @@ -60,7 +60,7 @@ test "win32 raw input record parses mouse records" { test_set_u32_le(buf, 8, MouseLeftButton) test_set_u32_le(buf, 12, ShiftPressed) test_set_u32_le(buf, 16, MouseMoved) - match test_raw_input_record(buf).parse() { + match test_input_record(buf) { Mouse(record) => { @debug.assert_eq(record.x, 30) @debug.assert_eq(record.y, 2) @@ -77,7 +77,7 @@ test "win32 raw input record parses mouse records" { test "win32 raw input record parses focus records" { let buf = test_raw_record(RecordFocus) test_set_u32_le(buf, 4, 1) - match test_raw_input_record(buf).parse() { + match test_input_record(buf) { Focus(true) => () _ => fail("expected focus record") } diff --git a/win32_input.mbt b/win32_input.mbt index da04535..385ae08 100644 --- a/win32_input.mbt +++ b/win32_input.mbt @@ -243,7 +243,7 @@ let sizeof_win32_input_record : Int = win32_sizeof_input_record() #cfg(platform="windows") extern "c" fn win32_read_console_input_record_ffi( fd : @async/types.Fd, - record : @win32.RawInputRecord, + record : Bytes, ) -> Int = "moonbit_tty_read_console_input_record" ///| @@ -251,7 +251,7 @@ extern "c" fn win32_read_console_input_record_ffi( fn win32_read_console_input_record( fd : @async/types.Fd, ) -> @win32.InputRecord? raise { - let record = @win32.RawInputRecord::new(sizeof_win32_input_record) + let record = Bytes::make(sizeof_win32_input_record, 0) let rc = win32_read_console_input_record_ffi(fd, record) if rc < 0 { @os_error.check_errno("ReadConsoleInputW") @@ -259,6 +259,6 @@ fn win32_read_console_input_record( if rc == 0 { None } else { - Some(record.parse()) + Some(@win32.InputRecord::parse(record)) } } From d8cec411444eb498abfd4a3e4efd4bcea561fec8 Mon Sep 17 00:00:00 2001 From: Haoxiang Fei Date: Tue, 9 Jun 2026 18:12:06 +0800 Subject: [PATCH 05/12] test(input): split Win32 record blackbox coverage --- internal/win32/moon.pkg | 5 ++ internal/win32/record_test.mbt | 121 +++++++++++++++++++++++++++++++ internal/win32/record_wbtest.mbt | 11 --- 3 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 internal/win32/record_test.mbt diff --git a/internal/win32/moon.pkg b/internal/win32/moon.pkg index fb29af8..961ca3e 100644 --- a/internal/win32/moon.pkg +++ b/internal/win32/moon.pkg @@ -2,6 +2,11 @@ import { "moonbit-community/tty/input" @public_input, } +import { + "moonbit-community/tty/input" @public_input, + "moonbitlang/core/debug", +} for "test" + import { "moonbitlang/core/debug", } for "wbtest" diff --git a/internal/win32/record_test.mbt b/internal/win32/record_test.mbt new file mode 100644 index 0000000..da156b6 --- /dev/null +++ b/internal/win32/record_test.mbt @@ -0,0 +1,121 @@ +///| +#cfg(platform="windows") +fn test_raw_record(event_type : Int) -> FixedArray[Byte] { + let buf = FixedArray::make(20, b'\x00') + test_set_u16_le(buf, 0, event_type) + buf +} + +///| +#cfg(platform="windows") +fn test_input_record(buf : FixedArray[Byte]) -> InputRecord { + InputRecord::parse(buf.unsafe_reinterpret_as_bytes()) +} + +///| +#cfg(platform="windows") +fn test_set_u16_le(buf : FixedArray[Byte], offset : Int, value : Int) -> Unit { + buf[offset] = (value & 0xff).to_byte() + buf[offset + 1] = ((value / 0x100) & 0xff).to_byte() +} + +///| +#cfg(platform="windows") +fn test_set_u32_le(buf : FixedArray[Byte], offset : Int, value : Int) -> Unit { + buf[offset] = (value & 0xff).to_byte() + buf[offset + 1] = ((value / 0x100) & 0xff).to_byte() + buf[offset + 2] = ((value / 0x10000) & 0xff).to_byte() + buf[offset + 3] = ((value / 0x1000000) & 0xff).to_byte() +} + +///| +#cfg(platform="windows") +test "win32 raw input record parses focus records" { + let buf = test_raw_record(RecordFocus) + test_set_u32_le(buf, 4, 1) + match test_input_record(buf) { + Focus(true) => () + _ => fail("expected focus record") + } +} + +///| +#cfg(platform="windows") +test "win32 raw input record parses window buffer size records" { + let buf = test_raw_record(RecordWindowBufferSize) + test_set_u16_le(buf, 4, 80) + test_set_u16_le(buf, 6, 24) + match test_input_record(buf) { + WindowBufferSize(_) => () + _ => fail("expected window buffer size record") + } +} + +///| +#cfg(platform="windows") +test "win32 raw input record parses unsupported records" { + match InputRecord::parse(b"\x01") { + Unsupported(0) => () + _ => fail("expected unsupported short record") + } + let buf = test_raw_record(0x1234) + match test_input_record(buf) { + Unsupported(0x1234) => () + _ => fail("expected unsupported record type") + } +} + +///| +#cfg(platform="windows") +test "win32 raw input record parses key records to input events" { + let buf = test_raw_record(RecordKey) + test_set_u32_le(buf, 4, 1) + test_set_u16_le(buf, 8, 1) + test_set_u16_le(buf, 10, 0x41) + test_set_u16_le(buf, 12, 0x1e) + test_set_u16_le(buf, 14, 'A'.to_int()) + test_set_u32_le(buf, 16, ShiftPressed) + match test_input_record(buf) { + Key(record) => + @debug.assert_eq( + record.input(Ref(0)), + Some( + Key( + @public_input.KeyEvent::new( + Char('A'), + modifiers=@public_input.KeyModifiers::new(shift=true), + text="A", + ), + ), + ), + ) + _ => fail("expected key record") + } +} + +///| +#cfg(platform="windows") +test "win32 raw input record parses mouse records to input events" { + let buf = test_raw_record(RecordMouse) + test_set_u16_le(buf, 4, 30) + test_set_u16_le(buf, 6, 2) + test_set_u32_le(buf, 12, ShiftPressed) + test_set_u32_le(buf, 16, MouseMoved) + match test_input_record(buf) { + Mouse(record) => + @debug.assert_eq( + MouseButtonState::new().input_event(record), + Some( + Mouse( + @public_input.MouseEvent::new( + Move, + 3, + 31, + modifiers=@public_input.KeyModifiers::new(shift=true), + ), + ), + ), + ) + _ => fail("expected mouse record") + } +} diff --git a/internal/win32/record_wbtest.mbt b/internal/win32/record_wbtest.mbt index 25d5fd7..b612925 100644 --- a/internal/win32/record_wbtest.mbt +++ b/internal/win32/record_wbtest.mbt @@ -71,14 +71,3 @@ test "win32 raw input record parses mouse records" { _ => fail("expected mouse record") } } - -///| -#cfg(platform="windows") -test "win32 raw input record parses focus records" { - let buf = test_raw_record(RecordFocus) - test_set_u32_le(buf, 4, 1) - match test_input_record(buf) { - Focus(true) => () - _ => fail("expected focus record") - } -} From 692426823bbd30e1bfd1941b32b33f7c35e29395 Mon Sep 17 00:00:00 2001 From: Haoxiang Fei Date: Tue, 9 Jun 2026 18:27:23 +0800 Subject: [PATCH 06/12] test(input): reproduce delayed mouse sequence tails --- internal/input/decoder_test.mbt | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/internal/input/decoder_test.mbt b/internal/input/decoder_test.mbt index dd12abb..7407e6f 100644 --- a/internal/input/decoder_test.mbt +++ b/internal/input/decoder_test.mbt @@ -436,6 +436,54 @@ async test "flush escape key" { }) } +///| +async test "decode delayed escape exposes csi mouse tail as text" { + with_delayed_input(b"\x1b", b"[3;71M", 50, events => { + @debug.assert_eq( + events.read_event(esc_timeout_ms=10), + input_key(@public_input.KeyEvent::new(Escape)), + ) + for char in "[3;71M" { + @debug.assert_eq( + events.read_event(esc_timeout_ms=10), + input_key(@public_input.KeyEvent::new(Char(char), text=[char])), + ) + } + }) +} + +///| +async test "decode delayed escape exposes sgr mouse tail as text" { + with_delayed_input(b"\x1b", b"[<3;71;12M", 50, events => { + @debug.assert_eq( + events.read_event(esc_timeout_ms=10), + input_key(@public_input.KeyEvent::new(Escape)), + ) + for char in "[<3;71;12M" { + @debug.assert_eq( + events.read_event(esc_timeout_ms=10), + input_key(@public_input.KeyEvent::new(Char(char), text=[char])), + ) + } + }) +} + +///| +async test "decode delayed sgr mouse prefix consumes csi prefix as unknown" { + with_delayed_input(b"\x1b[<", b"3;71;12M", 50, events => { + @debug.assert_eq( + events.read_event(esc_timeout_ms=10), + input_unknown(b"\x1b[<"), + ) + for char in "3;71;12M" { + @debug.assert_eq( + events.read_event(esc_timeout_ms=10), + input_key(@public_input.KeyEvent::new(Char(char), text=[char])), + ) + } + }) +} + ///| async test "decode escape followed by non printable byte" { with_input(b"\x1b\x80", events => { From 1c386912d2d6e211fd2e512a8d5189fe7ead17d0 Mon Sep 17 00:00:00 2001 From: Haoxiang Fei Date: Tue, 9 Jun 2026 19:57:57 +0800 Subject: [PATCH 07/12] fix(input): buffer Windows console decoder bytes --- .../2026-06-09-windows-input-record-enum.md | 138 ++++++++++++++++++ examples/raw/main.mbt | 34 +++-- examples/raw/main_wbtest.mbt | 10 +- internal/io/byte_queue.mbt | 74 ++++++++++ internal/io/byte_queue_test.mbt | 45 ++++++ internal/io/moon.pkg | 9 ++ internal/io/pkg.generated.mbti | 21 +++ moon.pkg | 1 + win32_input.mbt | 72 +++++---- win32_input_wbtest.mbt | 21 +++ 10 files changed, 384 insertions(+), 41 deletions(-) create mode 100644 internal/io/byte_queue.mbt create mode 100644 internal/io/byte_queue_test.mbt create mode 100644 internal/io/moon.pkg create mode 100644 internal/io/pkg.generated.mbti diff --git a/docs/plans/2026-06-09-windows-input-record-enum.md b/docs/plans/2026-06-09-windows-input-record-enum.md index 6dab546..9ca0465 100644 --- a/docs/plans/2026-06-09-windows-input-record-enum.md +++ b/docs/plans/2026-06-09-windows-input-record-enum.md @@ -263,3 +263,141 @@ platform FFI. - Passed: `moon test internal/win32 --target-dir .moon-test-internal-win32-parse-bytes-build` - Passed: `moon test . --filter "win32*" --target-dir .moon-test-win32-parse-bytes-build` - Passed: `moon info --target-dir .moon-info-win32-parse-bytes-build` + +## Follow-up: Drain Windows Console Records + +### Goal + +Reduce split VT input tails on Windows console input by draining the currently +available `ReadConsoleInputW` records before a short-lived record producer can +be cancelled by the direct-event vs decoded-byte race. + +### Accepted Design + +- Keep the change Windows-only and private to the root package. +- Keep the existing byte pipe and shared `EventReader`; do not introduce a new + byte-reader abstraction or persistent producer in this follow-up. +- In the Windows record producer, treat one polling cycle as: + - read records with the existing non-blocking `PeekConsoleInputW` / + `ReadConsoleInputW` FFI wrapper until it returns `None`; + - dispatch each record through the existing key, mouse, focus, resize, and + unsupported handlers in record order; + - sleep only when no record was available in that cycle. +- Queue direct events with synchronous `Queue::try_put` on the existing + unbounded direct-event queue, so putting a native mouse/focus/resize/key event + is not itself an async cancellation point. +- This reduces cancellation leaving an already-available VT sequence tail in + the Win32 console queue. It still does not guarantee none-or-all delivery if + the terminal or ConPTY delivers later records after the escape timeout. + +### Public API / Interface Diff + +- No public MoonBit API change intended. +- `pkg.generated.mbti` should remain unchanged after `moon info`. + +### Validation Plan + +- `moon fmt` +- `moon test . --filter "win32*" --target-dir .moon-test-win32-drain-build` +- `moon test internal/input --filter "decode delayed *mouse*" --target-dir .moon-test-input-delayed-mouse-drain-build` +- `moon check --target-dir .moon-check-win32-drain-build` +- `moon info --target-dir .moon-info-win32-drain-build` +- Review `.mbti` diff. +- `git diff --check` + +### Result + +- Added a private `Tty::drain_records` helper for Windows console input. +- The record producer now drains all currently available `INPUT_RECORD` values + before sleeping on an empty poll. +- Direct native events are queued with synchronous `Queue::try_put` on the + existing unbounded event queue, so native event delivery no longer introduces + an async cancellation point. +- Kept byte delivery on the existing pipe and did not change the input decoder + public or private API. + +### Validation Results + +- Passed: `moon fmt` +- Passed: `moon test . --filter "win32*" --target-dir .moon-test-win32-drain-build` +- Passed: `moon test internal/input --filter "decode delayed *mouse*" --target-dir .moon-test-input-delayed-mouse-drain-build` +- Passed: `moon check --target-dir .moon-check-win32-drain-build` +- Passed: `moon info --target-dir .moon-info-win32-drain-build` +- Passed: `git diff --check` +- Note: `moon info` still rewrites the pre-existing root `Fd::fd` generated + mbti spelling from `Int` to `@types.Fd`; this follow-up restored the root + generated interface to avoid unrelated public API churn. + +## Follow-up: Use ByteQueue Source For Windows Console Input + +### Goal + +Stop Windows console input from exposing VT sequence tails such as `[3;20M` +when `ReadConsoleInputW` key records are translated to bytes for the shared +input decoder. + +### Accepted Design + +- Add a separate internal package at `internal/io`. +- Expose an opaque internal `ByteQueue` that implements `@async/io.Reader` and + also provides synchronous byte enqueue helpers for producers. +- Keep the queue unbounded for the Windows console byte path so producer-side + `try_put` calls are synchronous and do not suspend. +- The reader blocks on `Queue::get` for the first byte, then drains any + immediately queued bytes with `try_get` into the caller's buffer. +- Replace the Windows console byte pipe with a single stored + `byte_queue : @internal_io.ByteQueue`. +- Use the same `ByteQueue` as the reader passed to the single `Tty.reader` and + as the producer-side byte sink for Windows key records. +- Keep direct native events on the existing event queue. +- Keep the existing drain loop over currently available `INPUT_RECORD` values. +- Close the byte queue with `@async/io.ReaderClosed` so the reader reports EOF + to `EventReader` callers. + +### Public API / Interface Diff + +- No root public MoonBit API change intended. +- Root `pkg.generated.mbti` should remain unchanged after `moon info`. +- `internal/io` exposes an internal-only reader type for use by root Windows + input code and tests. + +### Validation Plan + +- `moon fmt` +- `moon test internal/io --target-dir .moon-test-internal-io-bytequeue-build` +- `moon test . --filter "win32*" --target-dir .moon-test-win32-bytequeue-shape-build` +- `moon test internal/input --filter "decode delayed *mouse*" --target-dir .moon-test-input-delayed-mouse-bytequeue-shape-build` +- `moon check --target-dir .moon-check-bytequeue-shape-build` +- `moon info --target-dir .moon-info-bytequeue-shape-build` +- Review `.mbti` diff. +- `git diff --check` + +### Result + +- Added `internal/io` with an opaque internal `ByteQueue`. +- `ByteQueue` implements `@async/io.Reader`, blocks for the first byte, and + drains already queued bytes with `try_get`. +- `ByteQueue` also exposes producer-side `put_byte`, `put_data`, and `close` + helpers. +- Windows console input now stores one `ByteQueue` instead of an unbuffered pipe + or separate queue/reader fields. +- Key records that should go through the shared input decoder now enqueue UTF-8 + bytes synchronously through `ByteQueue`. +- Record dispatch for key, mouse, focus, resize, and unsupported records is now + synchronous after the `ReadConsoleInputW` poll succeeds, removing the pipe + write cancellation point that could leave VT mouse tails in the console queue. +- Added Windows white-box coverage proving native direct events can be queued + before a complete SGR mouse byte sequence without splitting the sequence. + +### Validation Results + +- Passed: `moon fmt` +- Passed: `moon test internal/io --target-dir .moon-test-internal-io-bytequeue-build` +- Passed: `moon test . --filter "win32*" --target-dir .moon-test-win32-bytequeue-shape-build` +- Passed: `moon test internal/input --filter "decode delayed *mouse*" --target-dir .moon-test-input-delayed-mouse-bytequeue-shape-build` +- Passed: `moon check --target-dir .moon-check-bytequeue-shape-build` +- Passed: `moon info --target-dir .moon-info-bytequeue-shape-build` +- Passed: `git diff --check` +- Note: `moon info` still rewrites the pre-existing root `Fd::fd` generated + mbti spelling from `Int` to `@types.Fd`; this follow-up restored the root + generated interface to avoid unrelated public API churn. diff --git a/examples/raw/main.mbt b/examples/raw/main.mbt index 8d0062a..eb29823 100644 --- a/examples/raw/main.mbt +++ b/examples/raw/main.mbt @@ -5,15 +5,29 @@ let number_to_xdigit : ReadOnlyArray[Byte] = [ ] ///| -fn byte_to_hex_line(byte : Byte) -> Bytes { - let buf : FixedArray[Byte] = FixedArray::make(6, 0) - buf[0] = b'0' - buf[1] = b'x' - buf[2] = number_to_xdigit[byte.to_int() / 16] - buf[3] = number_to_xdigit[byte.to_int() % 16] - buf[4] = b'\r' - buf[5] = b'\n' - buf.unsafe_reinterpret_as_bytes() +fn byte_to_printable_char(byte : Byte) -> Char? { + if byte >= b' ' && byte <= b'~' { + Some(byte.to_char()) + } else { + None + } +} + +///| +fn byte_to_raw_line(byte : Byte) -> String { + let line = StringBuilder(size_hint=16) + line.write_string("0x") + line.write_char(number_to_xdigit[byte.to_int() / 16].to_char()) + line.write_char(number_to_xdigit[byte.to_int() % 16].to_char()) + match byte_to_printable_char(byte) { + Some(char) => { + line.write_string(" ") + line.write_string(char.escape(quote=true)) + } + None => () + } + line.write_string("\r\n") + line.to_string() } ///| @@ -34,7 +48,7 @@ async fn run_raw_loop(tty : @tty.Tty) -> Unit { break } tty.write("read ") - tty.write(byte_to_hex_line(byte)) + tty.write(byte_to_raw_line(byte)) } } diff --git a/examples/raw/main_wbtest.mbt b/examples/raw/main_wbtest.mbt index 074422b..b16c94f 100644 --- a/examples/raw/main_wbtest.mbt +++ b/examples/raw/main_wbtest.mbt @@ -1,6 +1,8 @@ ///| -test "byte to hex line" { - assert_true(byte_to_hex_line(b'\x00') == b"0x00\r\n") - assert_true(byte_to_hex_line(b'\xab') == b"0xab\r\n") - assert_true(byte_to_hex_line(b'\xff') == b"0xff\r\n") +test "byte to raw line prints printable ascii char" { + assert_true(byte_to_raw_line(b'\x00') == "0x00\r\n") + assert_true(byte_to_raw_line(b'a') == "0x61 'a'\r\n") + assert_true(byte_to_raw_line(b' ') == "0x20 ' '\r\n") + assert_true(byte_to_raw_line(b'\\') == "0x5c '\\\\'\r\n") + assert_true(byte_to_raw_line(b'\xab') == "0xab\r\n") } diff --git a/internal/io/byte_queue.mbt b/internal/io/byte_queue.mbt new file mode 100644 index 0000000..c13dfb6 --- /dev/null +++ b/internal/io/byte_queue.mbt @@ -0,0 +1,74 @@ +///| +#warnings("-14") +pub struct ByteQueue { + priv queue : @async.Queue[Byte] + priv read_buf : @async/io.ReaderBuffer +} + +///| +#warnings("-14") +pub fn ByteQueue::new() -> ByteQueue { + { queue: Queue(kind=Unbounded), read_buf: @async/io.ReaderBuffer::new() } +} + +///| +pub fn ByteQueue::close(self : ByteQueue) -> Unit { + self.queue.close(error=@async/io.ReaderClosed, clear=true) +} + +///| +pub fn ByteQueue::put_byte(self : ByteQueue, byte : Byte) -> Unit { + try self.queue.try_put(byte) catch { + _ => () + } noraise { + _ => () + } +} + +///| +pub fn ByteQueue::put_data(self : ByteQueue, data : &@async/io.Data) -> Unit { + for byte in data.binary() { + self.put_byte(byte) + } +} + +///| +pub impl @async/io.Reader for ByteQueue with fn _get_internal_buffer(self) { + self.read_buf +} + +///| +pub impl @async/io.Reader for ByteQueue with fn _direct_read( + self, + dst, + offset~, + max_len~, +) { + if max_len <= 0 { + return 0 + } + let first = try self.queue.get() catch { + @async/io.ReaderClosed => return 0 + error => raise error + } noraise { + byte => byte + } + dst[offset] = first + let mut len = 1 + while len < max_len { + let next = try self.queue.try_get() catch { + @async/io.ReaderClosed => None + error => raise error + } noraise { + byte => byte + } + match next { + Some(byte) => { + dst[offset + len] = byte + len += 1 + } + None => break + } + } + len +} diff --git a/internal/io/byte_queue_test.mbt b/internal/io/byte_queue_test.mbt new file mode 100644 index 0000000..fc4145f --- /dev/null +++ b/internal/io/byte_queue_test.mbt @@ -0,0 +1,45 @@ +///| +async test "byte queue drains queued bytes" { + let queue = ByteQueue::new() + queue.put_byte(b'a') + queue.put_byte(b'b') + queue.put_byte(b'c') + let dst = FixedArray::make(4, b'\x00') + let n = queue.read(dst, max_len=4) + @debug.assert_eq(n, 3) + @debug.assert_eq(dst.unsafe_reinterpret_as_bytes()[:n].to_owned(), b"abc") +} + +///| +async test "byte queue waits for first byte" { + let queue = ByteQueue::new() + let dst = FixedArray::make(2, b'\x00') + @async.with_task_group() <| group => { + group.spawn_bg() <| () => { + @async.sleep(10) + queue.put_byte(b'x') + } + let n = queue.read(dst, max_len=2) + @debug.assert_eq(n, 1) + @debug.assert_eq(dst.unsafe_reinterpret_as_bytes()[:n].to_owned(), b"x") + } +} + +///| +async test "byte queue reports eof when closed" { + let queue = ByteQueue::new() + queue.close() + let dst = FixedArray::make(1, b'\x00') + @debug.assert_eq(queue.read(dst, max_len=1), 0) +} + +///| +async test "byte queue writes data" { + let queue = ByteQueue::new() + queue.put_data("ab") + queue.put_data(b"cd") + let dst = FixedArray::make(8, b'\x00') + let n = queue.read(dst, max_len=8) + @debug.assert_eq(n, 4) + @debug.assert_eq(dst.unsafe_reinterpret_as_bytes()[:n].to_owned(), b"abcd") +} diff --git a/internal/io/moon.pkg b/internal/io/moon.pkg new file mode 100644 index 0000000..b83a1a9 --- /dev/null +++ b/internal/io/moon.pkg @@ -0,0 +1,9 @@ +import { + "moonbitlang/async", + "moonbitlang/async/io" @async/io, +} + +import { + "moonbitlang/async", + "moonbitlang/core/debug", +} for "test" diff --git a/internal/io/pkg.generated.mbti b/internal/io/pkg.generated.mbti new file mode 100644 index 0000000..f88e8c3 --- /dev/null +++ b/internal/io/pkg.generated.mbti @@ -0,0 +1,21 @@ +// Generated using `moon info`, DON'T EDIT IT +package "moonbit-community/tty/internal/io" + +// Values + +// Errors + +// Types and methods +pub struct ByteQueue { + // private fields +} +pub fn ByteQueue::close(Self) -> Unit +pub fn ByteQueue::new() -> Self +pub fn ByteQueue::put_byte(Self, Byte) -> Unit +pub fn ByteQueue::put_data(Self, &@moonbitlang/async/io.Data) -> Unit +pub impl @moonbitlang/async/io.Reader for ByteQueue + +// Type aliases + +// Traits + diff --git a/moon.pkg b/moon.pkg index da52b3f..acdc000 100644 --- a/moon.pkg +++ b/moon.pkg @@ -9,6 +9,7 @@ import { "moonbitlang/async/io" @async/io, "moonbit-community/tty/color", "moonbit-community/tty/input" @public_input, + "moonbit-community/tty/internal/io" @internal_io, "moonbit-community/tty/internal/input", "moonbit-community/tty/internal/win32", "moonbit-community/tty/internal/vt", diff --git a/win32_input.mbt b/win32_input.mbt index 385ae08..cf3dadb 100644 --- a/win32_input.mbt +++ b/win32_input.mbt @@ -4,8 +4,7 @@ priv enum WindowsInput { ByteStream Console( input_fd~ : @async/types.Fd, - byte_read~ : @async/io.PipeRead, - byte_write~ : @async/io.PipeWrite, + byte_queue~ : @internal_io.ByteQueue, events~ : @async.Queue[Event], pending_surrogate~ : Ref[Int], mouse_button_state~ : @win32.MouseButtonState @@ -25,11 +24,10 @@ fn WindowsInput::new(input_fd : @async/types.Fd) -> WindowsInput { ///| #cfg(platform="windows") fn WindowsInput::console(input_fd : @async/types.Fd) -> WindowsInput { - let (byte_read, byte_write) = @async/io.pipe() + let byte_queue = @internal_io.ByteQueue::new() Console( input_fd~, - byte_read~, - byte_write~, + byte_queue~, events=Queue(kind=Unbounded), pending_surrogate=Ref(0), mouse_button_state=@win32.MouseButtonState::new(), @@ -44,8 +42,8 @@ fn WindowsInput::event_reader( ) -> @input.EventReader { match self { ByteStream => @input.EventReader::new(fallback) - Console(byte_read~, ..) => - @input.EventReader::new(byte_read as &@async/io.Reader) + Console(byte_queue~, ..) => + @input.EventReader::new(byte_queue as &@async/io.Reader) } } @@ -54,9 +52,8 @@ fn WindowsInput::event_reader( fn WindowsInput::close(self : WindowsInput) -> Unit { match self { ByteStream => () - Console(byte_read~, byte_write~, events~, ..) => { - byte_read.close() - byte_write.close() + Console(byte_queue~, events~, ..) => { + byte_queue.close() events.close(clear=true) } } @@ -134,9 +131,8 @@ async fn Tty::read_records(self : Self) -> Unit { match self.input_backend { Console(input_fd~, ..) => for ;; { - match win32_read_console_input_record(input_fd) { - None => @async.sleep(10) - Some(record) => self.read_record(record) + if !self.drain_records(input_fd) { + @async.sleep(10) } } ByteStream => () @@ -145,7 +141,32 @@ async fn Tty::read_records(self : Self) -> Unit { ///| #cfg(platform="windows") -async fn Tty::read_record(self : Self, record : @win32.InputRecord) -> Unit { +fn Tty::drain_records(self : Self, input_fd : @async/types.Fd) -> Bool raise { + let mut read_any = false + for ;; { + match win32_read_console_input_record(input_fd) { + None => return read_any + Some(record) => { + read_any = true + self.read_record(record) + } + } + } +} + +///| +#cfg(platform="windows") +fn queue_console_event(events : @async.Queue[Event], event : Event) -> Unit { + try events.try_put(event) catch { + _ => () + } noraise { + _ => () + } +} + +///| +#cfg(platform="windows") +fn Tty::read_record(self : Self, record : @win32.InputRecord) -> Unit { match self.input_backend { Console(events~, pending_surrogate~, ..) => match record { @@ -156,15 +177,15 @@ async fn Tty::read_record(self : Self, record : @win32.InputRecord) -> Unit { try self.window_size() catch { _ => () } noraise { - size => events.put(Resize(size)) + size => queue_console_event(events, Resize(size)) } } Focus(focus_set) => { pending_surrogate.val = 0 if focus_set { - events.put(Input(FocusIn)) + queue_console_event(events, Input(FocusIn)) } else { - events.put(Input(FocusOut)) + queue_console_event(events, Input(FocusOut)) } } Unsupported(_) => pending_surrogate.val = 0 @@ -175,9 +196,9 @@ async fn Tty::read_record(self : Self, record : @win32.InputRecord) -> Unit { ///| #cfg(platform="windows") -async fn Tty::read_key_record(self : Self, record : @win32.KeyRecord) -> Unit { +fn Tty::read_key_record(self : Self, record : @win32.KeyRecord) -> Unit { match self.input_backend { - Console(byte_write~, pending_surrogate~, ..) => { + Console(byte_queue~, pending_surrogate~, ..) => { if !record.is_down() { return } @@ -187,7 +208,7 @@ async fn Tty::read_key_record(self : Self, record : @win32.KeyRecord) -> Unit { match record.text(pending_surrogate) { Some(text) => for _ in 0.. self.queue_key_input(record) } @@ -198,13 +219,13 @@ async fn Tty::read_key_record(self : Self, record : @win32.KeyRecord) -> Unit { ///| #cfg(platform="windows") -async fn Tty::queue_key_input(self : Self, record : @win32.KeyRecord) -> Unit { +fn Tty::queue_key_input(self : Self, record : @win32.KeyRecord) -> Unit { match self.input_backend { Console(events~, pending_surrogate~, ..) => match record.input(pending_surrogate) { Some(input) => for _ in 0.. () } @@ -214,15 +235,12 @@ async fn Tty::queue_key_input(self : Self, record : @win32.KeyRecord) -> Unit { ///| #cfg(platform="windows") -async fn Tty::read_mouse_record( - self : Self, - record : @win32.MouseRecord, -) -> Unit { +fn Tty::read_mouse_record(self : Self, record : @win32.MouseRecord) -> Unit { match self.input_backend { Console(events~, pending_surrogate~, mouse_button_state~, ..) => { pending_surrogate.val = 0 match mouse_button_state.input_event(record) { - Some(input) => events.put(Input(input)) + Some(input) => queue_console_event(events, Input(input)) None => () } } diff --git a/win32_input_wbtest.mbt b/win32_input_wbtest.mbt index 029c336..4183a59 100644 --- a/win32_input_wbtest.mbt +++ b/win32_input_wbtest.mbt @@ -140,6 +140,27 @@ async test "win32 console source preserves native events during internal reads" } } +///| +#cfg(platform="windows") +async test "win32 console source keeps queued bytes after native event" { + let (dummy_input, dummy_output) = @async/pipe.pipe() + let source = win32_test_console_tty(dummy_input, dummy_output) + defer source.close() + source.read_record(Focus(true)) + for char in "\u{1B}[<0;20;4M" { + source.read_key_record(win32_test_key_record(char)) + } + match win32_test_try_get_event(source) { + Some(Input(FocusIn)) => () + _ => fail("expected native focus event to remain queued") + } + match source.read_decoded_input_event(50) { + Input(Mouse(event)) => + @debug.assert_eq(event, @public_input.MouseEvent::new(Press(Left), 4, 20)) + _ => fail("expected complete queued SGR mouse event") + } +} + ///| #cfg(platform="windows") async test "win32 console source maps native mouse records" { From cb0fcf5eeaa9a603cdfbac1fa0ee0a5a8c60f5b4 Mon Sep 17 00:00:00 2001 From: Haoxiang Fei Date: Wed, 10 Jun 2026 10:03:49 +0800 Subject: [PATCH 08/12] fix(ci): anchor non-Windows Win32 imports --- internal/win32/non_windows.mbt | 3 +++ internal/win32/non_windows_test.mbt | 5 +++++ 2 files changed, 8 insertions(+) create mode 100644 internal/win32/non_windows.mbt create mode 100644 internal/win32/non_windows_test.mbt diff --git a/internal/win32/non_windows.mbt b/internal/win32/non_windows.mbt new file mode 100644 index 0000000..3739dcc --- /dev/null +++ b/internal/win32/non_windows.mbt @@ -0,0 +1,3 @@ +///| +#cfg(not(platform="windows")) +let _public_input_import_anchor : @public_input.InputEvent? = None diff --git a/internal/win32/non_windows_test.mbt b/internal/win32/non_windows_test.mbt new file mode 100644 index 0000000..98108fe --- /dev/null +++ b/internal/win32/non_windows_test.mbt @@ -0,0 +1,5 @@ +///| +#cfg(not(platform="windows")) +test "non-Windows public input import anchor" { + let _ : @public_input.InputEvent? = None +} From 51802c6d3d0bd1b5500884b29fb1975668124c98 Mon Sep 17 00:00:00 2001 From: Haoxiang Fei Date: Wed, 10 Jun 2026 11:00:23 +0800 Subject: [PATCH 09/12] fix(ci): compile Win32 parser cross-platform --- .../2026-06-09-windows-input-record-enum.md | 64 +++++++++++++++++++ internal/win32/key.mbt | 29 --------- internal/win32/mouse.mbt | 8 --- internal/win32/non_windows.mbt | 3 - internal/win32/non_windows_test.mbt | 5 -- internal/win32/record.mbt | 37 ----------- internal/win32/record_test.mbt | 9 --- internal/win32/record_wbtest.mbt | 6 -- non_windows_imports.mbt | 6 ++ 9 files changed, 70 insertions(+), 97 deletions(-) delete mode 100644 internal/win32/non_windows.mbt delete mode 100644 internal/win32/non_windows_test.mbt create mode 100644 non_windows_imports.mbt diff --git a/docs/plans/2026-06-09-windows-input-record-enum.md b/docs/plans/2026-06-09-windows-input-record-enum.md index 9ca0465..bc4b52f 100644 --- a/docs/plans/2026-06-09-windows-input-record-enum.md +++ b/docs/plans/2026-06-09-windows-input-record-enum.md @@ -401,3 +401,67 @@ input decoder. - Note: `moon info` still rewrites the pre-existing root `Fd::fd` generated mbti spelling from `Int` to `@types.Fd`; this follow-up restored the root generated interface to avoid unrelated public API churn. + +## Follow-up: Keep Win32 Parser Imports Used On Non-Windows CI + +### Goal + +Fix `moon check --deny-warn` on macOS and Ubuntu after adding the Windows +console byte queue, without suppressing unused-package warnings. + +### Accepted Design + +- Treat `internal/win32` as a pure parser/mapping package that can compile on + all native platforms. +- Remove Windows platform gates from the pure `internal/win32` record, key, and + mouse parser/mapping declarations and tests. +- Keep root Windows console backend, `ReadConsoleInputW` FFI, and actual tty + dispatch Windows-only. +- Remove the previous non-Windows `internal/win32` import anchor files because + the package will now use `@public_input` directly on every platform. +- Add a root non-Windows private import anchor that references + `@internal_io.ByteQueue::new` and `@win32.InputRecord::parse`. +- Do not add warning suppression and do not change the root public API. + +### Public API / Interface Diff + +- No root public MoonBit API change intended. +- `internal/win32` generated interface should stay on the same parser/mapping + API across platforms. + +### Validation Plan + +- `moon fmt` +- `moon check --deny-warn` +- `moon check --deny-warn --target all` +- `moon test internal/win32 --target-dir .moon-test-internal-win32-ci-anchor-build` +- `moon test internal/io --target-dir .moon-test-internal-io-ci-anchor-build` +- `moon test . --filter "win32*" --target-dir .moon-test-win32-ci-anchor-build` +- `moon info --target-dir .moon-info-ci-anchor-build` +- Review `.mbti` diff. +- `git diff --check` + +### Result + +- Removed Windows platform gates from the pure `internal/win32` parser, + key-mapping, mouse-mapping, and parser test declarations. +- Removed the earlier non-Windows `internal/win32` import anchor files. +- Added a root non-Windows private import anchor for `@internal_io` and + `@win32`. +- Kept root Windows console backend and `ReadConsoleInputW` FFI code + Windows-only. + +### Validation Results + +- Passed: `moon fmt` +- Passed: `moon check --deny-warn --target-dir .moon-check-deny-ci-anchor-build` +- Passed: `moon check --deny-warn --target all --target-dir .moon-check-all-deny-ci-anchor-build` +- Passed: `moon test internal/win32 --target-dir .moon-test-internal-win32-ci-anchor-build` +- Passed: `moon test internal/io --target-dir .moon-test-internal-io-ci-anchor-build` +- Passed: `moon test . --filter "win32*" --target-dir .moon-test-win32-ci-anchor-build` +- Passed: `moon info --target-dir .moon-info-ci-anchor-build` +- Passed: `git diff --check` +- `.mbti` review: `internal/win32` and `internal/io` generated interfaces did + not change. `moon info` still rewrites the pre-existing root `Fd::fd` + generated mbti spelling from `Int` to `@types.Fd`; this follow-up restored + the root generated interface to avoid unrelated public API churn. diff --git a/internal/win32/key.mbt b/internal/win32/key.mbt index f96e2dc..c886689 100644 --- a/internal/win32/key.mbt +++ b/internal/win32/key.mbt @@ -1,11 +1,9 @@ ///| -#cfg(platform="windows") pub fn KeyRecord::is_down(self : KeyRecord) -> Bool { self.key_down != 0 } ///| -#cfg(platform="windows") pub fn KeyRecord::repeat_count(self : KeyRecord) -> Int { if self.repeat_count > 0 { self.repeat_count @@ -15,19 +13,16 @@ pub fn KeyRecord::repeat_count(self : KeyRecord) -> Int { } ///| -#cfg(platform="windows") pub fn KeyRecord::text_uses_decoder(self : KeyRecord) -> Bool { self.key_down != 0 && !self.has_direct_key_metadata() } ///| -#cfg(platform="windows") fn KeyRecord::has_direct_key_metadata(self : KeyRecord) -> Bool { (self.has_keyboard_modifier() && !self.is_alt_gr_text()) || self.is_keypad() } ///| -#cfg(platform="windows") fn KeyRecord::has_keyboard_modifier(self : KeyRecord) -> Bool { has_state( self.control_key_state, @@ -40,7 +35,6 @@ fn KeyRecord::has_keyboard_modifier(self : KeyRecord) -> Bool { } ///| -#cfg(platform="windows") fn KeyRecord::is_alt_gr_text(self : KeyRecord) -> Bool { let alt_gr_state = RightAltPressed | LeftCtrlPressed self.key_down != 0 && @@ -50,7 +44,6 @@ fn KeyRecord::is_alt_gr_text(self : KeyRecord) -> Bool { } ///| -#cfg(platform="windows") pub fn KeyRecord::text( self : KeyRecord, pending_surrogate : Ref[Int], @@ -79,7 +72,6 @@ pub fn KeyRecord::text( } ///| -#cfg(platform="windows") pub fn KeyRecord::input( self : KeyRecord, pending_surrogate : Ref[Int], @@ -89,7 +81,6 @@ pub fn KeyRecord::input( } ///| -#cfg(platform="windows") fn KeyRecord::key_kind(self : KeyRecord) -> @public_input.KeyEventKind { if self.key_down == 0 { Release @@ -101,13 +92,11 @@ fn KeyRecord::key_kind(self : KeyRecord) -> @public_input.KeyEventKind { } ///| -#cfg(platform="windows") fn KeyRecord::modifiers(self : KeyRecord) -> @public_input.KeyModifiers { modifiers_from_control_key_state(self.control_key_state) } ///| -#cfg(platform="windows") fn KeyRecord::key_state(self : KeyRecord) -> @public_input.KeyEventState { @public_input.KeyEventState::new( keypad=self.is_keypad(), @@ -117,7 +106,6 @@ fn KeyRecord::key_state(self : KeyRecord) -> @public_input.KeyEventState { } ///| -#cfg(platform="windows") fn KeyRecord::key_event( self : KeyRecord, pending_surrogate : Ref[Int], @@ -162,7 +150,6 @@ fn KeyRecord::key_event( } ///| -#cfg(platform="windows") fn KeyRecord::key_code_from_virtual_key( self : KeyRecord, ) -> @public_input.KeyCode? { @@ -174,7 +161,6 @@ fn KeyRecord::key_code_from_virtual_key( } ///| -#cfg(platform="windows") fn KeyRecord::is_keypad(self : KeyRecord) -> Bool { virtual_key_is_keypad(self.virtual_key_code) || self.is_keypad_enter() || @@ -182,21 +168,18 @@ fn KeyRecord::is_keypad(self : KeyRecord) -> Bool { } ///| -#cfg(platform="windows") fn KeyRecord::is_keypad_enter(self : KeyRecord) -> Bool { self.virtual_key_code == 0x0d && has_state(self.control_key_state, EnhancedKey) } ///| -#cfg(platform="windows") fn KeyRecord::is_keypad_navigation(self : KeyRecord) -> Bool { virtual_key_is_navigation(self.virtual_key_code) && !has_state(self.control_key_state, EnhancedKey) } ///| -#cfg(platform="windows") fn KeyRecord::char_key_event( self : KeyRecord, char : Char, @@ -212,7 +195,6 @@ fn KeyRecord::char_key_event( } ///| -#cfg(platform="windows") fn text_for_key(record : KeyRecord, char : Char) -> String? { if record.key_down == 0 { None @@ -222,7 +204,6 @@ fn text_for_key(record : KeyRecord, char : Char) -> String? { } ///| -#cfg(platform="windows") fn key_code_from_virtual_key(vk : Int) -> @public_input.KeyCode? { if vk >= 0x70 && vk <= 0x87 { return function_key(vk - 0x6f) @@ -258,7 +239,6 @@ fn key_code_from_virtual_key(vk : Int) -> @public_input.KeyCode? { } ///| -#cfg(platform="windows") fn control_code_from_char(charcode : Int) -> @public_input.KeyCode? { match charcode { 0x00 => Some(Char(' ')) @@ -270,7 +250,6 @@ fn control_code_from_char(charcode : Int) -> @public_input.KeyCode? { } ///| -#cfg(platform="windows") fn control_code_from_virtual_key(vk : Int) -> @public_input.KeyCode? { if vk == 0x20 { Some(Char(' ')) @@ -282,7 +261,6 @@ fn control_code_from_virtual_key(vk : Int) -> @public_input.KeyCode? { } ///| -#cfg(platform="windows") fn function_key(number : Int) -> @public_input.KeyCode? { match number { 1 => Some(F1) @@ -314,7 +292,6 @@ fn function_key(number : Int) -> @public_input.KeyCode? { } ///| -#cfg(platform="windows") fn keypad_digit(number : Int) -> @public_input.KeyCode? { match number { 0 => Some(Keypad0) @@ -332,13 +309,11 @@ fn keypad_digit(number : Int) -> @public_input.KeyCode? { } ///| -#cfg(platform="windows") fn virtual_key_is_keypad(vk : Int) -> Bool { vk >= 0x60 && vk <= 0x6f } ///| -#cfg(platform="windows") fn virtual_key_is_navigation(vk : Int) -> Bool { match vk { 0x21 | 0x22 | 0x23 | 0x24 | 0x25 | 0x26 | 0x27 | 0x28 | 0x2d | 0x2e => true @@ -347,25 +322,21 @@ fn virtual_key_is_navigation(vk : Int) -> Bool { } ///| -#cfg(platform="windows") fn has_state(state : Int, mask : Int) -> Bool { (state & mask) != 0 } ///| -#cfg(platform="windows") fn is_high_surrogate(code : Int) -> Bool { code >= 0xd800 && code <= 0xdbff } ///| -#cfg(platform="windows") fn is_low_surrogate(code : Int) -> Bool { code >= 0xdc00 && code <= 0xdfff } ///| -#cfg(platform="windows") fn decode_surrogate_pair(high : Int, low : Int) -> Char { let code = 0x10000 + (high - 0xd800) * 0x400 + (low - 0xdc00) code.unsafe_to_char() diff --git a/internal/win32/mouse.mbt b/internal/win32/mouse.mbt index cab7509..5530b63 100644 --- a/internal/win32/mouse.mbt +++ b/internal/win32/mouse.mbt @@ -1,5 +1,4 @@ ///| -#cfg(platform="windows") pub fn MouseButtonState::input_event( self : MouseButtonState, record : MouseRecord, @@ -21,7 +20,6 @@ pub fn MouseButtonState::input_event( } ///| -#cfg(platform="windows") fn MouseButtonState::event_kind( self : MouseButtonState, record : MouseRecord, @@ -80,13 +78,11 @@ fn MouseButtonState::event_kind( } ///| -#cfg(platform="windows") fn MouseRecord::modifiers(self : MouseRecord) -> @public_input.KeyModifiers { modifiers_from_control_key_state(self.control_key_state) } ///| -#cfg(platform="windows") fn newly_pressed_mouse_button( previous : Int, buttons : Int, @@ -105,7 +101,6 @@ fn newly_pressed_mouse_button( } ///| -#cfg(platform="windows") fn newly_released_mouse_button( previous : Int, buttons : Int, @@ -124,7 +119,6 @@ fn newly_released_mouse_button( } ///| -#cfg(platform="windows") fn mouse_button_from_state(state : Int) -> @public_input.MouseButton? { if (state & MouseLeftButton) != 0 { Some(Left) @@ -138,14 +132,12 @@ fn mouse_button_from_state(state : Int) -> @public_input.MouseButton? { } ///| -#cfg(platform="windows") fn signed_high_word(value : Int) -> Int { let low_word = value & 0xffff (value - low_word) / 0x10000 } ///| -#cfg(platform="windows") fn modifiers_from_control_key_state(state : Int) -> @public_input.KeyModifiers { @public_input.KeyModifiers::new( shift=has_state(state, ShiftPressed), diff --git a/internal/win32/non_windows.mbt b/internal/win32/non_windows.mbt deleted file mode 100644 index 3739dcc..0000000 --- a/internal/win32/non_windows.mbt +++ /dev/null @@ -1,3 +0,0 @@ -///| -#cfg(not(platform="windows")) -let _public_input_import_anchor : @public_input.InputEvent? = None diff --git a/internal/win32/non_windows_test.mbt b/internal/win32/non_windows_test.mbt deleted file mode 100644 index 98108fe..0000000 --- a/internal/win32/non_windows_test.mbt +++ /dev/null @@ -1,5 +0,0 @@ -///| -#cfg(not(platform="windows")) -test "non-Windows public input import anchor" { - let _ : @public_input.InputEvent? = None -} diff --git a/internal/win32/record.mbt b/internal/win32/record.mbt index fba69fe..92e01dd 100644 --- a/internal/win32/record.mbt +++ b/internal/win32/record.mbt @@ -1,85 +1,64 @@ ///| -#cfg(platform="windows") pub const RecordKey : Int = 0x0001 ///| -#cfg(platform="windows") pub const RecordMouse : Int = 0x0002 ///| -#cfg(platform="windows") pub const RecordWindowBufferSize : Int = 0x0004 ///| -#cfg(platform="windows") pub const RecordFocus : Int = 0x0010 ///| -#cfg(platform="windows") pub const RightAltPressed : Int = 0x0001 ///| -#cfg(platform="windows") pub const LeftAltPressed : Int = 0x0002 ///| -#cfg(platform="windows") pub const RightCtrlPressed : Int = 0x0004 ///| -#cfg(platform="windows") pub const LeftCtrlPressed : Int = 0x0008 ///| -#cfg(platform="windows") pub const ShiftPressed : Int = 0x0010 ///| -#cfg(platform="windows") pub const NumLockOn : Int = 0x0020 ///| -#cfg(platform="windows") pub const CapsLockOn : Int = 0x0080 ///| -#cfg(platform="windows") pub const EnhancedKey : Int = 0x0100 ///| -#cfg(platform="windows") pub const MouseMoved : Int = 0x0001 ///| -#cfg(platform="windows") pub const MouseDoubleClick : Int = 0x0002 ///| -#cfg(platform="windows") pub const MouseWheeled : Int = 0x0004 ///| -#cfg(platform="windows") pub const MouseHorizontalWheeled : Int = 0x0008 ///| -#cfg(platform="windows") pub const MouseLeftButton : Int = 0x0001 ///| -#cfg(platform="windows") pub const MouseRightButton : Int = 0x0002 ///| -#cfg(platform="windows") pub const MouseMiddleButton : Int = 0x0004 ///| -#cfg(platform="windows") const MouseButtonMask : Int = 0x0007 ///| -#cfg(platform="windows") pub(all) enum InputRecord { Key(KeyRecord) Mouse(MouseRecord) @@ -89,14 +68,12 @@ pub(all) enum InputRecord { } ///| -#cfg(platform="windows") pub struct Coord { x : Int y : Int } ///| -#cfg(platform="windows") pub struct KeyRecord { key_down : Int repeat_count : Int @@ -107,7 +84,6 @@ pub struct KeyRecord { } ///| -#cfg(platform="windows") pub struct MouseRecord { x : Int y : Int @@ -117,13 +93,11 @@ pub struct MouseRecord { } ///| -#cfg(platform="windows") pub struct MouseButtonState { mut buttons : Int } ///| -#cfg(platform="windows") pub fn KeyRecord::new( key_down? : Int = 1, repeat_count? : Int = 1, @@ -143,7 +117,6 @@ pub fn KeyRecord::new( } ///| -#cfg(platform="windows") pub fn MouseRecord::new( x? : Int = 0, y? : Int = 0, @@ -155,13 +128,11 @@ pub fn MouseRecord::new( } ///| -#cfg(platform="windows") pub fn MouseButtonState::new() -> MouseButtonState { { buttons: 0 } } ///| -#cfg(platform="windows") pub fn InputRecord::parse(record : Bytes) -> InputRecord { let bytes = record[:] guard u16_at(bytes, 0) is Some(event_type) else { return Unsupported(0) } @@ -175,7 +146,6 @@ pub fn InputRecord::parse(record : Bytes) -> InputRecord { } ///| -#cfg(platform="windows") fn parse_key_record(bytes : BytesView) -> InputRecord { guard bool_at(bytes, 4) is Some(key_down) else { return Unsupported(RecordKey) @@ -208,7 +178,6 @@ fn parse_key_record(bytes : BytesView) -> InputRecord { } ///| -#cfg(platform="windows") fn parse_mouse_record(bytes : BytesView) -> InputRecord { guard i16_at(bytes, 4) is Some(x) else { return Unsupported(RecordMouse) } guard i16_at(bytes, 6) is Some(y) else { return Unsupported(RecordMouse) } @@ -227,7 +196,6 @@ fn parse_mouse_record(bytes : BytesView) -> InputRecord { } ///| -#cfg(platform="windows") fn parse_window_buffer_size_record(bytes : BytesView) -> InputRecord { guard i16_at(bytes, 4) is Some(x) else { return Unsupported(RecordWindowBufferSize) @@ -239,7 +207,6 @@ fn parse_window_buffer_size_record(bytes : BytesView) -> InputRecord { } ///| -#cfg(platform="windows") fn parse_focus_record(bytes : BytesView) -> InputRecord { guard bool_at(bytes, 4) is Some(focus_set) else { return Unsupported(RecordFocus) @@ -248,13 +215,11 @@ fn parse_focus_record(bytes : BytesView) -> InputRecord { } ///| -#cfg(platform="windows") fn bool_at(bytes : BytesView, offset : Int) -> Bool? { dword_at(bytes, offset).map(value => value != 0) } ///| -#cfg(platform="windows") fn i16_at(bytes : BytesView, offset : Int) -> Int? { guard u16_at(bytes, offset) is Some(value) else { return None } if value >= 0x8000 { @@ -265,7 +230,6 @@ fn i16_at(bytes : BytesView, offset : Int) -> Int? { } ///| -#cfg(platform="windows") fn u16_at(bytes : BytesView, offset : Int) -> Int? { if offset < 0 || offset + 1 >= bytes.length() { return None @@ -274,7 +238,6 @@ fn u16_at(bytes : BytesView, offset : Int) -> Int? { } ///| -#cfg(platform="windows") fn dword_at(bytes : BytesView, offset : Int) -> Int? { guard u16_at(bytes, offset) is Some(low) else { return None } guard i16_at(bytes, offset + 2) is Some(high) else { return None } diff --git a/internal/win32/record_test.mbt b/internal/win32/record_test.mbt index da156b6..a91d59f 100644 --- a/internal/win32/record_test.mbt +++ b/internal/win32/record_test.mbt @@ -1,5 +1,4 @@ ///| -#cfg(platform="windows") fn test_raw_record(event_type : Int) -> FixedArray[Byte] { let buf = FixedArray::make(20, b'\x00') test_set_u16_le(buf, 0, event_type) @@ -7,20 +6,17 @@ fn test_raw_record(event_type : Int) -> FixedArray[Byte] { } ///| -#cfg(platform="windows") fn test_input_record(buf : FixedArray[Byte]) -> InputRecord { InputRecord::parse(buf.unsafe_reinterpret_as_bytes()) } ///| -#cfg(platform="windows") fn test_set_u16_le(buf : FixedArray[Byte], offset : Int, value : Int) -> Unit { buf[offset] = (value & 0xff).to_byte() buf[offset + 1] = ((value / 0x100) & 0xff).to_byte() } ///| -#cfg(platform="windows") fn test_set_u32_le(buf : FixedArray[Byte], offset : Int, value : Int) -> Unit { buf[offset] = (value & 0xff).to_byte() buf[offset + 1] = ((value / 0x100) & 0xff).to_byte() @@ -29,7 +25,6 @@ fn test_set_u32_le(buf : FixedArray[Byte], offset : Int, value : Int) -> Unit { } ///| -#cfg(platform="windows") test "win32 raw input record parses focus records" { let buf = test_raw_record(RecordFocus) test_set_u32_le(buf, 4, 1) @@ -40,7 +35,6 @@ test "win32 raw input record parses focus records" { } ///| -#cfg(platform="windows") test "win32 raw input record parses window buffer size records" { let buf = test_raw_record(RecordWindowBufferSize) test_set_u16_le(buf, 4, 80) @@ -52,7 +46,6 @@ test "win32 raw input record parses window buffer size records" { } ///| -#cfg(platform="windows") test "win32 raw input record parses unsupported records" { match InputRecord::parse(b"\x01") { Unsupported(0) => () @@ -66,7 +59,6 @@ test "win32 raw input record parses unsupported records" { } ///| -#cfg(platform="windows") test "win32 raw input record parses key records to input events" { let buf = test_raw_record(RecordKey) test_set_u32_le(buf, 4, 1) @@ -94,7 +86,6 @@ test "win32 raw input record parses key records to input events" { } ///| -#cfg(platform="windows") test "win32 raw input record parses mouse records to input events" { let buf = test_raw_record(RecordMouse) test_set_u16_le(buf, 4, 30) diff --git a/internal/win32/record_wbtest.mbt b/internal/win32/record_wbtest.mbt index b612925..43554c6 100644 --- a/internal/win32/record_wbtest.mbt +++ b/internal/win32/record_wbtest.mbt @@ -1,5 +1,4 @@ ///| -#cfg(platform="windows") fn test_raw_record(event_type : Int) -> FixedArray[Byte] { let buf = FixedArray::make(20, b'\x00') test_set_u16_le(buf, 0, event_type) @@ -7,20 +6,17 @@ fn test_raw_record(event_type : Int) -> FixedArray[Byte] { } ///| -#cfg(platform="windows") fn test_input_record(buf : FixedArray[Byte]) -> InputRecord { InputRecord::parse(buf.unsafe_reinterpret_as_bytes()) } ///| -#cfg(platform="windows") fn test_set_u16_le(buf : FixedArray[Byte], offset : Int, value : Int) -> Unit { buf[offset] = (value & 0xff).to_byte() buf[offset + 1] = ((value / 0x100) & 0xff).to_byte() } ///| -#cfg(platform="windows") fn test_set_u32_le(buf : FixedArray[Byte], offset : Int, value : Int) -> Unit { buf[offset] = (value & 0xff).to_byte() buf[offset + 1] = ((value / 0x100) & 0xff).to_byte() @@ -29,7 +25,6 @@ fn test_set_u32_le(buf : FixedArray[Byte], offset : Int, value : Int) -> Unit { } ///| -#cfg(platform="windows") test "win32 raw input record parses key records" { let buf = test_raw_record(RecordKey) test_set_u32_le(buf, 4, 1) @@ -52,7 +47,6 @@ test "win32 raw input record parses key records" { } ///| -#cfg(platform="windows") test "win32 raw input record parses mouse records" { let buf = test_raw_record(RecordMouse) test_set_u16_le(buf, 4, 30) diff --git a/non_windows_imports.mbt b/non_windows_imports.mbt new file mode 100644 index 0000000..8c76f9e --- /dev/null +++ b/non_windows_imports.mbt @@ -0,0 +1,6 @@ +///| +#cfg(not(platform="windows")) +let _windows_internal_import_anchor : Unit = { + ignore(@internal_io.ByteQueue::new) + ignore(@win32.InputRecord::parse) +} From 15a79265786ac9cb9ef38400c491ba8519d9dc42 Mon Sep 17 00:00:00 2001 From: Haoxiang Fei Date: Wed, 10 Jun 2026 11:27:58 +0800 Subject: [PATCH 10/12] fix(win32): address input review feedback --- examples/raw/main.mbt | 9 +- internal/io/byte_queue.mbt | 33 ++++---- internal/win32/key.mbt | 17 ++-- internal/win32/mouse.mbt | 44 +++++----- internal/win32/record_test.mbt | 60 +++++++------ internal/win32/record_wbtest.mbt | 34 ++++---- win32_input.mbt | 139 ++++++++++++++----------------- win32_input_wbtest.mbt | 73 +++++++--------- 8 files changed, 186 insertions(+), 223 deletions(-) diff --git a/examples/raw/main.mbt b/examples/raw/main.mbt index eb29823..db52483 100644 --- a/examples/raw/main.mbt +++ b/examples/raw/main.mbt @@ -19,12 +19,9 @@ fn byte_to_raw_line(byte : Byte) -> String { line.write_string("0x") line.write_char(number_to_xdigit[byte.to_int() / 16].to_char()) line.write_char(number_to_xdigit[byte.to_int() % 16].to_char()) - match byte_to_printable_char(byte) { - Some(char) => { - line.write_string(" ") - line.write_string(char.escape(quote=true)) - } - None => () + if byte_to_printable_char(byte) is Some(char) { + line.write_string(" ") + line.write_string(char.escape(quote=true)) } line.write_string("\r\n") line.to_string() diff --git a/internal/io/byte_queue.mbt b/internal/io/byte_queue.mbt index c13dfb6..30bbace 100644 --- a/internal/io/byte_queue.mbt +++ b/internal/io/byte_queue.mbt @@ -18,11 +18,7 @@ pub fn ByteQueue::close(self : ByteQueue) -> Unit { ///| pub fn ByteQueue::put_byte(self : ByteQueue, byte : Byte) -> Unit { - try self.queue.try_put(byte) catch { - _ => () - } noraise { - _ => () - } + guard (try! self.queue.try_put(byte)) } ///| @@ -53,22 +49,23 @@ pub impl @async/io.Reader for ByteQueue with fn _direct_read( } noraise { byte => byte } - dst[offset] = first - let mut len = 1 - while len < max_len { - let next = try self.queue.try_get() catch { - @async/io.ReaderClosed => None - error => raise error - } noraise { - byte => byte - } - match next { - Some(byte) => { + @async.protect_from_cancel() <| () => { + dst[offset] = first + let mut len = 1 + while len < max_len { + let next = try self.queue.try_get() catch { + @async/io.ReaderClosed => None + error => raise error + } noraise { + byte => byte + } + if next is Some(byte) { dst[offset + len] = byte len += 1 + } else { + break } - None => break } + len } - len } diff --git a/internal/win32/key.mbt b/internal/win32/key.mbt index c886689..11450fc 100644 --- a/internal/win32/key.mbt +++ b/internal/win32/key.mbt @@ -68,7 +68,7 @@ pub fn KeyRecord::text( return Some([char]) } pending_surrogate.val = 0 - Some([charcode.unsafe_to_char()]) + Some([codepoint_to_char(charcode)]) } ///| @@ -145,7 +145,7 @@ fn KeyRecord::key_event( return Some(self.char_key_event(char, modifiers, kind, state)) } pending_surrogate.val = 0 - let char = charcode.unsafe_to_char() + let char = codepoint_to_char(charcode) Some(self.char_key_event(char, modifiers, kind, state)) } @@ -242,9 +242,9 @@ fn key_code_from_virtual_key(vk : Int) -> @public_input.KeyCode? { fn control_code_from_char(charcode : Int) -> @public_input.KeyCode? { match charcode { 0x00 => Some(Char(' ')) - 0x01..=0x1a => Some(Char((charcode - 1 + 'a'.to_int()).unsafe_to_char())) + 0x01..=0x1a => Some(Char(codepoint_to_char(charcode - 1 + 'a'.to_int()))) 0x1b => Some(Escape) - 0x1c..=0x1f => Some(Char((charcode - 0x1c + '4'.to_int()).unsafe_to_char())) + 0x1c..=0x1f => Some(Char(codepoint_to_char(charcode - 0x1c + '4'.to_int()))) _ => None } } @@ -254,7 +254,7 @@ fn control_code_from_virtual_key(vk : Int) -> @public_input.KeyCode? { if vk == 0x20 { Some(Char(' ')) } else if vk >= 0x41 && vk <= 0x5a { - Some(Char((vk - 0x41 + 'a'.to_int()).unsafe_to_char())) + Some(Char(codepoint_to_char(vk - 0x41 + 'a'.to_int()))) } else { None } @@ -339,5 +339,10 @@ fn is_low_surrogate(code : Int) -> Bool { ///| fn decode_surrogate_pair(high : Int, low : Int) -> Char { let code = 0x10000 + (high - 0xd800) * 0x400 + (low - 0xdc00) - code.unsafe_to_char() + codepoint_to_char(code) +} + +///| +fn codepoint_to_char(code : Int) -> Char { + code.to_char().unwrap_or('\u{FFFD}') } diff --git a/internal/win32/mouse.mbt b/internal/win32/mouse.mbt index 5530b63..33dc4e7 100644 --- a/internal/win32/mouse.mbt +++ b/internal/win32/mouse.mbt @@ -3,20 +3,17 @@ pub fn MouseButtonState::input_event( self : MouseButtonState, record : MouseRecord, ) -> @public_input.InputEvent? { - match self.event_kind(record) { - Some(kind) => - Some( - Mouse( - @public_input.MouseEvent::new( - kind, - record.y + 1, - record.x + 1, - modifiers=record.modifiers(), - ), - ), - ) - None => None - } + guard self.event_kind(record) is Some(kind) else { return None } + Some( + Mouse( + @public_input.MouseEvent::new( + kind, + record.y + 1, + record.x + 1, + modifiers=record.modifiers(), + ), + ), + ) } ///| @@ -29,19 +26,20 @@ fn MouseButtonState::event_kind( 0 => { let previous = self.buttons self.buttons = buttons - match newly_pressed_mouse_button(previous, buttons) { - Some(button) => Some(Press(button)) - None => - newly_released_mouse_button(previous, buttons).map(button => { - Release(button) - }) + if newly_pressed_mouse_button(previous, buttons) is Some(button) { + Some(Press(button)) + } else if newly_released_mouse_button(previous, buttons) is Some(button) { + Some(Release(button)) + } else { + None } } MouseMoved => { self.buttons = buttons - match mouse_button_from_state(buttons) { - Some(button) => Some(Drag(button)) - None => Some(Move) + if mouse_button_from_state(buttons) is Some(button) { + Some(Drag(button)) + } else { + Some(Move) } } MouseDoubleClick => { diff --git a/internal/win32/record_test.mbt b/internal/win32/record_test.mbt index a91d59f..20b1a47 100644 --- a/internal/win32/record_test.mbt +++ b/internal/win32/record_test.mbt @@ -67,22 +67,21 @@ test "win32 raw input record parses key records to input events" { test_set_u16_le(buf, 12, 0x1e) test_set_u16_le(buf, 14, 'A'.to_int()) test_set_u32_le(buf, 16, ShiftPressed) - match test_input_record(buf) { - Key(record) => - @debug.assert_eq( - record.input(Ref(0)), - Some( - Key( - @public_input.KeyEvent::new( - Char('A'), - modifiers=@public_input.KeyModifiers::new(shift=true), - text="A", - ), - ), - ), - ) - _ => fail("expected key record") + guard test_input_record(buf) is Key(record) else { + fail("expected key record") } + @debug.assert_eq( + record.input(Ref(0)), + Some( + Key( + @public_input.KeyEvent::new( + Char('A'), + modifiers=@public_input.KeyModifiers::new(shift=true), + text="A", + ), + ), + ), + ) } ///| @@ -92,21 +91,20 @@ test "win32 raw input record parses mouse records to input events" { test_set_u16_le(buf, 6, 2) test_set_u32_le(buf, 12, ShiftPressed) test_set_u32_le(buf, 16, MouseMoved) - match test_input_record(buf) { - Mouse(record) => - @debug.assert_eq( - MouseButtonState::new().input_event(record), - Some( - Mouse( - @public_input.MouseEvent::new( - Move, - 3, - 31, - modifiers=@public_input.KeyModifiers::new(shift=true), - ), - ), - ), - ) - _ => fail("expected mouse record") + guard test_input_record(buf) is Mouse(record) else { + fail("expected mouse record") } + @debug.assert_eq( + MouseButtonState::new().input_event(record), + Some( + Mouse( + @public_input.MouseEvent::new( + Move, + 3, + 31, + modifiers=@public_input.KeyModifiers::new(shift=true), + ), + ), + ), + ) } diff --git a/internal/win32/record_wbtest.mbt b/internal/win32/record_wbtest.mbt index 43554c6..b9e34d9 100644 --- a/internal/win32/record_wbtest.mbt +++ b/internal/win32/record_wbtest.mbt @@ -33,17 +33,15 @@ test "win32 raw input record parses key records" { test_set_u16_le(buf, 12, 0x1e) test_set_u16_le(buf, 14, 'A'.to_int()) test_set_u32_le(buf, 16, ShiftPressed) - match test_input_record(buf) { - Key(record) => { - @debug.assert_eq(record.key_down, 1) - @debug.assert_eq(record.repeat_count, 2) - @debug.assert_eq(record.virtual_key_code, 0x41) - @debug.assert_eq(record.virtual_scan_code, 0x1e) - @debug.assert_eq(record.unicode_char, 'A'.to_int()) - @debug.assert_eq(record.control_key_state, ShiftPressed) - } - _ => fail("expected key record") + guard test_input_record(buf) is Key(record) else { + fail("expected key record") } + @debug.assert_eq(record.key_down, 1) + @debug.assert_eq(record.repeat_count, 2) + @debug.assert_eq(record.virtual_key_code, 0x41) + @debug.assert_eq(record.virtual_scan_code, 0x1e) + @debug.assert_eq(record.unicode_char, 'A'.to_int()) + @debug.assert_eq(record.control_key_state, ShiftPressed) } ///| @@ -54,14 +52,12 @@ test "win32 raw input record parses mouse records" { test_set_u32_le(buf, 8, MouseLeftButton) test_set_u32_le(buf, 12, ShiftPressed) test_set_u32_le(buf, 16, MouseMoved) - match test_input_record(buf) { - Mouse(record) => { - @debug.assert_eq(record.x, 30) - @debug.assert_eq(record.y, 2) - @debug.assert_eq(record.button_state, MouseLeftButton) - @debug.assert_eq(record.control_key_state, ShiftPressed) - @debug.assert_eq(record.event_flags, MouseMoved) - } - _ => fail("expected mouse record") + guard test_input_record(buf) is Mouse(record) else { + fail("expected mouse record") } + @debug.assert_eq(record.x, 30) + @debug.assert_eq(record.y, 2) + @debug.assert_eq(record.button_state, MouseLeftButton) + @debug.assert_eq(record.control_key_state, ShiftPressed) + @debug.assert_eq(record.event_flags, MouseMoved) } diff --git a/win32_input.mbt b/win32_input.mbt index cf3dadb..74f2d1f 100644 --- a/win32_input.mbt +++ b/win32_input.mbt @@ -128,14 +128,11 @@ async fn Tty::read_console_input_event( ///| #cfg(platform="windows") async fn Tty::read_records(self : Self) -> Unit { - match self.input_backend { - Console(input_fd~, ..) => - for ;; { - if !self.drain_records(input_fd) { - @async.sleep(10) - } - } - ByteStream => () + guard self.input_backend is Console(input_fd~, ..) else { return } + for ;; { + if !self.drain_records(input_fd) { + @async.sleep(10) + } } } @@ -144,12 +141,11 @@ async fn Tty::read_records(self : Self) -> Unit { fn Tty::drain_records(self : Self, input_fd : @async/types.Fd) -> Bool raise { let mut read_any = false for ;; { - match win32_read_console_input_record(input_fd) { - None => return read_any - Some(record) => { - read_any = true - self.read_record(record) - } + if win32_read_console_input_record(input_fd) is Some(record) { + read_any = true + self.read_record(record) + } else { + return read_any } } } @@ -157,94 +153,85 @@ fn Tty::drain_records(self : Self, input_fd : @async/types.Fd) -> Bool raise { ///| #cfg(platform="windows") fn queue_console_event(events : @async.Queue[Event], event : Event) -> Unit { - try events.try_put(event) catch { - _ => () - } noraise { - _ => () - } + guard (try! events.try_put(event)) } ///| #cfg(platform="windows") fn Tty::read_record(self : Self, record : @win32.InputRecord) -> Unit { - match self.input_backend { - Console(events~, pending_surrogate~, ..) => - match record { - Key(record) => self.read_key_record(record) - Mouse(record) => self.read_mouse_record(record) - WindowBufferSize(_) => { - pending_surrogate.val = 0 - try self.window_size() catch { - _ => () - } noraise { - size => queue_console_event(events, Resize(size)) - } - } - Focus(focus_set) => { - pending_surrogate.val = 0 - if focus_set { - queue_console_event(events, Input(FocusIn)) - } else { - queue_console_event(events, Input(FocusOut)) - } - } - Unsupported(_) => pending_surrogate.val = 0 + guard self.input_backend is Console(events~, pending_surrogate~, ..) else { + return + } + match record { + Key(record) => self.read_key_record(record) + Mouse(record) => self.read_mouse_record(record) + WindowBufferSize(_) => { + pending_surrogate.val = 0 + try self.window_size() catch { + _ => () + } noraise { + size => queue_console_event(events, Resize(size)) } - ByteStream => () + } + Focus(focus_set) => { + pending_surrogate.val = 0 + if focus_set { + queue_console_event(events, Input(FocusIn)) + } else { + queue_console_event(events, Input(FocusOut)) + } + } + Unsupported(_) => pending_surrogate.val = 0 } } ///| #cfg(platform="windows") fn Tty::read_key_record(self : Self, record : @win32.KeyRecord) -> Unit { - match self.input_backend { - Console(byte_queue~, pending_surrogate~, ..) => { - if !record.is_down() { - return - } - if !record.text_uses_decoder() { - return self.queue_key_input(record) - } - match record.text(pending_surrogate) { - Some(text) => - for _ in 0.. self.queue_key_input(record) + guard self.input_backend is Console(byte_queue~, pending_surrogate~, ..) else { + return + } + if !record.is_down() { + return + } + if !record.text_uses_decoder() { + return self.queue_key_input(record) + } + match record.text(pending_surrogate) { + Some(text) => + for _ in 0.. () + None => self.queue_key_input(record) } } ///| #cfg(platform="windows") fn Tty::queue_key_input(self : Self, record : @win32.KeyRecord) -> Unit { - match self.input_backend { - Console(events~, pending_surrogate~, ..) => - match record.input(pending_surrogate) { - Some(input) => - for _ in 0.. () + guard self.input_backend is Console(events~, pending_surrogate~, ..) else { + return + } + match record.input(pending_surrogate) { + Some(input) => + for _ in 0.. () + None => () } } ///| #cfg(platform="windows") fn Tty::read_mouse_record(self : Self, record : @win32.MouseRecord) -> Unit { - match self.input_backend { - Console(events~, pending_surrogate~, mouse_button_state~, ..) => { - pending_surrogate.val = 0 - match mouse_button_state.input_event(record) { - Some(input) => queue_console_event(events, Input(input)) - None => () - } - } - ByteStream => () + guard self.input_backend + is Console(events~, pending_surrogate~, mouse_button_state~, ..) else { + return + } + pending_surrogate.val = 0 + match mouse_button_state.input_event(record) { + Some(input) => queue_console_event(events, Input(input)) + None => () } } diff --git a/win32_input_wbtest.mbt b/win32_input_wbtest.mbt index 4183a59..c846d8b 100644 --- a/win32_input_wbtest.mbt +++ b/win32_input_wbtest.mbt @@ -100,16 +100,12 @@ async test "win32 console source decodes bracketed paste key records" { let (dummy_input, dummy_output) = @async/pipe.pipe() let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() - @async.with_task_group() <| group => { - group.spawn_bg() <| () => { - for char in "\u{1B}[200~hello\u{1B}[201~" { - source.read_key_record(win32_test_key_record(char)) - } - } - match source.read_decoded_input_event(50) { - Input(Paste(text)) => @debug.assert_eq(text, "hello") - _ => fail("expected bracketed paste event") - } + for char in "\u{1B}[200~hello\u{1B}[201~" { + source.read_key_record(win32_test_key_record(char)) + } + match source.read_decoded_input_event(50) { + Input(Paste(text)) => @debug.assert_eq(text, "hello") + _ => fail("expected bracketed paste event") } } @@ -119,24 +115,20 @@ async test "win32 console source preserves native events during internal reads" let (dummy_input, dummy_output) = @async/pipe.pipe() let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() - @async.with_task_group() <| group => { - group.spawn_bg() <| () => { - source.read_record(Focus(true)) - for char in "\u{1B}[7;9R" { - source.read_key_record(win32_test_key_record(char)) - } - } - match source.reader.read_event(esc_timeout_ms=50) { - CursorPosition(row~, col~) => { - @debug.assert_eq(row, 7) - @debug.assert_eq(col, 9) - } - _ => fail("expected cursor position event") - } - match win32_test_try_get_event(source) { - Some(Input(FocusIn)) => () - _ => fail("expected native focus event to remain queued") + source.read_record(Focus(true)) + for char in "\u{1B}[7;9R" { + source.read_key_record(win32_test_key_record(char)) + } + match source.reader.read_event(esc_timeout_ms=50) { + CursorPosition(row~, col~) => { + @debug.assert_eq(row, 7) + @debug.assert_eq(col, 9) } + _ => fail("expected cursor position event") + } + match win32_test_try_get_event(source) { + Some(Input(FocusIn)) => () + _ => fail("expected native focus event to remain queued") } } @@ -328,23 +320,16 @@ async test "win32 console source preserves AltGr text input" { let (dummy_input, dummy_output) = @async/pipe.pipe() let source = win32_test_console_tty(dummy_input, dummy_output) defer source.close() - @async.with_task_group() <| group => { - group.spawn_bg() <| () => { - source.read_key_record( - win32_test_key_record( - '@', - control_key_state=@win32.RightAltPressed | @win32.LeftCtrlPressed, - ), - ) - } - match source.read_decoded_input_event(50) { - Input(Key(event)) => - @debug.assert_eq( - event, - @public_input.KeyEvent::new(Char('@'), text="@"), - ) - _ => fail("expected decoded AltGr text key event") - } + source.read_key_record( + win32_test_key_record( + '@', + control_key_state=@win32.RightAltPressed | @win32.LeftCtrlPressed, + ), + ) + match source.read_decoded_input_event(50) { + Input(Key(event)) => + @debug.assert_eq(event, @public_input.KeyEvent::new(Char('@'), text="@")) + _ => fail("expected decoded AltGr text key event") } } From 74b691627cbaa6459b7adab47b64168edc8b3739 Mon Sep 17 00:00:00 2001 From: Haoxiang Fei Date: Wed, 10 Jun 2026 11:53:27 +0800 Subject: [PATCH 11/12] fix(internal): narrow byte queue visibility --- internal/io/byte_queue.mbt | 6 +++--- internal/io/pkg.generated.mbti | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/io/byte_queue.mbt b/internal/io/byte_queue.mbt index 30bbace..a1efe81 100644 --- a/internal/io/byte_queue.mbt +++ b/internal/io/byte_queue.mbt @@ -1,8 +1,8 @@ ///| #warnings("-14") -pub struct ByteQueue { - priv queue : @async.Queue[Byte] - priv read_buf : @async/io.ReaderBuffer +struct ByteQueue { + queue : @async.Queue[Byte] + read_buf : @async/io.ReaderBuffer } ///| diff --git a/internal/io/pkg.generated.mbti b/internal/io/pkg.generated.mbti index f88e8c3..69fd40e 100644 --- a/internal/io/pkg.generated.mbti +++ b/internal/io/pkg.generated.mbti @@ -6,9 +6,7 @@ package "moonbit-community/tty/internal/io" // Errors // Types and methods -pub struct ByteQueue { - // private fields -} +type ByteQueue pub fn ByteQueue::close(Self) -> Unit pub fn ByteQueue::new() -> Self pub fn ByteQueue::put_byte(Self, Byte) -> Unit From a267bf89c367b053d872608a6d2112059dd7768b Mon Sep 17 00:00:00 2001 From: Haoxiang Fei Date: Wed, 10 Jun 2026 13:10:09 +0800 Subject: [PATCH 12/12] fix(internal): remove byte queue cancel shield --- internal/io/byte_queue.mbt | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/internal/io/byte_queue.mbt b/internal/io/byte_queue.mbt index a1efe81..93e9883 100644 --- a/internal/io/byte_queue.mbt +++ b/internal/io/byte_queue.mbt @@ -49,23 +49,21 @@ pub impl @async/io.Reader for ByteQueue with fn _direct_read( } noraise { byte => byte } - @async.protect_from_cancel() <| () => { - dst[offset] = first - let mut len = 1 - while len < max_len { - let next = try self.queue.try_get() catch { - @async/io.ReaderClosed => None - error => raise error - } noraise { - byte => byte - } - if next is Some(byte) { - dst[offset + len] = byte - len += 1 - } else { - break - } + dst[offset] = first + let mut len = 1 + while len < max_len { + let next = try self.queue.try_get() catch { + @async/io.ReaderClosed => None + error => raise error + } noraise { + byte => byte + } + if next is Some(byte) { + dst[offset + len] = byte + len += 1 + } else { + break } - len } + len }