Skip to content

Commit 5aaecf0

Browse files
committed
test(windows): extend VT input diagnostics
1 parent e1c5446 commit 5aaecf0

1 file changed

Lines changed: 134 additions & 9 deletions

File tree

examples/windows-vt-input.rs

Lines changed: 134 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,109 @@
2222
2323
#[cfg(windows)]
2424
mod windows {
25+
use std::collections::HashMap;
2526
use std::io;
2627
use std::time::Duration;
2728

2829
use crossterm::event::{
29-
DisableBracketedPaste, EnableBracketedPaste, Event, KeyCode, KeyEventKind, poll, read,
30+
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
31+
Event, KeyCode, KeyEventKind, KeyModifiers, MouseEventKind, poll, read,
3032
};
3133
use crossterm::execute;
3234
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
3335
use crossterm_winapi::{ConsoleMode, Handle};
3436

3537
const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;
38+
const ENABLE_MOUSE_INPUT: u32 = 0x0010;
39+
const ENABLE_WINDOW_INPUT: u32 = 0x0008;
40+
41+
#[derive(Default)]
42+
struct SessionStats {
43+
mouse_moved: u64,
44+
mouse_clicks: u64,
45+
mouse_drags: u64,
46+
mouse_wheels: u64,
47+
key_presses: u64,
48+
key_releases: u64,
49+
press_balance: HashMap<(KeyCode, KeyModifiers), u32>,
50+
duplicate_candidates: u64,
51+
last_alt_press: Option<KeyCode>,
52+
duplicate_candidate_key: Option<KeyCode>,
53+
}
54+
55+
impl SessionStats {
56+
fn record(&mut self, event: &Event) {
57+
match event {
58+
Event::Mouse(mouse) if matches!(mouse.kind, MouseEventKind::Moved) => {
59+
self.mouse_moved += 1;
60+
}
61+
Event::Mouse(mouse) => match mouse.kind {
62+
MouseEventKind::Down(_) | MouseEventKind::Up(_) => self.mouse_clicks += 1,
63+
MouseEventKind::Drag(_) => self.mouse_drags += 1,
64+
MouseEventKind::ScrollUp
65+
| MouseEventKind::ScrollDown
66+
| MouseEventKind::ScrollLeft
67+
| MouseEventKind::ScrollRight => self.mouse_wheels += 1,
68+
MouseEventKind::Moved => unreachable!(),
69+
},
70+
Event::Key(key) if key.code == KeyCode::Esc => {
71+
// Escape terminates the diagnostic; do not report its intentional lone Press
72+
// as an unmatched key in the final summary.
73+
}
74+
Event::Key(key) => match key.kind {
75+
KeyEventKind::Press => {
76+
self.key_presses += 1;
77+
let identity = (key.code, key.modifiers);
78+
if self.duplicate_candidate_key == Some(key.code) {
79+
self.duplicate_candidates += 1;
80+
self.duplicate_candidate_key = None;
81+
}
82+
if key.modifiers.contains(KeyModifiers::ALT) {
83+
self.last_alt_press = Some(key.code);
84+
} else {
85+
self.last_alt_press = None;
86+
}
87+
*self.press_balance.entry(identity).or_default() += 1;
88+
}
89+
KeyEventKind::Release => {
90+
self.key_releases += 1;
91+
let identity = (key.code, key.modifiers);
92+
if let Some(balance) = self.press_balance.get_mut(&identity) {
93+
*balance = balance.saturating_sub(1);
94+
}
95+
self.duplicate_candidate_key = self
96+
.last_alt_press
97+
.filter(|press_code| *press_code == key.code);
98+
self.last_alt_press = None;
99+
}
100+
KeyEventKind::Repeat => {}
101+
},
102+
_ => {}
103+
}
104+
}
105+
106+
fn print_summary(&self) {
107+
let mismatches = self
108+
.press_balance
109+
.values()
110+
.filter(|count| **count != 0)
111+
.count();
112+
println!("\nSummary:");
113+
println!(
114+
" key Press={}, Release={}",
115+
self.key_presses, self.key_releases
116+
);
117+
println!(
118+
" mouse Moved={} (suppressed from log), click={}, drag={}, wheel={}",
119+
self.mouse_moved, self.mouse_clicks, self.mouse_drags, self.mouse_wheels
120+
);
121+
println!(" Press/Release mismatches={mismatches}");
122+
println!(
123+
" ALT Press -> Release -> same-code NONE Press candidates={} (manual check)",
124+
self.duplicate_candidates
125+
);
126+
}
127+
}
36128

37129
fn current_console_mode() -> io::Result<u32> {
38130
let handle = Handle::current_in_handle()?;
@@ -47,6 +139,9 @@ mod windows {
47139
fn run_session() -> io::Result<()> {
48140
let mut stdout = io::stdout();
49141
let mut raw_enabled = false;
142+
let mut mouse_enabled = false;
143+
let mut paste_enabled = false;
144+
let mut stats = SessionStats::default();
50145

51146
let session_result = (|| {
52147
enable_raw_mode()?;
@@ -61,14 +156,28 @@ mod windows {
61156
"raw-mode VT_INPUT activation: legacy/unsupported fallback (not a failure)"
62157
);
63158
}
159+
println!(
160+
"mouse mode bits after raw mode: MOUSE_INPUT={}, WINDOW_INPUT={}",
161+
raw_mode & ENABLE_MOUSE_INPUT != 0,
162+
raw_mode & ENABLE_WINDOW_INPUT != 0
163+
);
164+
execute!(stdout, EnableMouseCapture)?;
165+
mouse_enabled = true;
166+
let mouse_mode = current_console_mode()?;
167+
print_mode("after EnableMouseCapture", mouse_mode);
64168
execute!(stdout, EnableBracketedPaste)?;
169+
paste_enabled = true;
65170
println!(
66-
"Reading events. Expected: a/F1 Press+Release (Repeat while held), Alt+numpad 0233 as a character, and multiline paste as one Paste event. Press Esc to exit."
171+
"Reading events. Try click, drag, wheel, paste, ordinary keys, Ctrl+Shift+letter, and Alt+numpad 0233. Moved events are counted but not logged. Press Esc to exit."
67172
);
68173

69174
loop {
70175
let event = read()?;
71-
println!("Event: {event:?}");
176+
stats.record(&event);
177+
if !matches!(&event, Event::Mouse(mouse) if matches!(mouse.kind, MouseEventKind::Moved))
178+
{
179+
println!("Event: {event:?}");
180+
}
72181

73182
if let Event::Key(key) = event
74183
&& key.code == KeyCode::Esc
@@ -83,18 +192,33 @@ mod windows {
83192

84193
// Cleanup is deliberately unconditional: a command or read can fail after raw mode or
85194
// bracketed paste has already been enabled.
86-
let disable_paste_result = execute!(stdout, DisableBracketedPaste);
195+
let mut cleanup_error = None;
196+
if mouse_enabled {
197+
if let Err(error) = execute!(stdout, DisableMouseCapture) {
198+
cleanup_error = Some(error);
199+
}
200+
}
201+
if paste_enabled {
202+
if let Err(error) = execute!(stdout, DisableBracketedPaste) {
203+
cleanup_error.get_or_insert(error);
204+
}
205+
}
87206
let disable_raw_result = if raw_enabled {
88207
disable_raw_mode()
89208
} else {
90209
Ok(())
91210
};
211+
if let Err(error) = disable_raw_result {
212+
cleanup_error.get_or_insert(error);
213+
}
214+
stats.print_summary();
92215

93216
if let Err(error) = session_result {
94217
return Err(error);
95218
}
96-
disable_paste_result?;
97-
disable_raw_result?;
219+
if let Some(error) = cleanup_error {
220+
return Err(error);
221+
}
98222
Ok(())
99223
}
100224

@@ -123,10 +247,11 @@ mod windows {
123247
match final_mode_result {
124248
Ok(final_mode) => {
125249
print_mode("after cleanup", final_mode);
126-
let restored = final_mode & ENABLE_VIRTUAL_TERMINAL_INPUT
127-
== original_mode & ENABLE_VIRTUAL_TERMINAL_INPUT;
250+
let restore_mask =
251+
ENABLE_VIRTUAL_TERMINAL_INPUT | ENABLE_MOUSE_INPUT | ENABLE_WINDOW_INPUT;
252+
let restored = final_mode & restore_mask == original_mode & restore_mask;
128253
println!(
129-
"original VT_INPUT bit restoration: {}",
254+
"original VT_INPUT/MOUSE_INPUT/WINDOW_INPUT restoration: {}",
130255
if restored { "PASS" } else { "FAIL" }
131256
);
132257
if !restored && session_result.is_ok() {

0 commit comments

Comments
 (0)