#![doc(html_favicon_url = "https://watchexec.github.io/logo:clearscreen.svg")]
#![doc(html_logo_url = "https://watchexec.github.io/logo:clearscreen.svg")]
#![warn(missing_docs)]
use std::{
borrow::Cow,
env,
io::{self, Write},
process::{Command, ExitStatus},
};
use terminfo::{
capability::{self, Expansion},
expand::{Context, Parameter},
Capability, Database, Value,
};
use thiserror::Error;
use which::which;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ClearScreen {
Terminfo,
TerminfoScreen,
TerminfoScrollback,
TerminfoReset,
XtermClear,
XtermReset,
TputClear,
TputReset,
Cls,
WindowsVt,
WindowsVtClear,
#[cfg(feature = "windows-console")]
WindowsConsoleClear,
#[cfg(feature = "windows-console")]
WindowsConsoleBlank,
WindowsCooked,
VtRis,
VtLeaveAlt,
VtCooked,
VtWellDone,
}
impl Default for ClearScreen {
fn default() -> Self {
use env::var;
use std::ffi::OsStr;
fn varfull(key: impl AsRef<OsStr>) -> bool {
var(key).map_or(false, |s| !s.is_empty())
}
let term = var("TERM").ok();
let term = term.as_ref();
if cfg!(windows) {
return if is_microsoft_terminal() {
Self::XtermClear
} else if is_windows_10() {
Self::WindowsVtClear
} else if term.is_some() && varfull("TERMINFO") {
Self::Terminfo
} else if term.is_some() && which("tput").is_ok() {
Self::TputClear
} else {
Self::Cls
};
}
if let Some(term) = term {
if (term.starts_with("gnome")
&& varfull("GNOME_TERMINAL_SCREEN")
&& varfull("GNOME_TERMINAL_SERVICE"))
|| term == "xfce"
|| term.contains("termite")
{
return Self::XtermClear;
}
if term == "syncterm"
|| term.contains("rxvt")
|| term.contains("kitty")
|| var("CHROME_DESKTOP").map_or(false, |cd| cd == "tess.desktop")
|| varfull("ZUTTY_VERSION")
|| varfull("ZELLIJ")
{
return Self::VtRis;
}
if term.starts_with("screen") || term.starts_with("konsole") || term.starts_with("tmux") {
return Self::XtermClear;
}
if cfg!(target_os = "macos")
&& term.starts_with("xterm")
&& Database::from_env()
.map(|info| info.get::<ResetScrollback>().is_none())
.unwrap_or(true)
{
return Self::XtermClear;
}
if !term.is_empty() && Database::from_env().is_ok() {
return Self::Terminfo;
}
}
Self::XtermClear
}
}
const ESC: &[u8] = b"\x1b";
const CSI: &[u8] = b"\x1b[";
const RIS: &[u8] = b"c";
impl ClearScreen {
pub fn clear(self) -> Result<(), Error> {
let mut stdout = io::stdout();
self.clear_to(&mut stdout)
}
pub fn clear_to(self, mut w: &mut impl Write) -> Result<(), Error> {
match self {
Self::Terminfo => {
let info = Database::from_env()?;
let mut ctx = Context::default();
if let Some(seq) = info.get::<capability::ClearScreen>() {
seq.expand().with(&mut ctx).to(&mut w)?;
w.flush()?;
} else {
return Err(Error::TerminfoCap("clear"));
}
if let Some(seq) = info.get::<ResetScrollback>() {
seq.expand().with(&mut ctx).to(&mut w)?;
w.flush()?;
}
}
Self::TerminfoScreen => {
let info = Database::from_env()?;
if let Some(seq) = info.get::<capability::ClearScreen>() {
seq.expand().to(&mut w)?;
w.flush()?;
} else {
return Err(Error::TerminfoCap("clear"));
}
}
Self::TerminfoScrollback => {
let info = Database::from_env()?;
if let Some(seq) = info.get::<ResetScrollback>() {
seq.expand().to(&mut w)?;
w.flush()?;
} else {
return Err(Error::TerminfoCap("E3"));
}
}
Self::TerminfoReset => {
let info = Database::from_env()?;
let mut ctx = Context::default();
let mut reset = false;
if let Some(seq) = info.get::<capability::Reset1String>() {
reset = true;
seq.expand().with(&mut ctx).to(&mut w)?;
}
if let Some(seq) = info.get::<capability::Reset2String>() {
reset = true;
seq.expand().with(&mut ctx).to(&mut w)?;
}
if let Some(seq) = info.get::<capability::Reset3String>() {
reset = true;
seq.expand().with(&mut ctx).to(&mut w)?;
}
if let Some(seq) = info.get::<capability::ResetFile>() {
reset = true;
seq.expand().with(&mut ctx).to(&mut w)?;
}
w.flush()?;
if reset {
return Ok(());
}
if let Some(seq) = info.get::<capability::Init1String>() {
reset = true;
seq.expand().with(&mut ctx).to(&mut w)?;
}
if let Some(seq) = info.get::<capability::Init2String>() {
reset = true;
seq.expand().with(&mut ctx).to(&mut w)?;
}
if let Some(seq) = info.get::<capability::Init3String>() {
reset = true;
seq.expand().with(&mut ctx).to(&mut w)?;
}
if let Some(seq) = info.get::<capability::InitFile>() {
reset = true;
seq.expand().with(&mut ctx).to(&mut w)?;
}
w.flush()?;
if !reset {
return Err(Error::TerminfoCap("reset"));
}
}
Self::XtermClear => {
const CURSOR_HOME: &[u8] = b"H";
const ERASE_SCREEN: &[u8] = b"2J";
const ERASE_SCROLLBACK: &[u8] = b"3J";
w.write_all(CSI)?;
w.write_all(CURSOR_HOME)?;
w.write_all(CSI)?;
w.write_all(ERASE_SCREEN)?;
w.write_all(CSI)?;
w.write_all(ERASE_SCROLLBACK)?;
w.flush()?;
}
Self::XtermReset => {
const STR: &[u8] = b"!p";
const RESET_WIDTH_AND_SCROLL: &[u8] = b"?3;4l";
const RESET_REPLACE: &[u8] = b"4l";
const RESET_KEYPAD: &[u8] = b">";
const RESET_MARGINS: &[u8] = b"?69l";
w.write_all(ESC)?;
w.write_all(RIS)?;
w.write_all(CSI)?;
w.write_all(STR)?;
w.write_all(CSI)?;
w.write_all(RESET_WIDTH_AND_SCROLL)?;
w.write_all(CSI)?;
w.write_all(RESET_REPLACE)?;
w.write_all(ESC)?;
w.write_all(RESET_KEYPAD)?;
w.write_all(CSI)?;
w.write_all(RESET_MARGINS)?;
w.flush()?;
}
Self::TputClear => {
let status = Command::new("tput").arg("clear").status()?;
if !status.success() {
return Err(Error::Command("tput clear", status));
}
}
Self::TputReset => {
let status = Command::new("tput").arg("reset").status()?;
if !status.success() {
return Err(Error::Command("tput reset", status));
}
}
Self::Cls => {
let status = Command::new("cmd.exe").arg("/C").arg("cls").status()?;
if !status.success() {
return Err(Error::Command("cls", status));
}
}
Self::WindowsVt => win::vt()?,
Self::WindowsVtClear => {
let vtres = win::vt();
Self::XtermClear.clear_to(w)?;
vtres?;
}
#[cfg(feature = "windows-console")]
Self::WindowsConsoleClear => win::clear()?,
#[cfg(feature = "windows-console")]
Self::WindowsConsoleBlank => win::blank()?,
Self::WindowsCooked => win::cooked()?,
Self::VtRis => {
w.write_all(ESC)?;
w.write_all(RIS)?;
w.flush()?;
}
Self::VtLeaveAlt => {
const LEAVE_ALT: &[u8] = b"?1049l";
w.write_all(CSI)?;
w.write_all(LEAVE_ALT)?;
w.flush()?;
}
Self::VtCooked => unix::vt_cooked()?,
Self::VtWellDone => unix::vt_well_done()?,
}
Ok(())
}
}
pub fn clear() -> Result<(), Error> {
ClearScreen::default().clear()
}
pub fn is_microsoft_terminal() -> bool {
env::var("WT_SESSION").is_ok()
}
pub fn is_windows_10() -> bool {
win::is_windows_10()
}
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] io::Error),
#[error("{0}: {1}")]
Command(&'static str, ExitStatus),
#[cfg(unix)]
#[error(transparent)]
Nix(#[from] nix::Error),
#[error(transparent)]
Terminfo(#[from] terminfo::Error),
#[error("required terminfo capability not available: {0}")]
TerminfoCap(&'static str),
#[error("encountered a null pointer while reading {0}")]
NullPtr(&'static str),
}
#[cfg(unix)]
mod unix {
use super::Error;
use nix::{
libc::STDIN_FILENO,
sys::termios::{
tcgetattr, tcsetattr, ControlFlags, InputFlags, LocalFlags, OutputFlags,
SetArg::TCSANOW, Termios,
},
unistd::isatty,
};
use std::{fs::OpenOptions, os::unix::prelude::AsRawFd};
pub(crate) fn vt_cooked() -> Result<(), Error> {
write_termios(|t| {
t.input_flags.insert(
InputFlags::BRKINT
| InputFlags::ICRNL | InputFlags::IGNPAR
| InputFlags::ISTRIP | InputFlags::IXON,
);
t.output_flags.insert(OutputFlags::OPOST);
t.local_flags.insert(LocalFlags::ICANON | LocalFlags::ISIG);
})
}
pub(crate) fn vt_well_done() -> Result<(), Error> {
write_termios(|t| {
let mut inserts =
InputFlags::BRKINT
| InputFlags::ICRNL | InputFlags::IGNPAR
| InputFlags::IMAXBEL
| InputFlags::ISTRIP | InputFlags::IXON;
#[cfg(any(target_os = "android", target_os = "linux", target_os = "macos"))]
{
inserts |= InputFlags::IUTF8;
}
t.input_flags.insert(inserts);
t.output_flags
.insert(OutputFlags::ONLCR | OutputFlags::OPOST);
t.control_flags.insert(ControlFlags::CREAD);
t.local_flags.insert(LocalFlags::ICANON | LocalFlags::ISIG);
})
}
fn reset_termios(t: &mut Termios) {
t.input_flags.remove(InputFlags::all());
t.output_flags.remove(OutputFlags::all());
t.control_flags.remove(ControlFlags::all());
t.local_flags.remove(LocalFlags::all());
}
fn write_termios(f: impl Fn(&mut Termios)) -> Result<(), Error> {
if isatty(STDIN_FILENO)? {
let mut t = tcgetattr(STDIN_FILENO)?;
reset_termios(&mut t);
f(&mut t);
tcsetattr(STDIN_FILENO, TCSANOW, &t)?;
} else {
let tty = OpenOptions::new().read(true).write(true).open("/dev/tty")?;
let fd = tty.as_raw_fd();
let mut t = tcgetattr(fd)?;
reset_termios(&mut t);
f(&mut t);
tcsetattr(fd, TCSANOW, &t)?;
}
Ok(())
}
}
#[cfg(windows)]
mod win {
use super::Error;
use std::{convert::TryFrom, io, mem::size_of, ptr};
use winapi::{
shared::minwindef::{DWORD, FALSE},
um::{
consoleapi::{GetConsoleMode, SetConsoleMode},
handleapi::INVALID_HANDLE_VALUE,
lmapibuf::{NetApiBufferAllocate, NetApiBufferFree},
lmserver::{NetServerGetInfo, MAJOR_VERSION_MASK, SERVER_INFO_101, SV_PLATFORM_ID_NT},
lmwksta::{NetWkstaGetInfo, WKSTA_INFO_100},
processenv::GetStdHandle,
winbase::{VerifyVersionInfoW, STD_OUTPUT_HANDLE},
wincon::{
ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT,
ENABLE_VIRTUAL_TERMINAL_PROCESSING,
},
winnt::{
VerSetConditionMask, HANDLE, OSVERSIONINFOEXW, POSVERSIONINFOEXW, ULONGLONG,
VER_GREATER_EQUAL, VER_MAJORVERSION, VER_MINORVERSION, VER_SERVICEPACKMAJOR,
},
},
};
#[cfg(feature = "windows-console")]
use winapi::um::{
wincon::{
FillConsoleOutputAttribute, FillConsoleOutputCharacterW, GetConsoleScreenBufferInfo,
ScrollConsoleScreenBufferW, SetConsoleCursorPosition, CONSOLE_SCREEN_BUFFER_INFO,
PCONSOLE_SCREEN_BUFFER_INFO,
},
wincontypes::{CHAR_INFO_Char, CHAR_INFO, COORD, SMALL_RECT},
winnt::SHORT,
};
fn console_handle() -> Result<HANDLE, Error> {
match unsafe { GetStdHandle(STD_OUTPUT_HANDLE) } {
INVALID_HANDLE_VALUE => Err(io::Error::last_os_error().into()),
handle => Ok(handle),
}
}
#[cfg(feature = "windows-console")]
fn buffer_info(console: HANDLE) -> Result<CONSOLE_SCREEN_BUFFER_INFO, Error> {
let csbi: PCONSOLE_SCREEN_BUFFER_INFO = ptr::null_mut();
if unsafe { GetConsoleScreenBufferInfo(console, csbi) } == FALSE {
return Err(io::Error::last_os_error().into());
}
if csbi.is_null() {
Err(Error::NullPtr("GetConsoleScreenBufferInfo"))
} else {
Ok(unsafe { ptr::read(csbi) })
}
}
pub(crate) fn vt() -> Result<(), Error> {
let stdout = console_handle()?;
let mut mode = 0;
if unsafe { GetConsoleMode(stdout, &mut mode) } == FALSE {
return Err(io::Error::last_os_error().into());
}
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
if unsafe { SetConsoleMode(stdout, mode) } == FALSE {
return Err(io::Error::last_os_error().into());
}
Ok(())
}
#[cfg(feature = "windows-console")]
pub(crate) fn clear() -> Result<(), Error> {
let console = console_handle()?;
let csbi = buffer_info(console)?;
let rect = SMALL_RECT {
Left: 0,
Top: 0,
Right: csbi.dwSize.X,
Bottom: csbi.dwSize.Y,
};
let target = COORD {
X: 0,
Y: (0 - csbi.dwSize.Y) as SHORT,
};
let mut space = CHAR_INFO_Char::default();
unsafe { *space.AsciiChar_mut() = b' ' as i8 };
let fill = CHAR_INFO {
Char: space,
Attributes: csbi.wAttributes,
};
if unsafe { ScrollConsoleScreenBufferW(console, &rect, ptr::null(), target, &fill) }
== FALSE
{
return Err(io::Error::last_os_error().into());
}
let mut cursor = csbi.dwCursorPosition;
cursor.X = 0;
cursor.Y = 0;
if unsafe { SetConsoleCursorPosition(console, cursor) } == FALSE {
return Err(io::Error::last_os_error().into());
}
Ok(())
}
#[cfg(feature = "windows-console")]
pub(crate) fn blank() -> Result<(), Error> {
let console = console_handle()?;
let csbi = buffer_info(console)?;
let buffer_size = csbi.dwSize.X * csbi.dwSize.Y;
let home_coord = COORD { X: 0, Y: 0 };
if FALSE
== unsafe {
FillConsoleOutputCharacterW(
console,
b' ' as u16,
u32::try_from(buffer_size).unwrap_or(0),
home_coord,
ptr::null_mut(),
)
} {
return Err(io::Error::last_os_error().into());
}
let csbi = buffer_info(console)?;
if FALSE
== unsafe {
FillConsoleOutputAttribute(
console,
csbi.wAttributes,
u32::try_from(buffer_size).unwrap_or(0),
home_coord,
ptr::null_mut(),
)
} {
return Err(io::Error::last_os_error().into());
}
if unsafe { SetConsoleCursorPosition(console, home_coord) } == FALSE {
return Err(io::Error::last_os_error().into());
}
Ok(())
}
const ENABLE_COOKED_MODE: DWORD =
ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT;
pub(crate) fn cooked() -> Result<(), Error> {
let stdout = console_handle()?;
let mut mode = 0;
if unsafe { GetConsoleMode(stdout, &mut mode) } == FALSE {
return Err(io::Error::last_os_error().into());
}
mode |= ENABLE_COOKED_MODE;
if unsafe { SetConsoleMode(stdout, mode) } == FALSE {
return Err(io::Error::last_os_error().into());
}
Ok(())
}
const ABRACADABRA_THRESHOLD: (u8, u8) = (0x0A, 0x00);
#[inline]
fn um_verify_version() -> bool {
let condition_mask: ULONGLONG = unsafe {
VerSetConditionMask(
VerSetConditionMask(
VerSetConditionMask(0, VER_MAJORVERSION, VER_GREATER_EQUAL),
VER_MINORVERSION,
VER_GREATER_EQUAL,
),
VER_SERVICEPACKMAJOR,
VER_GREATER_EQUAL,
)
};
let mut osvi = OSVERSIONINFOEXW {
dwMinorVersion: ABRACADABRA_THRESHOLD.1 as _,
dwMajorVersion: ABRACADABRA_THRESHOLD.0 as _,
wServicePackMajor: 0,
..OSVERSIONINFOEXW::default()
};
let ret = unsafe {
VerifyVersionInfoW(
&mut osvi as POSVERSIONINFOEXW,
VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR,
condition_mask,
)
};
ret != FALSE
}
#[inline]
fn um_netserver() -> Result<bool, Error> {
unsafe {
let mut buf = ptr::null_mut();
match NetApiBufferAllocate(
u32::try_from(size_of::<SERVER_INFO_101>()).unwrap(),
&mut buf,
) {
0 => {}
err => return Err(io::Error::from_raw_os_error(i32::try_from(err).unwrap()).into()),
}
let ret = match NetServerGetInfo(ptr::null_mut(), 101, buf as _) {
0 => {
let info: SERVER_INFO_101 = ptr::read(buf as _);
let version = info.sv101_version_major | MAJOR_VERSION_MASK;
Ok(info.sv101_platform_id == SV_PLATFORM_ID_NT
&& version > ABRACADABRA_THRESHOLD.0 as _)
}
err => Err(io::Error::from_raw_os_error(i32::try_from(err).unwrap()).into()),
};
match NetApiBufferFree(buf) {
0 => {}
err => return Err(io::Error::from_raw_os_error(i32::try_from(err).unwrap()).into()),
}
ret
}
}
#[inline]
fn um_workstation() -> Result<bool, Error> {
unsafe {
let mut buf = ptr::null_mut();
match NetApiBufferAllocate(
u32::try_from(size_of::<WKSTA_INFO_100>()).unwrap(),
&mut buf,
) {
0 => {}
err => return Err(io::Error::from_raw_os_error(i32::try_from(err).unwrap()).into()),
}
let ret = match NetWkstaGetInfo(ptr::null_mut(), 100, buf as _) {
0 => {
let info: WKSTA_INFO_100 = ptr::read(buf as _);
Ok(info.wki100_platform_id == SV_PLATFORM_ID_NT
&& info.wki100_ver_major > ABRACADABRA_THRESHOLD.0 as _)
}
err => Err(io::Error::from_raw_os_error(i32::try_from(err).unwrap()).into()),
};
match NetApiBufferFree(buf) {
0 => {}
err => return Err(io::Error::from_raw_os_error(i32::try_from(err).unwrap()).into()),
}
ret
}
}
fn vt_attempt() -> Result<bool, Error> {
let stdout = console_handle()?;
let mut mode = 0;
if unsafe { GetConsoleMode(stdout, &mut mode) } == FALSE {
return Err(io::Error::last_os_error().into());
}
let mut support = false;
let mut newmode = mode;
newmode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
if unsafe { SetConsoleMode(stdout, newmode) } != FALSE {
support = true;
}
unsafe { SetConsoleMode(stdout, mode) };
Ok(support)
}
#[inline]
pub(crate) fn is_windows_10() -> bool {
if um_verify_version() {
return true;
}
if um_netserver().unwrap_or(false) {
return true;
}
if um_workstation().unwrap_or(false) {
return true;
}
vt_attempt().unwrap_or(false)
}
}
#[cfg(not(unix))]
#[allow(clippy::unnecessary_wraps)]
mod unix {
use super::Error;
pub(crate) fn vt_cooked() -> Result<(), Error> {
Ok(())
}
pub(crate) fn vt_well_done() -> Result<(), Error> {
Ok(())
}
}
#[cfg(not(windows))]
#[allow(clippy::unnecessary_wraps)]
mod win {
use super::Error;
pub(crate) fn vt() -> Result<(), Error> {
Ok(())
}
#[cfg(feature = "windows-console")]
pub(crate) fn clear() -> Result<(), Error> {
Ok(())
}
#[cfg(feature = "windows-console")]
pub(crate) fn blank() -> Result<(), Error> {
Ok(())
}
pub(crate) fn cooked() -> Result<(), Error> {
Ok(())
}
#[inline]
pub(crate) fn is_windows_10() -> bool {
false
}
}
#[derive(Eq, PartialEq, Clone, Debug)]
struct ResetScrollback<'a>(Cow<'a, [u8]>);
impl<'a> Capability<'a> for ResetScrollback<'a> {
#[inline]
fn name() -> &'static str {
"E3"
}
#[inline]
fn from(value: Option<&'a Value>) -> Option<Self> {
if let Some(&Value::String(ref value)) = value {
Some(Self(Cow::Borrowed(value)))
} else {
None
}
}
#[inline]
fn into(self) -> Option<Value> {
Some(Value::String(match self.0 {
Cow::Borrowed(value) => value.into(),
Cow::Owned(value) => value,
}))
}
}
impl<'a, T: AsRef<&'a [u8]>> From<T> for ResetScrollback<'a> {
#[inline]
fn from(value: T) -> Self {
Self(Cow::Borrowed(value.as_ref()))
}
}
impl<'a> AsRef<[u8]> for ResetScrollback<'a> {
#[inline]
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl<'a> ResetScrollback<'a> {
#[inline]
fn expand(&self) -> Expansion<Self> {
#[allow(dead_code)]
struct ExpansionHere<'a, T: 'a + AsRef<[u8]>> {
string: &'a T,
params: [Parameter; 9],
context: Option<&'a mut Context>,
}
let here = ExpansionHere {
string: self,
params: Default::default(),
context: None,
};
unsafe { std::mem::transmute(here) }
}
}