use super::options::EnterOptions;
use super::verbs::{self, CursorStyle};
use super::waker::TerminalWaker;
use super::win_logic::{clamp_repeat, ResizeTracker, Utf16Decoder, WakeLatch};
use super::{TermRead, Terminal};
use crate::base::{Error, Result, Size};
use std::sync::Arc;
use std::time::Instant;
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, WAIT_OBJECT_0, WAIT_TIMEOUT};
use windows_sys::Win32::System::Console::{
GetConsoleMode, GetConsoleOutputCP, GetConsoleScreenBufferInfo, GetNumberOfConsoleInputEvents,
ReadConsoleInputW, SetConsoleMode, SetConsoleOutputCP, CONSOLE_MODE,
CONSOLE_SCREEN_BUFFER_INFO, DISABLE_NEWLINE_AUTO_RETURN, ENABLE_ECHO_INPUT,
ENABLE_EXTENDED_FLAGS, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT, ENABLE_PROCESSED_OUTPUT,
ENABLE_QUICK_EDIT_MODE, ENABLE_VIRTUAL_TERMINAL_INPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING,
ENABLE_WINDOW_INPUT, INPUT_RECORD, KEY_EVENT, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
WINDOW_BUFFER_SIZE_EVENT,
};
use windows_sys::Win32::System::Threading::{
CreateEventW, SetEvent, WaitForMultipleObjects, WaitForSingleObject, INFINITE,
};
const UTF8_CODEPAGE: u32 = 65001;
const RECORD_BATCH: usize = 64;
const REPEAT_CAP: u16 = 1024;
#[path = "windows_sys.rs"]
mod sys;
pub use sys::emergency_restore;
pub use sys::have_tty;
use sys::{
open_console, win_err, write_all_handle, EmergencySlot, SavedModes, WakeEvent, EMERGENCY,
};
pub struct WindowsTerminal {
hin: HANDLE,
hout: HANDLE,
owns_in: bool,
owns_out: bool,
saved: Option<SavedModes>,
out: Vec<u8>,
in_buf: Vec<u8>,
utf16: Utf16Decoder,
pending_resize: Option<Size>,
resize: ResizeTracker,
wake_event: Option<Arc<WakeEvent>>,
waker: Option<TerminalWaker>,
wake: WakeLatch,
cursor_styled: bool,
title_pushed: bool,
pixel_moused: bool,
}
impl WindowsTerminal {
pub fn new() -> Result<Self> {
let (hin, owns_in) = open_console("CONIN$", STD_INPUT_HANDLE)?;
let (hout, owns_out) = match open_console("CONOUT$", STD_OUTPUT_HANDLE) {
Ok(v) => v,
Err(e) => {
if owns_in {
unsafe { CloseHandle(hin) };
}
return Err(e);
}
};
let (wake_event, waker) = Self::make_wake_event();
Ok(WindowsTerminal {
hin,
hout,
owns_in,
owns_out,
saved: None,
out: Vec::with_capacity(8192),
in_buf: Vec::with_capacity(4096),
utf16: Utf16Decoder::default(),
pending_resize: None,
resize: ResizeTracker::default(),
wake_event,
waker,
wake: WakeLatch::default(),
cursor_styled: false,
title_pushed: false,
pixel_moused: false,
})
}
fn append_emergency_leave(extra: &[u8]) {
if let Ok(mut g) = EMERGENCY.lock() {
if let Some(s) = g.as_mut() {
s.leave_bytes.extend_from_slice(extra);
}
}
}
fn prepend_emergency_leave(extra: &[u8]) {
if let Ok(mut g) = EMERGENCY.lock() {
if let Some(s) = g.as_mut() {
s.leave_bytes.splice(0..0, extra.iter().copied());
}
}
}
fn make_wake_event() -> (Option<Arc<WakeEvent>>, Option<TerminalWaker>) {
let h = unsafe { CreateEventW(std::ptr::null(), 0, 0, std::ptr::null()) };
if h.is_null() {
return (None, None);
}
let ev = Arc::new(WakeEvent(h));
let for_waker = ev.clone();
let waker = TerminalWaker::new(move || {
unsafe { SetEvent(for_waker.0) };
});
(Some(ev), Some(waker))
}
fn check_resize_query(&mut self) -> Option<Size> {
let fresh = self.query_size().ok()?;
self.resize.observe(fresh)
}
fn query_size(&self) -> Result<Size> {
let mut info: CONSOLE_SCREEN_BUFFER_INFO = unsafe { std::mem::zeroed() };
if unsafe { GetConsoleScreenBufferInfo(self.hout, &mut info) } == 0 {
return Err(win_err("GetConsoleScreenBufferInfo"));
}
let w = i32::from(info.srWindow.Right) - i32::from(info.srWindow.Left) + 1;
let h = i32::from(info.srWindow.Bottom) - i32::from(info.srWindow.Top) + 1;
Ok(Size::new(w.max(0), h.max(0)))
}
fn drain_records(&mut self) -> Result<()> {
loop {
let mut queued: u32 = 0;
if unsafe { GetNumberOfConsoleInputEvents(self.hin, &mut queued) } == 0 {
return Err(win_err("GetNumberOfConsoleInputEvents"));
}
if queued == 0 {
return Ok(());
}
let mut records: [INPUT_RECORD; RECORD_BATCH] =
unsafe { std::mem::zeroed() };
let mut read: u32 = 0;
let want = (queued as usize).min(RECORD_BATCH) as u32;
if unsafe { ReadConsoleInputW(self.hin, records.as_mut_ptr(), want, &mut read) } == 0 {
return Err(win_err("ReadConsoleInputW"));
}
for rec in records.iter().take(read as usize) {
match u32::from(rec.EventType) {
KEY_EVENT => {
let key = unsafe { rec.Event.KeyEvent };
if key.bKeyDown == 0 {
continue; }
let unit = unsafe { key.uChar.UnicodeChar };
if unit == 0 {
continue; }
let repeat = clamp_repeat(key.wRepeatCount, REPEAT_CAP);
for _ in 0..repeat {
self.utf16.push(unit, &mut self.in_buf);
}
}
WINDOW_BUFFER_SIZE_EVENT => {
if let Ok(fresh) = self.query_size() {
if let Some(sz) = self.resize.observe(fresh) {
self.pending_resize = Some(sz);
}
}
}
_ => {} }
}
}
}
}
impl Terminal for WindowsTerminal {
fn enter(&mut self, opts: &EnterOptions) -> Result<()> {
if self.saved.is_some() {
return Ok(());
}
let mut in_mode: CONSOLE_MODE = 0;
let mut out_mode: CONSOLE_MODE = 0;
unsafe {
if GetConsoleMode(self.hin, &mut in_mode) == 0 {
return Err(win_err("GetConsoleMode(in)"));
}
if GetConsoleMode(self.hout, &mut out_mode) == 0 {
return Err(win_err("GetConsoleMode(out)"));
}
}
let want_full = out_mode
| ENABLE_PROCESSED_OUTPUT
| ENABLE_VIRTUAL_TERMINAL_PROCESSING
| DISABLE_NEWLINE_AUTO_RETURN;
let ok = unsafe { SetConsoleMode(self.hout, want_full) } != 0
|| unsafe {
SetConsoleMode(
self.hout,
out_mode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING,
)
} != 0;
if !ok {
return Err(Error::Unsupported(
"console cannot enable VT output (Windows 10 1607+ required)".into(),
));
}
let raw_in =
(in_mode | ENABLE_VIRTUAL_TERMINAL_INPUT | ENABLE_WINDOW_INPUT | ENABLE_EXTENDED_FLAGS)
& !(ENABLE_LINE_INPUT
| ENABLE_ECHO_INPUT
| ENABLE_PROCESSED_INPUT
| ENABLE_QUICK_EDIT_MODE);
if unsafe { SetConsoleMode(self.hin, raw_in) } == 0 {
unsafe { SetConsoleMode(self.hout, out_mode) };
return Err(win_err("SetConsoleMode(in, VT)"));
}
let out_cp = unsafe { GetConsoleOutputCP() };
unsafe { SetConsoleOutputCP(UTF8_CODEPAGE) };
self.resize.reset(self.query_size().unwrap_or(Size::ZERO));
if let Ok(mut g) = EMERGENCY.lock() {
*g = Some(EmergencySlot {
hin: self.hin as isize,
hout: self.hout as isize,
in_mode,
out_mode,
out_cp,
leave_bytes: opts.leave_bytes(),
});
}
self.saved = Some(SavedModes {
in_mode,
out_mode,
out_cp,
opts: *opts,
});
self.write(&opts.enter_bytes())?;
self.flush()?;
Ok(())
}
fn leave(&mut self) -> Result<()> {
let saved = self.saved.take();
if saved.is_none() && !self.cursor_styled && !self.title_pushed && !self.pixel_moused {
return Ok(());
}
let mut first_err: Option<Error> = None;
if let Some(s) = &saved {
self.out.extend_from_slice(&s.opts.leave_bytes());
}
if self.pixel_moused {
self.out.extend_from_slice(verbs::PIXEL_MOUSE_OFF);
self.pixel_moused = false;
}
if self.cursor_styled {
self.out.extend_from_slice(verbs::CURSOR_STYLE_RESET);
self.cursor_styled = false;
}
if self.title_pushed {
self.out.extend_from_slice(verbs::TITLE_POP);
self.title_pushed = false;
}
if let Err(e) = self.flush() {
first_err.get_or_insert(e);
}
let Some(saved) = saved else {
return match first_err {
Some(e) => Err(e),
None => Ok(()),
};
};
unsafe {
if SetConsoleMode(self.hin, saved.in_mode) == 0 {
first_err.get_or_insert(win_err("SetConsoleMode(in, restore)"));
}
if SetConsoleMode(self.hout, saved.out_mode) == 0 {
first_err.get_or_insert(win_err("SetConsoleMode(out, restore)"));
}
SetConsoleOutputCP(saved.out_cp);
}
if let Ok(mut g) = EMERGENCY.lock() {
*g = None;
}
match first_err {
Some(e) => Err(e),
None => Ok(()),
}
}
fn size(&mut self) -> Result<Size> {
self.query_size()
}
fn read(&mut self, deadline: Option<Instant>) -> Result<TermRead<'_>> {
if let Some(sz) = self.pending_resize.take() {
return Ok(TermRead::Resize(sz));
}
loop {
self.in_buf.clear();
self.drain_records()?;
if !self.in_buf.is_empty() {
return Ok(TermRead::Input(&self.in_buf));
}
if let Some(sz) = self.pending_resize.take() {
return Ok(TermRead::Resize(sz));
}
if let Some(sz) = self.check_resize_query() {
return Ok(TermRead::Resize(sz));
}
if self.wake.take() {
return Ok(TermRead::Wake);
}
let wait_ms = match deadline {
None => INFINITE,
Some(d) => {
let rem = d.saturating_duration_since(Instant::now());
if rem.is_zero() {
return Ok(TermRead::Idle);
}
rem.as_millis().min(u128::from(u32::MAX - 1)) as u32
}
};
let rc = match &self.wake_event {
Some(ev) => {
let handles = [self.hin, ev.0];
unsafe { WaitForMultipleObjects(2, handles.as_ptr(), 0, wait_ms) }
}
None => unsafe { WaitForSingleObject(self.hin, wait_ms) },
};
const WAIT_WAKE: u32 = WAIT_OBJECT_0 + 1;
match rc {
WAIT_OBJECT_0 => continue, WAIT_WAKE if self.wake_event.is_some() => {
self.wake.arm();
continue;
}
WAIT_TIMEOUT => continue, _ => return Err(win_err("WaitForConsoleInput")),
}
}
}
fn write(&mut self, bytes: &[u8]) -> Result<()> {
self.out.extend_from_slice(bytes);
if self.out.len() >= 1 << 16 {
self.flush()?;
}
Ok(())
}
fn flush(&mut self) -> Result<()> {
if self.out.is_empty() {
return Ok(());
}
let res = write_all_handle(self.hout, &self.out);
self.out.clear();
res
}
fn waker(&self) -> Option<TerminalWaker> {
self.waker.clone()
}
fn is_tty(&self) -> bool {
let mut mode: CONSOLE_MODE = 0;
unsafe { GetConsoleMode(self.hout, &mut mode) != 0 }
}
fn set_cursor_style(&mut self, style: CursorStyle) -> Result<()> {
self.write(&verbs::cursor_style_bytes(style))?;
if style != CursorStyle::Default && !self.cursor_styled {
self.cursor_styled = true;
Self::append_emergency_leave(verbs::CURSOR_STYLE_RESET);
}
Ok(())
}
fn set_title(&mut self, title: &str) -> Result<()> {
if !self.title_pushed {
self.title_pushed = true;
self.write(verbs::TITLE_PUSH)?;
Self::append_emergency_leave(verbs::TITLE_POP);
}
self.write(&verbs::set_title_bytes(title))
}
fn set_pixel_mouse(&mut self, on: bool) -> Result<()> {
self.write(if on {
verbs::PIXEL_MOUSE_ON
} else {
verbs::PIXEL_MOUSE_OFF
})?;
if on && !self.pixel_moused {
Self::append_emergency_leave(verbs::PIXEL_MOUSE_OFF);
}
self.pixel_moused = on;
Ok(())
}
fn set_mouse_reporting(&mut self, on: bool) -> Result<()> {
let Some(saved) = &self.saved else {
return Err(Error::Term(
"set_mouse_reporting outside a session — enter() first".into(),
));
};
let mode = saved.opts.mouse;
self.write(if on {
mode.arm_bytes()
} else {
mode.disarm_bytes()
})
}
fn set_kitty_keyboard(&mut self, flags: super::options::KittyFlags) -> Result<()> {
let Some(saved) = &mut self.saved else {
return Err(Error::Term(
"set_kitty_keyboard outside a session — enter() first".into(),
));
};
let prev = saved.opts.kitty_keyboard;
if prev == flags {
return Ok(());
}
saved.opts.kitty_keyboard = flags;
let mut bytes = Vec::with_capacity(16);
if !prev.is_empty() {
bytes.extend_from_slice(super::options::KittyFlags::POP_BYTES);
}
if !flags.is_empty() {
bytes.extend_from_slice(&flags.push_bytes());
}
if prev.is_empty() && !flags.is_empty() {
Self::prepend_emergency_leave(super::options::KittyFlags::POP_BYTES);
}
self.write(&bytes)
}
}
impl Drop for WindowsTerminal {
fn drop(&mut self) {
let _ = self.leave();
unsafe {
if self.owns_in {
CloseHandle(self.hin);
}
if self.owns_out {
CloseHandle(self.hout);
}
}
}
}