use std::fs::{File, OpenOptions};
use std::io::{Stderr, Write};
use crossterm::tty::IsTty;
pub struct UiStream {
inner: Inner,
}
enum Inner {
Tty(File),
Stderr(Stderr),
}
impl UiStream {
pub fn open() -> Self {
#[cfg(unix)]
const CONSOLE: &str = "/dev/tty";
#[cfg(windows)]
const CONSOLE: &str = "CONOUT$";
let inner = match OpenOptions::new().read(true).write(true).open(CONSOLE) {
Ok(file) => {
#[cfg(windows)]
let _ = enable_vt(&file);
Inner::Tty(file)
}
Err(_) => Inner::Stderr(std::io::stderr()),
};
UiStream { inner }
}
pub fn is_tty(&self) -> bool {
match &self.inner {
Inner::Tty(file) => file.is_tty(),
Inner::Stderr(err) => err.is_tty(),
}
}
pub fn is_dev_tty(&self) -> bool {
matches!(self.inner, Inner::Tty(_))
}
#[allow(dead_code)]
pub fn size(&self) -> (u16, u16) {
crossterm::terminal::size().unwrap_or((80, 24))
}
}
impl Write for UiStream {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match &mut self.inner {
Inner::Tty(file) => {
#[cfg(windows)]
{
if let Some(n) = write_console_utf16(file, buf) {
return Ok(n);
}
}
file.write(buf)
}
Inner::Stderr(err) => err.write(buf),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match &mut self.inner {
Inner::Tty(file) => file.flush(),
Inner::Stderr(err) => err.flush(),
}
}
}
#[cfg(windows)]
fn enable_vt(file: &File) -> bool {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::System::Console::{
CONSOLE_MODE, ENABLE_VIRTUAL_TERMINAL_PROCESSING, GetConsoleMode, SetConsoleMode,
};
let handle = file.as_raw_handle();
let mut mode: CONSOLE_MODE = 0;
unsafe {
if GetConsoleMode(handle, &mut mode) == 0 {
return false;
}
if mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 {
return true;
}
SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0
}
}
#[cfg(windows)]
fn write_console_utf16(file: &File, buf: &[u8]) -> Option<usize> {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::System::Console::WriteConsoleW;
let text = String::from_utf8_lossy(buf);
let wide: Vec<u16> = text.encode_utf16().collect();
let mut offset = 0usize;
while offset < wide.len() {
let mut written: u32 = 0;
let ok = unsafe {
WriteConsoleW(
file.as_raw_handle(),
wide[offset..].as_ptr().cast(),
(wide.len() - offset) as u32,
&mut written,
std::ptr::null(),
)
};
if ok == 0 {
return None;
}
offset += written as usize;
if written == 0 {
return None;
}
}
Some(buf.len())
}
pub struct ConsoleUtf8Guard {
#[cfg(windows)]
saved: Option<(u32, u32)>,
}
impl ConsoleUtf8Guard {
#[cfg(windows)]
pub fn enable() -> Self {
use windows_sys::Win32::System::Console::{
GetConsoleCP, GetConsoleOutputCP, SetConsoleCP, SetConsoleOutputCP,
};
const CP_UTF8: u32 = 65001;
let saved = unsafe {
let input = GetConsoleCP();
let output = GetConsoleOutputCP();
if input == 0 || output == 0 {
None
} else {
SetConsoleCP(CP_UTF8);
SetConsoleOutputCP(CP_UTF8);
Some((input, output))
}
};
ConsoleUtf8Guard { saved }
}
#[cfg(not(windows))]
pub fn enable() -> Self {
ConsoleUtf8Guard {}
}
}
#[cfg(windows)]
impl Drop for ConsoleUtf8Guard {
fn drop(&mut self) {
use windows_sys::Win32::System::Console::{SetConsoleCP, SetConsoleOutputCP};
if let Some((input, output)) = self.saved {
unsafe {
SetConsoleCP(input);
SetConsoleOutputCP(output);
}
}
}
}
pub struct RawModeGuard;
impl RawModeGuard {
pub fn enable() -> std::io::Result<Self> {
crossterm::terminal::enable_raw_mode()?;
Ok(RawModeGuard)
}
}
impl Drop for RawModeGuard {
fn drop(&mut self) {
let _ = crossterm::terminal::disable_raw_mode();
}
}
pub struct AltScreenGuard;
impl AltScreenGuard {
pub fn enter() -> std::io::Result<Self> {
use std::io::Write;
let mut out = std::io::stdout();
out.write_all(b"\x1b[?1049h")?;
out.flush()?;
Ok(AltScreenGuard)
}
}
impl Drop for AltScreenGuard {
fn drop(&mut self) {
use std::io::Write;
let mut out = std::io::stdout();
let _ = out.write_all(b"\x1b[?1049l");
let _ = out.flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(windows)]
#[test]
fn vt_enable_declines_a_non_console_handle() {
let file = tempfile::tempfile().expect("temp file opens");
assert!(!enable_vt(&file));
}
#[test]
fn open_never_panics_and_size_is_positive() {
let stream = UiStream::open();
let (w, h) = stream.size();
assert!(w > 0 && h > 0);
}
#[test]
fn console_utf8_guard_constructs_everywhere() {
let _guard = ConsoleUtf8Guard::enable();
}
#[cfg(windows)]
#[test]
fn console_utf8_guard_sets_and_restores_codepages() {
use windows_sys::Win32::System::Console::{GetConsoleCP, GetConsoleOutputCP};
let before_in = unsafe { GetConsoleCP() };
let before_out = unsafe { GetConsoleOutputCP() };
{
let _guard = ConsoleUtf8Guard::enable();
if before_in != 0 && before_out != 0 {
assert_eq!(unsafe { GetConsoleCP() }, 65001);
assert_eq!(unsafe { GetConsoleOutputCP() }, 65001);
}
}
assert_eq!(unsafe { GetConsoleCP() }, before_in);
assert_eq!(unsafe { GetConsoleOutputCP() }, before_out);
}
#[test]
fn a_regular_file_is_not_a_tty() {
let file = tempfile::tempfile().expect("tempfile");
assert!(!file.is_tty());
}
}