blob: 33e8dbaf169c685e68e95fc9f5725c842f30b6f1 [file]
use std::{
fmt,
fs::{self, File},
io::{self, BufWriter, IsTerminal as _, Write as _},
mem,
os::windows::prelude::*,
ptr,
};
use windows_sys::Win32::{
Storage::FileSystem::WriteFile,
System::Console::{
self, FlushConsoleInputBuffer, GetConsoleCP, GetConsoleMode, GetConsoleOutputCP,
GetConsoleScreenBufferInfo, GetNumberOfConsoleInputEvents, ReadConsoleInputA,
ReadConsoleInputW, SetConsoleCP, SetConsoleMode, SetConsoleOutputCP, CONSOLE_MODE,
CONSOLE_SCREEN_BUFFER_INFO, INPUT_RECORD,
},
};
use crate::{
event::source::WindowsEventSource, windows::InputReaderMode, Event, EventReader, OneBased,
WindowSize,
};
use super::Terminal;
macro_rules! bail {
($msg:literal $(,)?) => {
return Err(::std::io::Error::new(::std::io::ErrorKind::Other, $msg))
};
($fmt:expr $(,)?, $($arg:tt)*) => {
return Err(::std::io::Error::new(::std::io::ErrorKind::Other, format!($fmt, $($arg)*)))
};
}
const BUF_SIZE: usize = 128;
type CodePageID = u32;
/// The code page ID for UTF-8 encoding.
/// This is the same as `windows_sys::Win32::Globalization::CP_UTF8`. It is copied here rather
/// than `use`d because it is the only thing we want from the globalization API. Avoiding the
/// `Win32_Globalization` feature for `windows_sys` saves a fair amount of compilation time.
/// And it's unimaginable that Windows would ever change a constant like this given their passion
/// for backwards compatibility.
const CP_UTF8: CodePageID = 65001;
// CREDIT: Like the Unix terminal module this is mainly based on WezTerm code (except for the
// event source parts in `src/event/source/windows.rs` which reaches into these functions).
// <https://github.com/wezterm/wezterm/blob/a87358516004a652ad840bc1661bdf65ffc89b43/filedescriptor/src/windows.rs>
// This crate however uses `windows-sys` instead of `winapi` and has a slightly different API for
// the `InputHandle` and `OutputHandle`.
#[derive(Debug)]
pub enum Handle {
/// A Windows handle owned by Termina.
Owned(OwnedHandle),
/// A borrowed process-global handle such as stdin or stdout.
Borrowed(BorrowedHandle<'static>),
}
impl AsRawHandle for Handle {
fn as_raw_handle(&self) -> RawHandle {
match self {
Self::Owned(handle) => handle.as_raw_handle(),
Self::Borrowed(handle) => handle.as_raw_handle(),
}
}
}
impl Handle {
// NOTE: stdin/stdout satisfy the `'static` lifetime requirements of `BorrowedHandle`.
// > must remain open for the duration of the returned `BorrowedHandle`
// This is true since stdin/stdout have process-global lifetimes.
/// Returns a borrowed handle for process stdin.
pub fn stdin() -> Self {
let stdin = io::stdin().as_raw_handle();
Self::Borrowed(unsafe { BorrowedHandle::borrow_raw(stdin) })
}
/// Returns a borrowed handle for process stdout.
pub fn stdout() -> Self {
let stdout = io::stdout().as_raw_handle();
Self::Borrowed(unsafe { BorrowedHandle::borrow_raw(stdout) })
}
pub fn try_clone(&self) -> io::Result<Self> {
let this = match self {
Self::Owned(handle) => Self::Owned(handle.try_clone()?),
Self::Borrowed(handle) => Self::Borrowed(*handle),
};
Ok(this)
}
}
impl From<File> for Handle {
fn from(file: File) -> Self {
Self::Owned(OwnedHandle::from(file))
}
}
pub(crate) struct InputHandle {
handle: Handle,
input_buf: Vec<INPUT_RECORD>,
mode: InputReaderMode,
}
impl fmt::Debug for InputHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InputHandle")
.field("handle", &self.handle)
.finish_non_exhaustive()
}
}
impl InputHandle {
fn new(handle: Handle, mode: InputReaderMode) -> Self {
let mut input_buf = Vec::with_capacity(BUF_SIZE);
let zeroed: INPUT_RECORD = unsafe { mem::zeroed() };
input_buf.resize(BUF_SIZE, zeroed);
Self {
handle,
mode,
input_buf,
}
}
fn try_clone(&self) -> io::Result<Self> {
Ok(Self {
handle: self.handle.try_clone()?,
input_buf: self.input_buf.clone(),
mode: self.mode,
})
}
fn get_mode(&self) -> io::Result<CONSOLE_MODE> {
let mut mode = 0;
if unsafe { GetConsoleMode(self.as_raw_handle(), &mut mode) } == 0 {
bail!(
"failed to get input console mode: {}",
io::Error::last_os_error()
);
}
Ok(mode)
}
fn set_mode(&mut self, mode: CONSOLE_MODE) -> io::Result<()> {
if unsafe { SetConsoleMode(self.as_raw_handle(), mode) } == 0 {
bail!(
"failed to set input console mode: {}",
io::Error::last_os_error()
);
}
Ok(())
}
fn get_code_page(&self) -> io::Result<CodePageID> {
let cp = unsafe { GetConsoleCP() };
if cp == 0 {
bail!(
"failed to get input console codepage ID: {}",
io::Error::last_os_error()
);
}
Ok(cp)
}
fn set_code_page(&mut self, cp: CodePageID) -> io::Result<()> {
if unsafe { SetConsoleCP(cp) } == 0 {
bail!(
"failed to set input console codepage ID: {}",
io::Error::last_os_error()
);
}
Ok(())
}
pub fn has_pending_input_events(&mut self) -> io::Result<bool> {
let mut num = 0;
// Since we use UTF-8 code pages and call ReadConsoleInputA to read UTF-8 data,
// we can't rely on the result from GetNumberOfConsoleInputEvents.
// Its return value matches the result from ReadConsoleInputW, which may not be the
// same when typing some Unicode values.
// Instead, we can just use it as a quick check to see if events are available.
if unsafe { GetNumberOfConsoleInputEvents(self.as_raw_handle(), &mut num) } == 0 {
bail!(
"failed to read input console number of pending events: {}",
io::Error::last_os_error()
);
}
Ok(num > 0)
}
/// Discard every input record still queued in the console input buffer.
///
/// While raw mode and mouse/window reporting are active the console keeps queuing records.
/// Draining the queue on teardown drops that backlog so it can't leak onto the next shell
/// prompt.
fn flush(&mut self) -> io::Result<()> {
if unsafe { FlushConsoleInputBuffer(self.as_raw_handle()) } == 0 {
bail!(
"failed to flush console input buffer: {}",
io::Error::last_os_error()
);
}
Ok(())
}
pub fn read_console_input(&mut self) -> io::Result<&[INPUT_RECORD]> {
let mut num = 0;
// NOTE: <https://learn.microsoft.com/en-us/windows/console/classic-vs-vt#unicode>
// > UTF-8 support in the console can be utilized via the A variant of Console APIs
// > against console handles after setting the codepage to 65001 or CP_UTF8 with the
// > SetConsoleOutputCP and SetConsoleCP methods, as appropriate.
if unsafe {
match self.mode {
InputReaderMode::Vte => ReadConsoleInputA(
self.as_raw_handle(),
self.input_buf.as_mut_ptr(),
self.input_buf.capacity() as u32,
&mut num,
),
InputReaderMode::Legacy =>
// ReadConsoleInputA seems to have an issue that causes extra characters to be appended to some unicode characters.
// Could be the same issue as this: https://github.com/microsoft/terminal/issues/19436
{
ReadConsoleInputW(
self.as_raw_handle(),
self.input_buf.as_mut_ptr(),
self.input_buf.capacity() as u32,
&mut num,
)
}
}
} == 0
{
bail!(
"failed to read console input events: {}",
io::Error::last_os_error()
);
}
unsafe { self.input_buf.set_len(num as usize) };
Ok(&self.input_buf)
}
}
impl AsRawHandle for InputHandle {
fn as_raw_handle(&self) -> RawHandle {
self.handle.as_raw_handle()
}
}
/// Windows console output handle.
///
/// `OutputHandle` writes bytes with `WriteFile` after `WindowsTerminal` enables virtual-terminal
/// output processing. Panic hooks receive this type so they can write cleanup escape sequences.
#[derive(Debug)]
pub struct OutputHandle {
handle: Handle,
}
impl OutputHandle {
fn new(handle: Handle) -> Self {
Self { handle }
}
fn get_mode(&self) -> io::Result<CONSOLE_MODE> {
let mut mode = 0;
if unsafe { GetConsoleMode(self.as_raw_handle(), &mut mode) } == 0 {
bail!(
"failed to get output console mode: {}",
io::Error::last_os_error()
);
}
Ok(mode)
}
fn set_mode(&mut self, mode: CONSOLE_MODE) -> io::Result<()> {
if unsafe { SetConsoleMode(self.as_raw_handle(), mode) } == 0 {
bail!(
"failed to set output console mode: {}",
io::Error::last_os_error()
);
}
Ok(())
}
fn get_code_page(&self) -> io::Result<CodePageID> {
let cp = unsafe { GetConsoleOutputCP() };
if cp == 0 {
bail!(
"failed to get output console codepage ID: {}",
io::Error::last_os_error()
);
}
Ok(cp)
}
fn set_code_page(&mut self, cp: CodePageID) -> io::Result<()> {
if unsafe { SetConsoleOutputCP(cp) } == 0 {
bail!(
"failed to set output console codepage ID: {}",
io::Error::last_os_error()
);
}
Ok(())
}
fn get_dimensions(&self) -> io::Result<WindowSize> {
let mut info: CONSOLE_SCREEN_BUFFER_INFO = unsafe { mem::zeroed() };
if unsafe { GetConsoleScreenBufferInfo(self.as_raw_handle(), &mut info) } == 0 {
bail!(
"failed to get console screen buffer info: {}",
io::Error::last_os_error()
);
}
let rows = OneBased::from_zero_based((info.srWindow.Bottom - info.srWindow.Top) as u16);
let cols = OneBased::from_zero_based((info.srWindow.Right - info.srWindow.Left) as u16);
Ok(WindowSize {
rows: rows.get(),
cols: cols.get(),
pixel_width: None,
pixel_height: None,
})
}
}
impl AsRawHandle for OutputHandle {
fn as_raw_handle(&self) -> RawHandle {
self.handle.as_raw_handle()
}
}
impl io::Write for OutputHandle {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let mut num_written = 0;
if unsafe {
WriteFile(
self.as_raw_handle(),
buf.as_ptr(),
buf.len() as u32,
&mut num_written,
ptr::null_mut(),
)
} == 0
{
Err(io::Error::last_os_error())
} else {
Ok(num_written as usize)
}
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn open_pty(mode: InputReaderMode) -> io::Result<(InputHandle, OutputHandle)> {
let input = if io::stdin().is_terminal() {
Handle::stdin()
} else {
open_file("CONIN$")?.into()
};
let output = if io::stdout().is_terminal() {
Handle::stdout()
} else {
open_file("CONOUT$")?.into()
};
Ok((InputHandle::new(input, mode), OutputHandle::new(output)))
}
fn open_file(path: &str) -> io::Result<File> {
fs::OpenOptions::new().read(true).write(true).open(path)
}
// CREDIT: Again, like the UnixTerminal in the unix module this is mostly based on WezTerm but
// only covers the parts not related to the event source.
// <https://github.com/wezterm/wezterm/blob/a87358516004a652ad840bc1661bdf65ffc89b43/termwiz/src/terminal/windows.rs#L482-L860>
// Also, the legacy Console API is not implemented.
/// Windows terminal handle.
///
/// `WindowsTerminal` opens `CONIN$` or stdin for input and `CONOUT$` or stdout for output, enables
/// virtual-terminal output processing, and captures console modes/code pages so they can be
/// restored on drop.
#[derive(Debug)]
pub struct WindowsTerminal {
input: InputHandle,
output: BufWriter<OutputHandle>,
reader: EventReader,
original_input_mode: CONSOLE_MODE,
original_output_mode: CONSOLE_MODE,
original_input_cp: CodePageID,
original_output_cp: CodePageID,
has_panic_hook: bool,
discard_input_on_drop: bool,
mode: InputReaderMode,
}
impl WindowsTerminal {
/// Opens the Windows terminal in [VTE input mode][InputReaderMode::Vte].
///
/// This mode enables virtual-terminal input and sets the input/output code pages to UTF-8
/// while the terminal is active.
pub fn new() -> io::Result<Self> {
Self::with_mode_internal(InputReaderMode::Vte)
}
/// Controls whether cleanup discards records still queued for the next input owner.
///
/// Enabled by default. Embedding applications that own input shutdown separately can
/// disable this before starting their reader to preserve unread input when modes restore.
/// Set this before installing a panic hook, which captures the current cleanup policy.
pub fn set_discard_input_on_drop(&mut self, discard: bool) {
self.discard_input_on_drop = discard;
}
/// Opens the Windows terminal using the specified [`InputReaderMode`].
///
/// This is available only with the `windows-legacy` feature because legacy mode needs the
/// classic console-event parser compiled in.
#[cfg(feature = "windows-legacy")]
pub fn with_mode(mode: InputReaderMode) -> io::Result<Self> {
// This is only exposed publicly when `windows-legacy` is enabled
// because creating an instance with `InputReaderMode::Legacy`
// without the appropriate parsing mechanisms enabled will
// result in no events being parsed.
Self::with_mode_internal(mode)
}
fn with_mode_internal(mode: InputReaderMode) -> io::Result<Self> {
let (mut input, mut output) = open_pty(mode)?;
let original_input_mode = input.get_mode()?;
let original_output_mode = output.get_mode()?;
let original_input_cp = input.get_code_page()?;
let original_output_cp = output.get_code_page()?;
// Switch the console to UTF-8 + VT modes. Each step mutates global console state, and a
// later step can fail. Because there is no `WindowsTerminal` yet, `Drop` won't run, so on
// any failure we must roll back to the original values here.
let reader = match (|| -> io::Result<EventReader> {
if mode == InputReaderMode::Vte {
input.set_code_page(CP_UTF8)?;
output.set_code_page(CP_UTF8)?;
}
// Enable VT processing for the output handle.
let desired_output_mode = original_output_mode
| Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING
| Console::DISABLE_NEWLINE_AUTO_RETURN;
output.set_mode(desired_output_mode).map_err(|_| {
io::Error::new(
io::ErrorKind::Other,
"virtual terminal processing could not be enabled for the output handle",
)
})?;
if mode == InputReaderMode::Vte {
// And now the input handle too.
let desired_input_mode =
original_input_mode | Console::ENABLE_VIRTUAL_TERMINAL_INPUT;
input.set_mode(desired_input_mode).map_err(|_| {
io::Error::new(
io::ErrorKind::Other,
"virtual terminal processing could not be enabled for the input handle",
)
})?;
}
Ok(EventReader::new(WindowsEventSource::new(
input.try_clone()?,
mode,
)?))
})() {
Ok(reader) => reader,
Err(err) => {
let _ = input.set_code_page(original_input_cp);
let _ = output.set_code_page(original_output_cp);
let _ = input.set_mode(original_input_mode);
let _ = output.set_mode(original_output_mode);
return Err(err);
}
};
Ok(Self {
input,
output: BufWriter::with_capacity(BUF_SIZE, output),
reader,
original_input_mode,
original_output_mode,
original_input_cp,
original_output_cp,
mode,
has_panic_hook: false,
discard_input_on_drop: true,
})
}
}
impl Terminal for WindowsTerminal {
fn enter_raw_mode(&mut self) -> io::Result<()> {
let mode = self.output.get_mut().get_mode()?;
self.output
.get_mut()
.set_mode(mode | Console::DISABLE_NEWLINE_AUTO_RETURN)
.ok();
let mode = self.input.get_mode()?;
self.input.set_mode(
(mode
& !(Console::ENABLE_ECHO_INPUT
| Console::ENABLE_LINE_INPUT
| Console::ENABLE_PROCESSED_INPUT))
| Console::ENABLE_MOUSE_INPUT
| Console::ENABLE_WINDOW_INPUT,
)?;
Ok(())
}
fn enter_cooked_mode(&mut self) -> io::Result<()> {
let mode = self.output.get_mut().get_mode()?;
self.output
.get_mut()
.set_mode(mode & !Console::DISABLE_NEWLINE_AUTO_RETURN)
.ok();
let mode = self.input.get_mode()?;
self.input.set_mode(
(mode & !(Console::ENABLE_MOUSE_INPUT | Console::ENABLE_WINDOW_INPUT))
| Console::ENABLE_ECHO_INPUT
| Console::ENABLE_LINE_INPUT
| Console::ENABLE_PROCESSED_INPUT,
)?;
Ok(())
}
fn get_dimensions(&self) -> io::Result<WindowSize> {
// NOTE: setting dimensions should be done by VT instead of `SetConsoleScreenBufferInfo`.
// <https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#window-width>
self.output.get_ref().get_dimensions()
}
fn event_reader(&self) -> EventReader {
self.reader.clone()
}
fn poll<F: Fn(&Event) -> bool>(
&self,
filter: F,
timeout: Option<std::time::Duration>,
) -> io::Result<bool> {
self.reader.poll(timeout, filter)
}
fn read<F: Fn(&Event) -> bool>(&self, filter: F) -> io::Result<Event> {
self.reader.read(filter)
}
fn set_panic_hook(&mut self, f: impl Fn(&mut OutputHandle) + Send + Sync + 'static) {
let original_input_cp = self.original_input_cp;
let original_input_mode = self.original_input_mode;
let original_output_cp = self.original_output_cp;
let original_output_mode = self.original_output_mode;
let hook = std::panic::take_hook();
let mode = self.mode;
let discard_input = self.discard_input_on_drop;
std::panic::set_hook(Box::new(move |info| {
if let Ok((mut input, mut output)) = open_pty(mode) {
f(&mut output);
if discard_input {
let _ = input.flush();
}
let _ = input.set_code_page(original_input_cp);
let _ = input.set_mode(original_input_mode);
let _ = output.set_code_page(original_output_cp);
let _ = output.set_mode(original_output_mode);
}
hook(info);
}));
self.has_panic_hook = true;
}
}
impl Drop for WindowsTerminal {
fn drop(&mut self) {
if !self.has_panic_hook || !std::thread::panicking() {
let _ = self.flush();
if self.discard_input_on_drop {
let _ = self.input.flush();
}
let _ = self.input.set_code_page(self.original_input_cp);
let _ = self.output.get_mut().set_code_page(self.original_output_cp);
let _ = self.input.set_mode(self.original_input_mode);
let _ = self.output.get_mut().set_mode(self.original_output_mode);
}
}
}
impl io::Write for WindowsTerminal {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.output.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.output.flush()
}
}