blob: 77ee4ef8705ea8a414e328825332c56ebb869d6f [file] [log] [blame]
use std::cmp;
use std::env;
use std::ffi::OsStr;
use std::fmt::Display;
use std::io;
use std::iter::once;
use std::mem;
use std::os::windows::ffi::OsStrExt;
use std::os::windows::io::AsRawHandle;
use std::slice;
use std::{char, mem::MaybeUninit};
use encode_unicode::error::InvalidUtf16Tuple;
use encode_unicode::CharExt;
use winapi::ctypes::c_void;
use winapi::shared::minwindef::DWORD;
use winapi::shared::minwindef::MAX_PATH;
use winapi::um::consoleapi::{GetConsoleMode, SetConsoleMode};
use winapi::um::consoleapi::{GetNumberOfConsoleInputEvents, ReadConsoleInputW};
use winapi::um::fileapi::FILE_NAME_INFO;
use winapi::um::handleapi::INVALID_HANDLE_VALUE;
use winapi::um::minwinbase::FileNameInfo;
use winapi::um::processenv::GetStdHandle;
use winapi::um::winbase::GetFileInformationByHandleEx;
use winapi::um::winbase::{STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE};
use winapi::um::wincon::{
FillConsoleOutputAttribute, FillConsoleOutputCharacterA, GetConsoleCursorInfo,
GetConsoleScreenBufferInfo, SetConsoleCursorInfo, SetConsoleCursorPosition, SetConsoleTitleW,
CONSOLE_CURSOR_INFO, CONSOLE_SCREEN_BUFFER_INFO, COORD, INPUT_RECORD, KEY_EVENT,
KEY_EVENT_RECORD,
};
use winapi::um::winnt::{CHAR, HANDLE, INT, WCHAR};
#[cfg(feature = "windows-console-colors")]
use winapi_util::console::{Color, Console, Intense};
use crate::common_term;
use crate::kb::Key;
use crate::term::{Term, TermTarget};
#[cfg(feature = "windows-console-colors")]
use once_cell::sync::Lazy;
#[cfg(feature = "windows-console-colors")]
use regex::Regex;
#[cfg(feature = "windows-console-colors")]
static INTENSE_COLOR_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\x1b\[(3|4)8;5;(8|9|1[0-5])m").unwrap());
#[cfg(feature = "windows-console-colors")]
static NORMAL_COLOR_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\x1b\[(3|4)([0-7])m").unwrap());
#[cfg(feature = "windows-console-colors")]
static ATTR_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\x1b\[([1-8])m").unwrap());
const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x4;
pub const DEFAULT_WIDTH: u16 = 79;
pub fn as_handle(term: &Term) -> HANDLE {
// convert between winapi::um::winnt::HANDLE and std::os::windows::raw::HANDLE
// which are both c_void. would be nice to find a better way to do this
term.as_raw_handle() as HANDLE
}
pub fn is_a_terminal(out: &Term) -> bool {
let (fd, others) = match out.target() {
TermTarget::Stdout => (STD_OUTPUT_HANDLE, [STD_INPUT_HANDLE, STD_ERROR_HANDLE]),
TermTarget::Stderr => (STD_ERROR_HANDLE, [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE]),
};
if unsafe { console_on_any(&[fd]) } {
// False positives aren't possible. If we got a console then
// we definitely have a tty on stdin.
return true;
}
// At this point, we *could* have a false negative. We can determine that
// this is true negative if we can detect the presence of a console on
// any of the other streams. If another stream has a console, then we know
// we're in a Windows console and can therefore trust the negative.
if unsafe { console_on_any(&others) } {
return false;
}
msys_tty_on(out)
}
pub fn is_a_color_terminal(out: &Term) -> bool {
if !is_a_terminal(out) {
return false;
}
if msys_tty_on(out) {
return match env::var("TERM") {
Ok(term) => term != "dumb",
Err(_) => true,
};
}
enable_ansi_on(out)
}
fn enable_ansi_on(out: &Term) -> bool {
unsafe {
let handle = as_handle(out);
let mut dw_mode = 0;
if GetConsoleMode(handle, &mut dw_mode) == 0 {
return false;
}
dw_mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
if SetConsoleMode(handle, dw_mode) == 0 {
return false;
}
true
}
}
unsafe fn console_on_any(fds: &[DWORD]) -> bool {
for &fd in fds {
let mut out = 0;
let handle = GetStdHandle(fd);
if GetConsoleMode(handle, &mut out) != 0 {
return true;
}
}
false
}
#[inline]
pub fn terminal_size(out: &Term) -> Option<(u16, u16)> {
terminal_size::terminal_size_using_handle(out.as_raw_handle()).map(|x| ((x.1).0, (x.0).0))
}
pub fn move_cursor_to(out: &Term, x: usize, y: usize) -> io::Result<()> {
if out.is_msys_tty {
return common_term::move_cursor_to(out, x, y);
}
if let Some((hand, _)) = get_console_screen_buffer_info(as_handle(out)) {
unsafe {
SetConsoleCursorPosition(
hand,
COORD {
X: x as i16,
Y: y as i16,
},
);
}
}
Ok(())
}
pub fn move_cursor_up(out: &Term, n: usize) -> io::Result<()> {
if out.is_msys_tty {
return common_term::move_cursor_up(out, n);
}
if let Some((_, csbi)) = get_console_screen_buffer_info(as_handle(out)) {
move_cursor_to(out, 0, csbi.dwCursorPosition.Y as usize - n)?;
}
Ok(())
}
pub fn move_cursor_down(out: &Term, n: usize) -> io::Result<()> {
if out.is_msys_tty {
return common_term::move_cursor_down(out, n);
}
if let Some((_, csbi)) = get_console_screen_buffer_info(as_handle(out)) {
move_cursor_to(out, 0, csbi.dwCursorPosition.Y as usize + n)?;
}
Ok(())
}
pub fn move_cursor_left(out: &Term, n: usize) -> io::Result<()> {
if out.is_msys_tty {
return common_term::move_cursor_left(out, n);
}
if let Some((_, csbi)) = get_console_screen_buffer_info(as_handle(out)) {
move_cursor_to(
out,
csbi.dwCursorPosition.X as usize - n,
csbi.dwCursorPosition.Y as usize,
)?;
}
Ok(())
}
pub fn move_cursor_right(out: &Term, n: usize) -> io::Result<()> {
if out.is_msys_tty {
return common_term::move_cursor_right(out, n);
}
if let Some((_, csbi)) = get_console_screen_buffer_info(as_handle(out)) {
move_cursor_to(
out,
csbi.dwCursorPosition.X as usize + n,
csbi.dwCursorPosition.Y as usize,
)?;
}
Ok(())
}
pub fn clear_line(out: &Term) -> io::Result<()> {
if out.is_msys_tty {
return common_term::clear_line(out);
}
if let Some((hand, csbi)) = get_console_screen_buffer_info(as_handle(out)) {
unsafe {
let width = csbi.srWindow.Right - csbi.srWindow.Left;
let pos = COORD {
X: 0,
Y: csbi.dwCursorPosition.Y,
};
let mut written = 0;
FillConsoleOutputCharacterA(hand, b' ' as CHAR, width as DWORD, pos, &mut written);
FillConsoleOutputAttribute(hand, csbi.wAttributes, width as DWORD, pos, &mut written);
SetConsoleCursorPosition(hand, pos);
}
}
Ok(())
}
pub fn clear_chars(out: &Term, n: usize) -> io::Result<()> {
if out.is_msys_tty {
return common_term::clear_chars(out, n);
}
if let Some((hand, csbi)) = get_console_screen_buffer_info(as_handle(out)) {
unsafe {
let width = cmp::min(csbi.dwCursorPosition.X, n as i16);
let pos = COORD {
X: csbi.dwCursorPosition.X - width,
Y: csbi.dwCursorPosition.Y,
};
let mut written = 0;
FillConsoleOutputCharacterA(hand, b' ' as CHAR, width as DWORD, pos, &mut written);
FillConsoleOutputAttribute(hand, csbi.wAttributes, width as DWORD, pos, &mut written);
SetConsoleCursorPosition(hand, pos);
}
}
Ok(())
}
pub fn clear_screen(out: &Term) -> io::Result<()> {
if out.is_msys_tty {
return common_term::clear_screen(out);
}
if let Some((hand, csbi)) = get_console_screen_buffer_info(as_handle(out)) {
unsafe {
let cells = csbi.dwSize.X as DWORD * csbi.dwSize.Y as DWORD; // as DWORD, or else this causes stack overflows.
let pos = COORD { X: 0, Y: 0 };
let mut written = 0;
FillConsoleOutputCharacterA(hand, b' ' as CHAR, cells, pos, &mut written); // cells as DWORD no longer needed.
FillConsoleOutputAttribute(hand, csbi.wAttributes, cells, pos, &mut written);
SetConsoleCursorPosition(hand, pos);
}
}
Ok(())
}
pub fn clear_to_end_of_screen(out: &Term) -> io::Result<()> {
if out.is_msys_tty {
return common_term::clear_to_end_of_screen(out);
}
if let Some((hand, csbi)) = get_console_screen_buffer_info(as_handle(out)) {
unsafe {
let bottom = csbi.srWindow.Right as DWORD * csbi.srWindow.Bottom as DWORD;
let cells =
bottom - (csbi.dwCursorPosition.X as DWORD * csbi.dwCursorPosition.Y as DWORD); // as DWORD, or else this causes stack overflows.
let pos = COORD {
X: 0,
Y: csbi.dwCursorPosition.Y,
};
let mut written = 0;
FillConsoleOutputCharacterA(hand, b' ' as CHAR, cells, pos, &mut written); // cells as DWORD no longer needed.
FillConsoleOutputAttribute(hand, csbi.wAttributes, cells, pos, &mut written);
SetConsoleCursorPosition(hand, pos);
}
}
Ok(())
}
pub fn show_cursor(out: &Term) -> io::Result<()> {
if out.is_msys_tty {
return common_term::show_cursor(out);
}
if let Some((hand, mut cci)) = get_console_cursor_info(as_handle(out)) {
unsafe {
cci.bVisible = 1;
SetConsoleCursorInfo(hand, &cci);
}
}
Ok(())
}
pub fn hide_cursor(out: &Term) -> io::Result<()> {
if out.is_msys_tty {
return common_term::hide_cursor(out);
}
if let Some((hand, mut cci)) = get_console_cursor_info(as_handle(out)) {
unsafe {
cci.bVisible = 0;
SetConsoleCursorInfo(hand, &cci);
}
}
Ok(())
}
fn get_console_screen_buffer_info(hand: HANDLE) -> Option<(HANDLE, CONSOLE_SCREEN_BUFFER_INFO)> {
let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = unsafe { mem::zeroed() };
match unsafe { GetConsoleScreenBufferInfo(hand, &mut csbi) } {
0 => None,
_ => Some((hand, csbi)),
}
}
fn get_console_cursor_info(hand: HANDLE) -> Option<(HANDLE, CONSOLE_CURSOR_INFO)> {
let mut cci: CONSOLE_CURSOR_INFO = unsafe { mem::zeroed() };
match unsafe { GetConsoleCursorInfo(hand, &mut cci) } {
0 => None,
_ => Some((hand, cci)),
}
}
pub fn key_from_key_code(code: INT) -> Key {
match code {
winapi::um::winuser::VK_LEFT => Key::ArrowLeft,
winapi::um::winuser::VK_RIGHT => Key::ArrowRight,
winapi::um::winuser::VK_UP => Key::ArrowUp,
winapi::um::winuser::VK_DOWN => Key::ArrowDown,
winapi::um::winuser::VK_RETURN => Key::Enter,
winapi::um::winuser::VK_ESCAPE => Key::Escape,
winapi::um::winuser::VK_BACK => Key::Backspace,
winapi::um::winuser::VK_TAB => Key::Tab,
winapi::um::winuser::VK_HOME => Key::Home,
winapi::um::winuser::VK_END => Key::End,
winapi::um::winuser::VK_DELETE => Key::Del,
winapi::um::winuser::VK_SHIFT => Key::Shift,
winapi::um::winuser::VK_MENU => Key::Alt,
_ => Key::Unknown,
}
}
pub fn read_secure() -> io::Result<String> {
let mut rv = String::new();
loop {
match read_single_key()? {
Key::Enter => {
break;
}
Key::Char('\x08') => {
if !rv.is_empty() {
let new_len = rv.len() - 1;
rv.truncate(new_len);
}
}
Key::Char(c) => {
rv.push(c);
}
_ => {}
}
}
Ok(rv)
}
pub fn read_single_key() -> io::Result<Key> {
let key_event = read_key_event()?;
let unicode_char = unsafe { *key_event.uChar.UnicodeChar() };
if unicode_char == 0 {
Ok(key_from_key_code(key_event.wVirtualKeyCode as INT))
} else {
// This is a unicode character, in utf-16. Try to decode it by itself.
match char::from_utf16_tuple((unicode_char, None)) {
Ok(c) => {
// Maintain backward compatibility. The previous implementation (_getwch()) would return
// a special keycode for `Enter`, while ReadConsoleInputW() prefers to use '\r'.
if c == '\r' {
Ok(Key::Enter)
} else if c == '\x08' {
Ok(Key::Backspace)
} else if c == '\x1B' {
Ok(Key::Escape)
} else {
Ok(Key::Char(c))
}
}
// This is part of a surrogate pair. Try to read the second half.
Err(InvalidUtf16Tuple::MissingSecond) => {
// Confirm that there is a next character to read.
if get_key_event_count()? == 0 {
let message = format!(
"Read invlid utf16 {}: {}",
unicode_char,
InvalidUtf16Tuple::MissingSecond
);
return Err(io::Error::new(io::ErrorKind::InvalidData, message));
}
// Read the next character.
let next_event = read_key_event()?;
let next_surrogate = unsafe { *next_event.uChar.UnicodeChar() };
// Attempt to decode it.
match char::from_utf16_tuple((unicode_char, Some(next_surrogate))) {
Ok(c) => Ok(Key::Char(c)),
// Return an InvalidData error. This is the recommended value for UTF-related I/O errors.
// (This error is given when reading a non-UTF8 file into a String, for example.)
Err(e) => {
let message = format!(
"Read invalid surrogate pair ({}, {}): {}",
unicode_char, next_surrogate, e
);
Err(io::Error::new(io::ErrorKind::InvalidData, message))
}
}
}
// Return an InvalidData error. This is the recommended value for UTF-related I/O errors.
// (This error is given when reading a non-UTF8 file into a String, for example.)
Err(e) => {
let message = format!("Read invalid utf16 {}: {}", unicode_char, e);
Err(io::Error::new(io::ErrorKind::InvalidData, message))
}
}
}
}
fn get_stdin_handle() -> io::Result<HANDLE> {
let handle = unsafe { GetStdHandle(STD_INPUT_HANDLE) };
if handle == INVALID_HANDLE_VALUE {
Err(io::Error::last_os_error())
} else {
Ok(handle)
}
}
/// Get the number of pending events in the ReadConsoleInput queue. Note that while
/// these aren't necessarily key events, the only way that multiple events can be
/// put into the queue simultaneously is if a unicode character spanning multiple u16's
/// is read.
///
/// Therefore, this is accurate as long as at least one KEY_EVENT has already been read.
fn get_key_event_count() -> io::Result<DWORD> {
let handle = get_stdin_handle()?;
let mut event_count: DWORD = unsafe { mem::zeroed() };
let success = unsafe { GetNumberOfConsoleInputEvents(handle, &mut event_count) };
if success == 0 {
Err(io::Error::last_os_error())
} else {
Ok(event_count)
}
}
fn read_key_event() -> io::Result<KEY_EVENT_RECORD> {
let handle = get_stdin_handle()?;
let mut buffer: INPUT_RECORD = unsafe { mem::zeroed() };
let mut events_read: DWORD = unsafe { mem::zeroed() };
let mut key_event: KEY_EVENT_RECORD;
loop {
let success = unsafe { ReadConsoleInputW(handle, &mut buffer, 1, &mut events_read) };
if success == 0 {
return Err(io::Error::last_os_error());
}
if events_read == 0 {
return Err(io::Error::new(
io::ErrorKind::Other,
"ReadConsoleInput returned no events, instead of waiting for an event",
));
}
if events_read == 1 && buffer.EventType != KEY_EVENT {
// This isn't a key event; ignore it.
continue;
}
key_event = unsafe { mem::transmute(buffer.Event) };
if key_event.bKeyDown == 0 {
// This is a key being released; ignore it.
continue;
}
return Ok(key_event);
}
}
pub fn wants_emoji() -> bool {
// If WT_SESSION is set, we can assume we're running in the nne
// Windows Terminal. The correct way to detect this is not available
// yet. See https://github.com/microsoft/terminal/issues/1040
env::var("WT_SESSION").is_ok()
}
/// Returns true if there is an MSYS tty on the given handle.
pub fn msys_tty_on(term: &Term) -> bool {
let handle = term.as_raw_handle();
unsafe {
// Check whether the Windows 10 native pty is enabled
{
let mut out = MaybeUninit::uninit();
let res = GetConsoleMode(handle as *mut _, out.as_mut_ptr());
if res != 0 // If res is true then out was initialized.
&& (out.assume_init() & ENABLE_VIRTUAL_TERMINAL_PROCESSING)
== ENABLE_VIRTUAL_TERMINAL_PROCESSING
{
return true;
}
}
let size = mem::size_of::<FILE_NAME_INFO>();
let mut name_info_bytes = vec![0u8; size + MAX_PATH * mem::size_of::<WCHAR>()];
let res = GetFileInformationByHandleEx(
handle as *mut _,
FileNameInfo,
&mut *name_info_bytes as *mut _ as *mut c_void,
name_info_bytes.len() as u32,
);
if res == 0 {
return false;
}
let name_info: &FILE_NAME_INFO = &*(name_info_bytes.as_ptr() as *const FILE_NAME_INFO);
let s = slice::from_raw_parts(
name_info.FileName.as_ptr(),
name_info.FileNameLength as usize / 2,
);
let name = String::from_utf16_lossy(s);
// This checks whether 'pty' exists in the file name, which indicates that
// a pseudo-terminal is attached. To mitigate against false positives
// (e.g., an actual file name that contains 'pty'), we also require that
// either the strings 'msys-' or 'cygwin-' are in the file name as well.)
let is_msys = name.contains("msys-") || name.contains("cygwin-");
let is_pty = name.contains("-pty");
is_msys && is_pty
}
}
pub fn set_title<T: Display>(title: T) {
let buffer: Vec<u16> = OsStr::new(&format!("{}", title))
.encode_wide()
.chain(once(0))
.collect();
unsafe {
SetConsoleTitleW(buffer.as_ptr());
}
}
#[cfg(feature = "windows-console-colors")]
pub fn console_colors(out: &Term, mut con: Console, bytes: &[u8]) -> io::Result<()> {
use crate::ansi::AnsiCodeIterator;
use std::str::from_utf8;
let s = from_utf8(bytes).expect("data to be printed is not an ansi string");
let mut iter = AnsiCodeIterator::new(s);
while !iter.rest_slice().is_empty() {
if let Some((part, is_esc)) = iter.next() {
if !is_esc {
out.write_through_common(part.as_bytes())?;
} else if part == "\x1b[0m" {
con.reset()?;
} else if let Some(cap) = INTENSE_COLOR_RE.captures(part) {
let color = get_color_from_ansi(cap.get(2).unwrap().as_str());
match cap.get(1).unwrap().as_str() {
"3" => con.fg(Intense::Yes, color)?,
"4" => con.bg(Intense::Yes, color)?,
_ => unreachable!(),
};
} else if let Some(cap) = NORMAL_COLOR_RE.captures(part) {
let color = get_color_from_ansi(cap.get(2).unwrap().as_str());
match cap.get(1).unwrap().as_str() {
"3" => con.fg(Intense::No, color)?,
"4" => con.bg(Intense::No, color)?,
_ => unreachable!(),
};
} else if !ATTR_RE.is_match(part) {
out.write_through_common(part.as_bytes())?;
}
}
}
Ok(())
}
#[cfg(feature = "windows-console-colors")]
fn get_color_from_ansi(ansi: &str) -> Color {
match ansi {
"0" | "8" => Color::Black,
"1" | "9" => Color::Red,
"2" | "10" => Color::Green,
"3" | "11" => Color::Yellow,
"4" | "12" => Color::Blue,
"5" | "13" => Color::Magenta,
"6" | "14" => Color::Cyan,
"7" | "15" => Color::White,
_ => unreachable!(),
}
}