fix: refresh diff colors on terminal theme changes
This commit is contained in:
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
//! This module provides platform related functions.
|
||||
|
||||
#[cfg(unix)]
|
||||
#[cfg(feature = "events")]
|
||||
pub use self::unix::supports_keyboard_enhancement;
|
||||
#[cfg(unix)]
|
||||
pub(crate) use self::unix::{
|
||||
disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, size, window_size,
|
||||
};
|
||||
#[cfg(windows)]
|
||||
#[cfg(feature = "events")]
|
||||
pub use self::windows::supports_keyboard_enhancement;
|
||||
#[cfg(all(windows, test))]
|
||||
pub(crate) use self::windows::temp_screen_buffer;
|
||||
#[cfg(windows)]
|
||||
pub(crate) use self::windows::{
|
||||
clear, disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, scroll_down, scroll_up,
|
||||
set_size, set_window_title, size, window_size,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod file_descriptor;
|
||||
#[cfg(unix)]
|
||||
mod unix;
|
||||
@@ -0,0 +1,154 @@
|
||||
use std::io;
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
use libc::size_t;
|
||||
#[cfg(not(feature = "libc"))]
|
||||
use rustix::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd};
|
||||
#[cfg(feature = "libc")]
|
||||
use std::{
|
||||
fs,
|
||||
marker::PhantomData,
|
||||
os::unix::{
|
||||
io::{IntoRawFd, RawFd},
|
||||
prelude::AsRawFd,
|
||||
},
|
||||
};
|
||||
|
||||
/// A file descriptor wrapper.
|
||||
///
|
||||
/// It allows to retrieve raw file descriptor, write to the file descriptor and
|
||||
/// mainly it closes the file descriptor once dropped.
|
||||
#[derive(Debug)]
|
||||
#[cfg(feature = "libc")]
|
||||
pub struct FileDesc<'a> {
|
||||
fd: RawFd,
|
||||
close_on_drop: bool,
|
||||
phantom: PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
pub enum FileDesc<'a> {
|
||||
Owned(OwnedFd),
|
||||
Borrowed(BorrowedFd<'a>),
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
impl FileDesc<'_> {
|
||||
/// Constructs a new `FileDesc` with the given `RawFd`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fd` - raw file descriptor
|
||||
/// * `close_on_drop` - specify if the raw file descriptor should be closed once the `FileDesc` is dropped
|
||||
pub fn new(fd: RawFd, close_on_drop: bool) -> FileDesc<'static> {
|
||||
FileDesc {
|
||||
fd,
|
||||
close_on_drop,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self, buffer: &mut [u8]) -> io::Result<usize> {
|
||||
let result = unsafe {
|
||||
libc::read(
|
||||
self.fd,
|
||||
buffer.as_mut_ptr() as *mut libc::c_void,
|
||||
buffer.len() as size_t,
|
||||
)
|
||||
};
|
||||
|
||||
if result < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(result as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the underlying file descriptor.
|
||||
pub fn raw_fd(&self) -> RawFd {
|
||||
self.fd
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
impl FileDesc<'_> {
|
||||
pub fn read(&self, buffer: &mut [u8]) -> io::Result<usize> {
|
||||
let fd = match self {
|
||||
FileDesc::Owned(fd) => fd.as_fd(),
|
||||
FileDesc::Borrowed(fd) => fd.as_fd(),
|
||||
};
|
||||
let result = rustix::io::read(fd, buffer)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn raw_fd(&self) -> RawFd {
|
||||
match self {
|
||||
FileDesc::Owned(fd) => fd.as_raw_fd(),
|
||||
FileDesc::Borrowed(fd) => fd.as_raw_fd(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
impl Drop for FileDesc<'_> {
|
||||
fn drop(&mut self) {
|
||||
if self.close_on_drop {
|
||||
// Note that errors are ignored when closing a file descriptor. The
|
||||
// reason for this is that if an error occurs we don't actually know if
|
||||
// the file descriptor was closed or not, and if we retried (for
|
||||
// something like EINTR), we might close another valid file descriptor
|
||||
// opened after we closed ours.
|
||||
let _ = unsafe { libc::close(self.fd) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRawFd for FileDesc<'_> {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.raw_fd()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
impl AsFd for FileDesc<'_> {
|
||||
fn as_fd(&self) -> BorrowedFd<'_> {
|
||||
match self {
|
||||
FileDesc::Owned(fd) => fd.as_fd(),
|
||||
FileDesc::Borrowed(fd) => fd.as_fd(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
/// Creates a file descriptor pointing to the standard input or `/dev/tty`.
|
||||
pub fn tty_fd() -> io::Result<FileDesc<'static>> {
|
||||
let (fd, close_on_drop) = if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } {
|
||||
(libc::STDIN_FILENO, false)
|
||||
} else {
|
||||
(
|
||||
fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("/dev/tty")?
|
||||
.into_raw_fd(),
|
||||
true,
|
||||
)
|
||||
};
|
||||
|
||||
Ok(FileDesc::new(fd, close_on_drop))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
/// Creates a file descriptor pointing to the standard input or `/dev/tty`.
|
||||
pub fn tty_fd() -> io::Result<FileDesc<'static>> {
|
||||
use std::fs::File;
|
||||
|
||||
let stdin = rustix::stdio::stdin();
|
||||
let fd = if rustix::termios::isatty(stdin) {
|
||||
FileDesc::Borrowed(stdin)
|
||||
} else {
|
||||
let dev_tty = File::options().read(true).write(true).open("/dev/tty")?;
|
||||
FileDesc::Owned(dev_tty.into())
|
||||
};
|
||||
Ok(fd)
|
||||
}
|
||||
+326
@@ -0,0 +1,326 @@
|
||||
//! UNIX related logic for terminal manipulation.
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
use crate::event::KeyboardEnhancementFlags;
|
||||
use crate::terminal::{
|
||||
sys::file_descriptor::{tty_fd, FileDesc},
|
||||
WindowSize,
|
||||
};
|
||||
#[cfg(feature = "libc")]
|
||||
use libc::{
|
||||
cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, STDOUT_FILENO, TCSANOW,
|
||||
TIOCGWINSZ,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
#[cfg(not(feature = "libc"))]
|
||||
use rustix::{
|
||||
fd::AsFd,
|
||||
termios::{Termios, Winsize},
|
||||
};
|
||||
|
||||
use std::{fs::File, io, process};
|
||||
#[cfg(feature = "libc")]
|
||||
use std::{
|
||||
mem,
|
||||
os::unix::io::{IntoRawFd, RawFd},
|
||||
};
|
||||
|
||||
// Some(Termios) -> we're in the raw mode and this is the previous mode
|
||||
// None -> we're not in the raw mode
|
||||
static TERMINAL_MODE_PRIOR_RAW_MODE: Mutex<Option<Termios>> = parking_lot::const_mutex(None);
|
||||
|
||||
pub(crate) fn is_raw_mode_enabled() -> bool {
|
||||
TERMINAL_MODE_PRIOR_RAW_MODE.lock().is_some()
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
impl From<winsize> for WindowSize {
|
||||
fn from(size: winsize) -> WindowSize {
|
||||
WindowSize {
|
||||
columns: size.ws_col,
|
||||
rows: size.ws_row,
|
||||
width: size.ws_xpixel,
|
||||
height: size.ws_ypixel,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "libc"))]
|
||||
impl From<Winsize> for WindowSize {
|
||||
fn from(size: Winsize) -> WindowSize {
|
||||
WindowSize {
|
||||
columns: size.ws_col,
|
||||
rows: size.ws_row,
|
||||
width: size.ws_xpixel,
|
||||
height: size.ws_ypixel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
#[cfg(feature = "libc")]
|
||||
pub(crate) fn window_size() -> io::Result<WindowSize> {
|
||||
// http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc
|
||||
let mut size = winsize {
|
||||
ws_row: 0,
|
||||
ws_col: 0,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
|
||||
let file = File::open("/dev/tty").map(|file| (FileDesc::new(file.into_raw_fd(), true)));
|
||||
let fd = if let Ok(file) = &file {
|
||||
file.raw_fd()
|
||||
} else {
|
||||
// Fallback to libc::STDOUT_FILENO if /dev/tty is missing
|
||||
STDOUT_FILENO
|
||||
};
|
||||
|
||||
if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() {
|
||||
return Ok(size.into());
|
||||
}
|
||||
|
||||
Err(std::io::Error::last_os_error().into())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
pub(crate) fn window_size() -> io::Result<WindowSize> {
|
||||
let file = File::open("/dev/tty").map(|file| FileDesc::Owned(file.into()));
|
||||
let fd = if let Ok(file) = &file {
|
||||
file.as_fd()
|
||||
} else {
|
||||
// Fallback to libc::STDOUT_FILENO if /dev/tty is missing
|
||||
rustix::stdio::stdout()
|
||||
};
|
||||
let size = rustix::termios::tcgetwinsize(fd)?;
|
||||
Ok(size.into())
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
pub(crate) fn size() -> io::Result<(u16, u16)> {
|
||||
if let Ok(window_size) = window_size() {
|
||||
return Ok((window_size.columns, window_size.rows));
|
||||
}
|
||||
|
||||
tput_size().ok_or_else(|| std::io::Error::last_os_error().into())
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
pub(crate) fn enable_raw_mode() -> io::Result<()> {
|
||||
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||
if original_mode.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let tty = tty_fd()?;
|
||||
let fd = tty.raw_fd();
|
||||
let mut ios = get_terminal_attr(fd)?;
|
||||
let original_mode_ios = ios;
|
||||
raw_terminal_attr(&mut ios);
|
||||
set_terminal_attr(fd, &ios)?;
|
||||
// Keep it last - set the original mode only if we were able to switch to the raw mode
|
||||
*original_mode = Some(original_mode_ios);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
pub(crate) fn enable_raw_mode() -> io::Result<()> {
|
||||
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||
if original_mode.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let tty = tty_fd()?;
|
||||
let mut ios = get_terminal_attr(&tty)?;
|
||||
let original_mode_ios = ios.clone();
|
||||
ios.make_raw();
|
||||
set_terminal_attr(&tty, &ios)?;
|
||||
// Keep it last - set the original mode only if we were able to switch to the raw mode
|
||||
*original_mode = Some(original_mode_ios);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reset the raw mode.
|
||||
///
|
||||
/// More precisely, reset the whole termios mode to what it was before the first call
|
||||
/// to [enable_raw_mode]. If you don't mess with termios outside of crossterm, it's
|
||||
/// effectively disabling the raw mode and doing nothing else.
|
||||
#[cfg(feature = "libc")]
|
||||
pub(crate) fn disable_raw_mode() -> io::Result<()> {
|
||||
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||
if let Some(original_mode_ios) = original_mode.as_ref() {
|
||||
let tty = tty_fd()?;
|
||||
set_terminal_attr(tty.raw_fd(), original_mode_ios)?;
|
||||
// Keep it last - remove the original mode only if we were able to switch back
|
||||
*original_mode = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
pub(crate) fn disable_raw_mode() -> io::Result<()> {
|
||||
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||
if let Some(original_mode_ios) = original_mode.as_ref() {
|
||||
let tty = tty_fd()?;
|
||||
set_terminal_attr(&tty, original_mode_ios)?;
|
||||
// Keep it last - remove the original mode only if we were able to switch back
|
||||
*original_mode = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
fn get_terminal_attr(fd: impl AsFd) -> io::Result<Termios> {
|
||||
let result = rustix::termios::tcgetattr(fd)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
fn set_terminal_attr(fd: impl AsFd, termios: &Termios) -> io::Result<()> {
|
||||
rustix::termios::tcsetattr(fd, rustix::termios::OptionalActions::Now, termios)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Queries the terminal's support for progressive keyboard enhancement.
|
||||
///
|
||||
/// On unix systems, this function will block and possibly time out while
|
||||
/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
|
||||
#[cfg(feature = "events")]
|
||||
pub fn supports_keyboard_enhancement() -> io::Result<bool> {
|
||||
query_keyboard_enhancement_flags().map(|flags| flags.is_some())
|
||||
}
|
||||
|
||||
/// Queries the terminal's currently active keyboard enhancement flags.
|
||||
///
|
||||
/// On unix systems, this function will block and possibly time out while
|
||||
/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
|
||||
#[cfg(feature = "events")]
|
||||
pub fn query_keyboard_enhancement_flags() -> io::Result<Option<KeyboardEnhancementFlags>> {
|
||||
if is_raw_mode_enabled() {
|
||||
query_keyboard_enhancement_flags_raw()
|
||||
} else {
|
||||
query_keyboard_enhancement_flags_nonraw()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
fn query_keyboard_enhancement_flags_nonraw() -> io::Result<Option<KeyboardEnhancementFlags>> {
|
||||
enable_raw_mode()?;
|
||||
let flags = query_keyboard_enhancement_flags_raw();
|
||||
disable_raw_mode()?;
|
||||
flags
|
||||
}
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
fn query_keyboard_enhancement_flags_raw() -> io::Result<Option<KeyboardEnhancementFlags>> {
|
||||
use crate::event::{
|
||||
filter::{KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter},
|
||||
poll_internal, read_internal, InternalEvent,
|
||||
};
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
// This is the recommended method for testing support for the keyboard enhancement protocol.
|
||||
// We send a query for the flags supported by the terminal and then the primary device attributes
|
||||
// query. If we receive the primary device attributes response but not the keyboard enhancement
|
||||
// flags, none of the flags are supported.
|
||||
//
|
||||
// See <https://sw.kovidgoyal.net/kitty/keyboard-protocol/#detection-of-support-for-this-protocol>
|
||||
|
||||
// ESC [ ? u Query progressive keyboard enhancement flags (kitty protocol).
|
||||
// ESC [ c Query primary device attributes.
|
||||
const QUERY: &[u8] = b"\x1B[?u\x1B[c";
|
||||
|
||||
let result = File::open("/dev/tty").and_then(|mut file| {
|
||||
file.write_all(QUERY)?;
|
||||
file.flush()
|
||||
});
|
||||
if result.is_err() {
|
||||
let mut stdout = io::stdout();
|
||||
stdout.write_all(QUERY)?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
|
||||
loop {
|
||||
match poll_internal(
|
||||
Some(Duration::from_millis(2000)),
|
||||
&KeyboardEnhancementFlagsFilter,
|
||||
) {
|
||||
Ok(true) => {
|
||||
match read_internal(&KeyboardEnhancementFlagsFilter) {
|
||||
Ok(InternalEvent::KeyboardEnhancementFlags(current_flags)) => {
|
||||
// Flush the PrimaryDeviceAttributes out of the event queue.
|
||||
read_internal(&PrimaryDeviceAttributesFilter).ok();
|
||||
return Ok(Some(current_flags));
|
||||
}
|
||||
_ => return Ok(None),
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"The keyboard enhancement status could not be read within a normal duration",
|
||||
));
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// execute tput with the given argument and parse
|
||||
/// the output as a u16.
|
||||
///
|
||||
/// The arg should be "cols" or "lines"
|
||||
fn tput_value(arg: &str) -> Option<u16> {
|
||||
let output = process::Command::new("tput").arg(arg).output().ok()?;
|
||||
let value = output
|
||||
.stdout
|
||||
.into_iter()
|
||||
.filter_map(|b| char::from(b).to_digit(10))
|
||||
.fold(0, |v, n| v * 10 + n as u16);
|
||||
|
||||
if value > 0 {
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the size of the screen as determined by tput.
|
||||
///
|
||||
/// This alternate way of computing the size is useful
|
||||
/// when in a subshell.
|
||||
fn tput_size() -> Option<(u16, u16)> {
|
||||
match (tput_value("cols"), tput_value("lines")) {
|
||||
(Some(w), Some(h)) => Some((w, h)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
// Transform the given mode into an raw mode (non-canonical) mode.
|
||||
fn raw_terminal_attr(termios: &mut Termios) {
|
||||
unsafe { cfmakeraw(termios) }
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
fn get_terminal_attr(fd: RawFd) -> io::Result<Termios> {
|
||||
unsafe {
|
||||
let mut termios = mem::zeroed();
|
||||
wrap_with_result(tcgetattr(fd, &mut termios))?;
|
||||
Ok(termios)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
fn set_terminal_attr(fd: RawFd, termios: &Termios) -> io::Result<()> {
|
||||
wrap_with_result(unsafe { tcsetattr(fd, TCSANOW, termios) })
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
fn wrap_with_result(result: i32) -> io::Result<()> {
|
||||
if result == -1 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+471
@@ -0,0 +1,471 @@
|
||||
//! WinAPI related logic for terminal manipulation.
|
||||
|
||||
use std::fmt::{self, Write};
|
||||
use std::io::{self};
|
||||
|
||||
use crossterm_winapi::{Console, ConsoleMode, Coord, Handle, ScreenBuffer, Size};
|
||||
use winapi::{
|
||||
shared::minwindef::DWORD,
|
||||
um::wincon::{SetConsoleTitleW, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cursor,
|
||||
terminal::{ClearType, WindowSize},
|
||||
};
|
||||
|
||||
/// bits which can't be set in raw mode
|
||||
const NOT_RAW_MODE_MASK: DWORD = ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT;
|
||||
|
||||
pub(crate) fn is_raw_mode_enabled() -> std::io::Result<bool> {
|
||||
let console_mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||
|
||||
let dw_mode = console_mode.mode()?;
|
||||
|
||||
Ok(
|
||||
// check none of the "not raw" bits is set
|
||||
dw_mode & NOT_RAW_MODE_MASK == 0,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn enable_raw_mode() -> std::io::Result<()> {
|
||||
let console_mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||
|
||||
let dw_mode = console_mode.mode()?;
|
||||
|
||||
let new_mode = dw_mode & !NOT_RAW_MODE_MASK;
|
||||
|
||||
console_mode.set_mode(new_mode)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn disable_raw_mode() -> std::io::Result<()> {
|
||||
let console_mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||
|
||||
let dw_mode = console_mode.mode()?;
|
||||
|
||||
let new_mode = dw_mode | NOT_RAW_MODE_MASK;
|
||||
|
||||
console_mode.set_mode(new_mode)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn size() -> io::Result<(u16, u16)> {
|
||||
let terminal_size = ScreenBuffer::current()?.info()?.terminal_size();
|
||||
// windows starts counting at 0, unix at 1, add one to replicated unix behaviour.
|
||||
Ok((
|
||||
(terminal_size.width + 1) as u16,
|
||||
(terminal_size.height + 1) as u16,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn window_size() -> io::Result<WindowSize> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"Window pixel size not implemented for the Windows API.",
|
||||
))
|
||||
}
|
||||
|
||||
/// Queries the terminal's support for progressive keyboard enhancement.
|
||||
///
|
||||
/// This always returns `Ok(false)` on Windows.
|
||||
#[cfg(feature = "events")]
|
||||
pub fn supports_keyboard_enhancement() -> std::io::Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub(crate) fn clear(clear_type: ClearType) -> std::io::Result<()> {
|
||||
let screen_buffer = ScreenBuffer::current()?;
|
||||
let csbi = screen_buffer.info()?;
|
||||
|
||||
let pos = csbi.cursor_pos();
|
||||
let buffer_size = csbi.buffer_size();
|
||||
let current_attribute = csbi.attributes();
|
||||
|
||||
match clear_type {
|
||||
ClearType::All => {
|
||||
clear_entire_screen(buffer_size, current_attribute)?;
|
||||
}
|
||||
ClearType::FromCursorDown => clear_after_cursor(pos, buffer_size, current_attribute)?,
|
||||
ClearType::FromCursorUp => clear_before_cursor(pos, buffer_size, current_attribute)?,
|
||||
ClearType::CurrentLine => clear_current_line(pos, buffer_size, current_attribute)?,
|
||||
ClearType::UntilNewLine => clear_until_line(pos, buffer_size, current_attribute)?,
|
||||
_ => {
|
||||
clear_entire_screen(buffer_size, current_attribute)?;
|
||||
} //TODO: make purge flush the entire screen buffer not just the visible window.
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn scroll_up(row_count: u16) -> std::io::Result<()> {
|
||||
let csbi = ScreenBuffer::current()?;
|
||||
let mut window = csbi.info()?.terminal_window();
|
||||
|
||||
// check whether the window is too close to the screen buffer top
|
||||
let count = row_count as i16;
|
||||
if window.top >= count {
|
||||
window.top -= count; // move top down
|
||||
window.bottom -= count; // move bottom down
|
||||
|
||||
Console::output()?.set_console_info(true, window)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn scroll_down(row_count: u16) -> std::io::Result<()> {
|
||||
let screen_buffer = ScreenBuffer::current()?;
|
||||
let csbi = screen_buffer.info()?;
|
||||
let mut window = csbi.terminal_window();
|
||||
let buffer_size = csbi.buffer_size();
|
||||
|
||||
// check whether the window is too close to the screen buffer top
|
||||
let count = row_count as i16;
|
||||
if window.bottom < buffer_size.height - count {
|
||||
window.top += count; // move top down
|
||||
window.bottom += count; // move bottom down
|
||||
|
||||
Console::output()?.set_console_info(true, window)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_size(width: u16, height: u16) -> std::io::Result<()> {
|
||||
if width <= 1 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"terminal width must be at least 1",
|
||||
));
|
||||
}
|
||||
|
||||
if height <= 1 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"terminal height must be at least 1",
|
||||
));
|
||||
}
|
||||
|
||||
// get the position of the current console window
|
||||
let screen_buffer = ScreenBuffer::current()?;
|
||||
let console = Console::from(screen_buffer.handle().clone());
|
||||
let csbi = screen_buffer.info()?;
|
||||
|
||||
let current_size = csbi.buffer_size();
|
||||
let window = csbi.terminal_window();
|
||||
|
||||
let mut new_size = Size::new(current_size.width, current_size.height);
|
||||
|
||||
// If the buffer is smaller than this new window size, resize the
|
||||
// buffer to be large enough. Include window position.
|
||||
let mut resize_buffer = false;
|
||||
|
||||
let width = width as i16;
|
||||
if current_size.width < window.left + width {
|
||||
if window.left >= i16::MAX - width {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"terminal width too large",
|
||||
));
|
||||
}
|
||||
|
||||
new_size.width = window.left + width;
|
||||
resize_buffer = true;
|
||||
}
|
||||
let height = height as i16;
|
||||
if current_size.height < window.top + height {
|
||||
if window.top >= i16::MAX - height {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"terminal height too large",
|
||||
));
|
||||
}
|
||||
|
||||
new_size.height = window.top + height;
|
||||
resize_buffer = true;
|
||||
}
|
||||
|
||||
if resize_buffer {
|
||||
screen_buffer.set_size(new_size.width - 1, new_size.height - 1)?;
|
||||
}
|
||||
|
||||
let mut window = window;
|
||||
|
||||
// preserve the position, but change the size.
|
||||
window.bottom = window.top + height - 1;
|
||||
window.right = window.left + width - 1;
|
||||
console.set_console_info(true, window)?;
|
||||
|
||||
// if we resized the buffer, un-resize it.
|
||||
if resize_buffer {
|
||||
screen_buffer.set_size(current_size.width - 1, current_size.height - 1)?;
|
||||
}
|
||||
|
||||
let bounds = console.largest_window_size()?;
|
||||
|
||||
if width > bounds.x {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("terminal width {width} too large"),
|
||||
));
|
||||
}
|
||||
if height > bounds.y {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("terminal height {height} too large"),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_window_title(title: impl fmt::Display) -> std::io::Result<()> {
|
||||
struct Utf16Encoder(Vec<u16>);
|
||||
impl Write for Utf16Encoder {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
self.0.extend(s.encode_utf16());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let mut title_utf16 = Utf16Encoder(Vec::new());
|
||||
write!(title_utf16, "{title}").expect("formatting failed");
|
||||
title_utf16.0.push(0);
|
||||
let title = title_utf16.0;
|
||||
|
||||
let result = unsafe { SetConsoleTitleW(title.as_ptr()) };
|
||||
if result != 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_after_cursor(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let (mut x, mut y) = (location.x, location.y);
|
||||
|
||||
// if cursor position is at the outer right position
|
||||
if x > buffer_size.width {
|
||||
y += 1;
|
||||
x = 0;
|
||||
}
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(x, y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = buffer_size.width as u32 * buffer_size.height as u32;
|
||||
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)
|
||||
}
|
||||
|
||||
fn clear_before_cursor(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let (xpos, ypos) = (location.x, location.y);
|
||||
|
||||
// one cell after cursor position
|
||||
let x = 0;
|
||||
// one at row of cursor position
|
||||
let y = 0;
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(x, y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = (buffer_size.width as u32 * ypos as u32) + (xpos as u32 + 1);
|
||||
|
||||
// clear everything before cursor position
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)
|
||||
}
|
||||
|
||||
fn clear_entire_screen(buffer_size: Size, current_attribute: u16) -> std::io::Result<()> {
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = buffer_size.width as u32 * buffer_size.height as u32;
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(0, 0);
|
||||
|
||||
// clear the entire screen
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)?;
|
||||
|
||||
// put the cursor back at cell 0,0
|
||||
cursor::sys::move_to(0, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_current_line(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(0, location.y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = buffer_size.width as u32;
|
||||
|
||||
// clear the whole current line
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)?;
|
||||
|
||||
// put the cursor back at cell 1 on current row
|
||||
cursor::sys::move_to(0, location.y as u16)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_until_line(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let (x, y) = (location.x, location.y);
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(x, y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = (buffer_size.width - x) as u32;
|
||||
|
||||
// clear until the current line
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)?;
|
||||
|
||||
// put the cursor back at original cursor position before we did the clearing
|
||||
cursor::sys::move_to(x as u16, y as u16)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_winapi(
|
||||
start_location: Coord,
|
||||
cells_to_write: u32,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let console = Console::from(Handle::current_out_handle()?);
|
||||
console.fill_whit_character(start_location, cells_to_write, ' ')?;
|
||||
console.fill_whit_attribute(start_location, cells_to_write, current_attribute)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
// Create a new screen buffer to avoid changing the terminal the test
|
||||
// is running within.
|
||||
pub fn temp_screen_buffer() -> std::io::Result<ScreenBuffer> {
|
||||
let alternate_screen = ScreenBuffer::create()?;
|
||||
alternate_screen.show().unwrap();
|
||||
Ok(alternate_screen)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{ffi::OsString, os::windows::ffi::OsStringExt};
|
||||
|
||||
use crossterm_winapi::ScreenBuffer;
|
||||
use serial_test::serial;
|
||||
use winapi::um::wincon::GetConsoleTitleW;
|
||||
|
||||
use super::{scroll_down, scroll_up, set_size, set_window_title, size, temp_screen_buffer};
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_resize_winapi_20_21() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let (width, height) = size().unwrap();
|
||||
|
||||
// The values 20 and 21 are arbitrary and different from each other
|
||||
// just to see they're not crossed over.
|
||||
set_size(20, 21).unwrap();
|
||||
assert_eq!((20, 21), size().unwrap());
|
||||
|
||||
// reset to previous size
|
||||
set_size(width, height).unwrap();
|
||||
assert_eq!((width, height), size().unwrap());
|
||||
}
|
||||
|
||||
// This is similar to test_resize_winapi_20_21() above. This verifies that
|
||||
// another test of similar functionality runs independently (that a testing
|
||||
// race condition has been addressed).
|
||||
#[test]
|
||||
#[serial]
|
||||
#[ignore]
|
||||
fn test_resize_winapi_30_31() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let (width, height) = size().unwrap();
|
||||
|
||||
set_size(30, 31).unwrap();
|
||||
assert_eq!((30, 31), size().unwrap());
|
||||
|
||||
// reset to previous size
|
||||
set_size(width, height).unwrap();
|
||||
assert_eq!((width, height), size().unwrap());
|
||||
}
|
||||
|
||||
// Test is disabled, because it's failing on Travis CI
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_scroll_down_winapi() {
|
||||
let current_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
scroll_down(2).unwrap();
|
||||
|
||||
let new_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
assert_eq!(new_window.top, current_window.top + 2);
|
||||
assert_eq!(new_window.bottom, current_window.bottom + 2);
|
||||
}
|
||||
|
||||
// Test is disabled, because it's failing on Travis CI
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_scroll_up_winapi() {
|
||||
// move the terminal buffer down before moving it up
|
||||
test_scroll_down_winapi();
|
||||
|
||||
let current_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
scroll_up(2).unwrap();
|
||||
|
||||
let new_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
assert_eq!(new_window.top, current_window.top - 2);
|
||||
assert_eq!(new_window.bottom, current_window.bottom - 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_set_title_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let test_title = "this is a crossterm test title";
|
||||
set_window_title(test_title).unwrap();
|
||||
|
||||
let mut raw = [0_u16; 128];
|
||||
let length = unsafe { GetConsoleTitleW(raw.as_mut_ptr(), raw.len() as u32) } as usize;
|
||||
assert_ne!(0, length);
|
||||
|
||||
let console_title = OsString::from_wide(&raw[..length]).into_string().unwrap();
|
||||
assert_eq!(test_title, &console_title[..]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user