#![allow(unsafe_code)]
use windows_sys::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE};
use windows_sys::Win32::System::Console::{
GetConsoleMode, GetConsoleScreenBufferInfo, GetStdHandle, SetConsoleMode, CONSOLE_MODE,
CONSOLE_SCREEN_BUFFER_INFO, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT,
ENABLE_VIRTUAL_TERMINAL_INPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, STD_ERROR_HANDLE,
STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
};
pub(crate) struct RawMode {
stdin: HANDLE,
stdin_original: CONSOLE_MODE,
stdout: HANDLE,
stdout_original: CONSOLE_MODE,
}
unsafe impl Send for RawMode {}
impl RawMode {
pub(crate) fn enable() -> Option<Self> {
let stdin = unsafe { GetStdHandle(STD_INPUT_HANDLE) };
let stdout = unsafe { GetStdHandle(STD_OUTPUT_HANDLE) };
Self::enable_on(stdin, stdout)
}
pub(super) fn enable_on(stdin: HANDLE, stdout: HANDLE) -> Option<Self> {
if stdin.is_null() || stdin == INVALID_HANDLE_VALUE {
return None;
}
if stdout.is_null() || stdout == INVALID_HANDLE_VALUE {
return None;
}
let mut stdin_original: CONSOLE_MODE = 0;
if unsafe { GetConsoleMode(stdin, &mut stdin_original) } == 0 {
return None;
}
let mut stdout_original: CONSOLE_MODE = 0;
if unsafe { GetConsoleMode(stdout, &mut stdout_original) } == 0 {
return None;
}
let stdin_raw = (stdin_original
& !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT))
| ENABLE_VIRTUAL_TERMINAL_INPUT;
if unsafe { SetConsoleMode(stdin, stdin_raw) } == 0 {
return None;
}
let stdout_raw = stdout_original | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
if unsafe { SetConsoleMode(stdout, stdout_raw) } == 0 {
unsafe { SetConsoleMode(stdin, stdin_original) };
return None;
}
Some(Self {
stdin,
stdin_original,
stdout,
stdout_original,
})
}
}
impl Drop for RawMode {
fn drop(&mut self) {
unsafe {
SetConsoleMode(self.stdin, self.stdin_original);
SetConsoleMode(self.stdout, self.stdout_original);
}
}
}
pub(crate) fn window_size() -> Option<(u16, u16)> {
let stdout = unsafe { GetStdHandle(STD_OUTPUT_HANDLE) };
let stderr = unsafe { GetStdHandle(STD_ERROR_HANDLE) };
size_of(stdout).or_else(|| size_of(stderr))
}
fn size_of(handle: HANDLE) -> Option<(u16, u16)> {
if handle.is_null() || handle == INVALID_HANDLE_VALUE {
return None;
}
let mut info: CONSOLE_SCREEN_BUFFER_INFO = unsafe { std::mem::zeroed() };
if unsafe { GetConsoleScreenBufferInfo(handle, &mut info) } != 0 {
return window_extent(&info);
}
None
}
fn window_extent(info: &CONSOLE_SCREEN_BUFFER_INFO) -> Option<(u16, u16)> {
let rows = i32::from(info.srWindow.Bottom) - i32::from(info.srWindow.Top) + 1;
let cols = i32::from(info.srWindow.Right) - i32::from(info.srWindow.Left) + 1;
if rows <= 0 || cols <= 0 {
return None;
}
Some((rows as u16, cols as u16))
}
pub(crate) struct ResizeWatcher {
interval: tokio::time::Interval,
last: Option<(u16, u16)>,
}
impl ResizeWatcher {
pub(crate) fn new() -> std::io::Result<Self> {
let mut interval = tokio::time::interval(std::time::Duration::from_millis(250));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
Ok(Self {
interval,
last: window_size(),
})
}
pub(crate) async fn next(&mut self) -> (u16, u16) {
loop {
self.interval.tick().await;
if let Some(size) = resize_due(&mut self.last, window_size()) {
return size;
}
}
}
}
fn resize_due(last: &mut Option<(u16, u16)>, current: Option<(u16, u16)>) -> Option<(u16, u16)> {
let current = current?;
if *last == Some(current) {
return None;
}
*last = Some(current);
Some(current)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_non_console_handle_is_declined() {
use std::os::windows::io::AsRawHandle;
let devnull = std::fs::File::open("NUL").expect("NUL opens");
let handle = devnull.as_raw_handle() as HANDLE;
assert!(
RawMode::enable_on(handle, handle).is_none(),
"a non-console handle must not be switched to raw mode"
);
}
#[test]
fn a_non_console_handle_has_no_size() {
use std::os::windows::io::AsRawHandle;
let devnull = std::fs::File::open("NUL").expect("NUL opens");
assert_eq!(size_of(devnull.as_raw_handle() as HANDLE), None);
}
#[test]
fn the_window_extent_is_the_visible_rectangle() {
let mut info: CONSOLE_SCREEN_BUFFER_INFO = unsafe { std::mem::zeroed() };
info.dwSize.X = 120;
info.dwSize.Y = 9001; info.srWindow.Left = 0;
info.srWindow.Right = 119;
info.srWindow.Top = 8971;
info.srWindow.Bottom = 9000;
assert_eq!(window_extent(&info), Some((30, 120)));
}
#[test]
fn a_degenerate_window_has_no_size() {
let mut info: CONSOLE_SCREEN_BUFFER_INFO = unsafe { std::mem::zeroed() };
info.srWindow.Right = -2;
assert_eq!(window_extent(&info), None);
}
#[test]
fn resize_is_due_only_on_a_changed_readable_size() {
let mut last = Some((24, 80));
assert_eq!(resize_due(&mut last, Some((24, 80))), None);
assert_eq!(resize_due(&mut last, None), None);
assert_eq!(last, Some((24, 80)));
assert_eq!(resize_due(&mut last, Some((30, 120))), Some((30, 120)));
assert_eq!(last, Some((30, 120)));
assert_eq!(resize_due(&mut last, Some((30, 120))), None);
}
}