fix: refresh diff colors on terminal theme changes

This commit is contained in:
2026-10-11 21:21:17 +02:00
parent d6f6203fb6
commit ffdd42fcce
55 changed files with 11999 additions and 24 deletions
+27
View File
@@ -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;
+154
View File
@@ -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
View File
@@ -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
View File
@@ -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[..]);
}
}