#[cfg(target_arch = "wasm32")]
use crate::crossterm_stub as crossterm;
use std::cell::Cell;
use std::fmt;
use std::io::{self, Write};
use std::panic::UnwindSafe;
use std::process::Command;
use std::sync::Once;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::time::{Duration, Instant};
#[cfg(target_arch = "wasm32")]
use crate::crossterm_stub::{from_crossterm, from_crossterm_kitty};
use crate::event::Event;
#[cfg(not(target_arch = "wasm32"))]
use crate::event::{from_crossterm, from_crossterm_kitty};
#[cfg(all(unix, feature = "termion"))]
mod with_termion;
#[cfg(feature = "termwiz")]
mod with_termwiz;
const RAW: u8 = 1;
const ALTERNATE: u8 = 2;
const MOUSE: u8 = 4;
const PASTE: u8 = 8;
const KITTY: u8 = 16;
const SYNC: u8 = 32;
const PIXELS: u8 = 64;
const PUSH_KITTY: &str = "\x1b[>3u";
const PUSH_KITTY_ALL: &str = "\x1b[>15u";
#[cfg(feature = "termwiz")]
const PUSH_KITTY_KEYS: &str = "\x1b[>1u";
const POP_KITTY: &str = "\x1b[<1u";
static ANSWERS: AtomicU8 = AtomicU8::new(0);
pub const BEGIN_SYNCHRONIZED_UPDATE: &str = "\x1b[?2026h";
pub const END_SYNCHRONIZED_UPDATE: &str = "\x1b[?2026l";
#[cfg(unix)]
const ASK_SYNC: &str = "\x1b[?2026$p";
pub const SYNC_OUTPUT_ENV: &str = "RICH_SYNC_OUTPUT";
#[cfg(unix)]
const ASK_PIXELS: &str = "\x1b[?1016$p";
pub const PIXEL_MOUSE_ENV: &str = "RICH_PIXEL_MOUSE";
fn pixels_allowed(value: Option<&str>) -> bool {
value.map(str::trim) != Some("0")
}
pub fn synchronized_update(frame: &str) -> String {
if frame.is_empty() {
return String::new();
}
let mut out = String::with_capacity(
BEGIN_SYNCHRONIZED_UPDATE.len() + frame.len() + END_SYNCHRONIZED_UPDATE.len(),
);
out.push_str(BEGIN_SYNCHRONIZED_UPDATE);
out.push_str(frame);
out.push_str(END_SYNCHRONIZED_UPDATE);
out
}
fn sync_override(value: Option<&str>) -> Option<bool> {
match value.map(str::trim) {
Some("0") => Some(false),
Some("1") => Some(true),
_ => None,
}
}
fn sync_from_env() -> Option<bool> {
sync_override(std::env::var(SYNC_OUTPUT_ENV).ok().as_deref())
}
static ACTIVE: AtomicU8 = AtomicU8::new(0);
static HOOK: Once = Once::new();
static OWNED: AtomicBool = AtomicBool::new(false);
static ON_STDERR: AtomicBool = AtomicBool::new(false);
static LIBRARY: AtomicU8 = AtomicU8::new(0);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BackendKind {
#[default]
Crossterm,
#[cfg(all(unix, feature = "termion"))]
Termion,
#[cfg(feature = "termwiz")]
Termwiz,
}
impl BackendKind {
pub const ALL: &'static [BackendKind] = &[
BackendKind::Crossterm,
#[cfg(all(unix, feature = "termion"))]
BackendKind::Termion,
#[cfg(feature = "termwiz")]
BackendKind::Termwiz,
];
pub fn name(self) -> &'static str {
match self {
BackendKind::Crossterm => "crossterm",
#[cfg(all(unix, feature = "termion"))]
BackendKind::Termion => "termion",
#[cfg(feature = "termwiz")]
BackendKind::Termwiz => "termwiz",
}
}
pub fn from_name(name: &str) -> Option<BackendKind> {
BackendKind::ALL
.iter()
.copied()
.find(|kind| kind.name().eq_ignore_ascii_case(name))
}
fn index(self) -> u8 {
match self {
BackendKind::Crossterm => 0,
#[cfg(all(unix, feature = "termion"))]
BackendKind::Termion => 1,
#[cfg(feature = "termwiz")]
BackendKind::Termwiz => 2,
}
}
fn live() -> BackendKind {
let index = LIBRARY.load(Ordering::SeqCst);
BackendKind::ALL
.iter()
.copied()
.find(|kind| kind.index() == index)
.unwrap_or_default()
}
fn raw_mode(self, on: bool, wait: bool) -> io::Result<()> {
let _ = wait;
match self {
BackendKind::Crossterm if on => crossterm::terminal::enable_raw_mode(),
BackendKind::Crossterm => crossterm::terminal::disable_raw_mode(),
#[cfg(all(unix, feature = "termion"))]
BackendKind::Termion => with_termion::raw_mode(on, wait),
#[cfg(feature = "termwiz")]
BackendKind::Termwiz => with_termwiz::raw_mode(on, wait),
}
}
fn owns_alternate_screen(self) -> bool {
#[cfg(feature = "termwiz")]
if self == BackendKind::Termwiz {
return true;
}
false
}
fn alternate_screen(self, on: bool, wait: bool) -> Option<io::Result<()>> {
let _ = (on, wait);
match self {
#[cfg(feature = "termwiz")]
BackendKind::Termwiz => Some(with_termwiz::alternate_screen(on, wait)),
_ => None,
}
}
fn alternate_sequence(self, on: bool) -> String {
match self {
#[cfg(all(unix, feature = "termion"))]
BackendKind::Termion if on => termion::screen::ToAlternateScreen.to_string(),
#[cfg(all(unix, feature = "termion"))]
BackendKind::Termion => termion::screen::ToMainScreen.to_string(),
_ if on => "\x1b[?1049h".into(),
_ => "\x1b[?1049l".into(),
}
}
fn kitty_push(self) -> &'static str {
match self {
#[cfg(feature = "termwiz")]
BackendKind::Termwiz => PUSH_KITTY_KEYS,
_ => PUSH_KITTY,
}
}
fn after_raw_mode(self) -> &'static str {
match self {
#[cfg(all(unix, feature = "termwiz"))]
BackendKind::Termwiz => "\x1b[>4;0m",
_ => "",
}
}
fn release(self) {
match self {
#[cfg(all(unix, feature = "termion"))]
BackendKind::Termion => with_termion::release(),
#[cfg(feature = "termwiz")]
BackendKind::Termwiz => with_termwiz::release(),
_ => {}
}
}
}
impl fmt::Display for BackendKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Output {
#[default]
Stdout,
Stderr,
}
impl Output {
pub fn is_terminal(self) -> bool {
use std::io::IsTerminal;
match self {
Output::Stdout => io::stdout().is_terminal(),
Output::Stderr => io::stderr().is_terminal(),
}
}
fn write(self, text: &str) -> io::Result<()> {
match self {
Output::Stdout => {
let mut out = io::stdout().lock();
out.write_all(text.as_bytes())?;
out.flush()
}
Output::Stderr => {
let mut out = io::stderr().lock();
out.write_all(text.as_bytes())?;
out.flush()
}
}
}
fn live() -> Output {
if ON_STDERR.load(Ordering::SeqCst) {
Output::Stderr
} else {
Output::Stdout
}
}
}
pub(crate) fn keys_available() -> bool {
use std::io::IsTerminal;
if io::stdin().is_terminal() {
return true;
}
#[cfg(unix)]
let terminal = "/dev/tty";
#[cfg(windows)]
let terminal = "CONIN$";
#[cfg(any(unix, windows))]
return std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(terminal)
.is_ok();
#[cfg(not(any(unix, windows)))]
false
}
fn restore(wait: bool) -> io::Result<()> {
let active = ACTIVE.swap(0, Ordering::SeqCst);
if active == 0 {
return Ok(());
}
let library = BackendKind::live();
let out = undo(active, library);
let written = Output::live().write(&out);
let left = if active & ALTERNATE != 0 {
library.alternate_screen(false, wait).unwrap_or(Ok(()))
} else {
Ok(())
};
let raw = if active & RAW != 0 {
let off = library.raw_mode(false, wait);
let after = match library.after_raw_mode() {
"" => Ok(()),
after => Output::live().write(after),
};
off.and(after)
} else {
Ok(())
};
written.and(left).and(raw)
}
fn undo(active: u8, library: BackendKind) -> String {
let mut out = String::new();
if active & SYNC != 0 {
out.push_str(END_SYNCHRONIZED_UPDATE);
}
if active & KITTY != 0 {
out.push_str(POP_KITTY);
}
if active & PIXELS != 0 {
out.push_str("\x1b[?1016l");
}
if active & MOUSE != 0 {
out.push_str("\x1b[?1006l\x1b[?1015l\x1b[?1003l\x1b[?1002l\x1b[?1000l");
}
if active & PASTE != 0 {
out.push_str("\x1b[?2004l");
}
out.push_str("\x1b[?25h");
if active & ALTERNATE != 0 && !library.owns_alternate_screen() {
out.push_str(&library.alternate_sequence(false));
}
out
}
static RAW_LINE: AtomicBool = AtomicBool::new(false);
#[cfg(unix)]
static SIGNALS: Once = Once::new();
pub(crate) fn raw_line(on: bool) {
if on {
watch_signals();
}
RAW_LINE.store(on, Ordering::SeqCst);
}
fn watch_signals() {
#[cfg(unix)]
SIGNALS.call_once(|| {
use signal_hook::consts::{SIGHUP, SIGQUIT, SIGTERM, SIGTSTP};
let Ok(mut signals) =
signal_hook::iterator::Signals::new([SIGTERM, SIGHUP, SIGQUIT, SIGTSTP])
else {
return;
};
let _ = std::thread::Builder::new()
.name("rich-interact-signals".into())
.spawn(move || {
for signal in signals.forever() {
if signal == SIGTSTP {
suspend();
RESUMED.store(true, Ordering::SeqCst);
} else {
restore_for_signal();
run_teardowns();
let _ = signal_hook::low_level::emulate_default_handler(signal);
}
}
});
});
}
type Teardown = Box<dyn Fn() + Send + Sync>;
static TEARDOWNS: std::sync::Mutex<Vec<Teardown>> = std::sync::Mutex::new(Vec::new());
pub fn on_terminate(teardown: impl Fn() + Send + Sync + 'static) {
TEARDOWNS
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(Box::new(teardown));
}
#[cfg(unix)]
fn run_teardowns() {
let all = std::mem::take(&mut *TEARDOWNS.lock().unwrap_or_else(|e| e.into_inner()));
for teardown in all {
teardown();
}
}
#[cfg(unix)]
fn restore_for_signal() {
let active = ACTIVE.swap(0, Ordering::SeqCst);
let library = BackendKind::live();
if active != 0 {
write_direct(&undo(active, library));
}
if active & ALTERNATE != 0 {
let _ = library.alternate_screen(false, true);
}
if active & RAW != 0 {
let _ = library.raw_mode(false, true);
write_direct(library.after_raw_mode());
}
if RAW_LINE.swap(false, Ordering::SeqCst) {
let _ = crossterm::terminal::disable_raw_mode();
}
}
static RESUMED: AtomicBool = AtomicBool::new(false);
static SUSPENDING: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(unix)]
fn suspend() {
let _suspending = SUSPENDING.lock().unwrap_or_else(|e| e.into_inner());
let active = ACTIVE.load(Ordering::SeqCst);
let raw_line = RAW_LINE.load(Ordering::SeqCst);
let library = BackendKind::live();
restore_for_signal();
let _ = signal_hook::low_level::emulate_default_handler(signal_hook::consts::SIGTSTP);
if active & RAW != 0 {
let _ = library.raw_mode(true, true);
}
if raw_line {
let _ = crossterm::terminal::enable_raw_mode();
}
RAW_LINE.store(raw_line, Ordering::SeqCst);
let mut out = String::new();
if active & ALTERNATE != 0 {
if library.alternate_screen(true, true).is_none() {
out.push_str(&library.alternate_sequence(true));
}
out.push_str("\x1b[H");
}
if active & KITTY != 0 {
out.push_str(library.kitty_push());
}
if active & MOUSE != 0 {
out.push_str("\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h");
}
if active & PIXELS != 0 {
out.push_str("\x1b[?1016h");
}
if active & PASTE != 0 {
out.push_str("\x1b[?2004h");
}
if active != 0 {
out.push_str("\x1b[?25l");
}
ACTIVE.fetch_or(active, Ordering::SeqCst);
write_direct(&out);
}
#[cfg(unix)]
fn write_direct(text: &str) {
if text.is_empty() {
return;
}
let direct = std::fs::OpenOptions::new()
.write(true)
.open("/dev/tty")
.and_then(|mut tty| tty.write_all(text.as_bytes()));
if direct.is_err() {
let _ = Output::live().write(text);
}
}
#[cfg(any(all(unix, feature = "termion"), feature = "termwiz"))]
fn lock<T>(mutex: &std::sync::Mutex<T>, wait: bool) -> io::Result<std::sync::MutexGuard<'_, T>> {
use std::sync::TryLockError;
if wait {
return Ok(mutex.lock().unwrap_or_else(|e| e.into_inner()));
}
match mutex.try_lock() {
Ok(guard) => Ok(guard),
Err(TryLockError::Poisoned(e)) => Ok(e.into_inner()),
Err(TryLockError::WouldBlock) => Err(io::ErrorKind::WouldBlock.into()),
}
}
#[cfg(unix)]
const ANSWER_WAIT: Duration = Duration::from_secs(1);
#[cfg(unix)]
const ANSWER_WAIT_TYPING: Duration = Duration::from_millis(250);
#[cfg(unix)]
struct Asked {
bytes: Vec<u8>,
answered: bool,
}
#[cfg(unix)]
fn ask_terminal(query: &str, done: impl Fn(&[u8]) -> bool) -> Option<Asked> {
use std::io::Read;
use std::os::unix::io::AsRawFd;
let mut tty = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty")
.ok()?;
tty.write_all(query.as_bytes()).ok()?;
tty.flush().ok()?;
let mut end = Instant::now() + ANSWER_WAIT;
let mut typing = false;
let mut bytes = Vec::new();
loop {
if done(&bytes) {
return Some(Asked {
bytes,
answered: true,
});
}
if !typing && !strip_answers(&bytes).is_empty() {
typing = true;
end = end.min(Instant::now() + ANSWER_WAIT_TYPING);
}
let left = end.saturating_duration_since(Instant::now());
if left.is_zero() {
break;
}
let mut ready = [filedescriptor::pollfd {
fd: tty.as_raw_fd(),
events: filedescriptor::POLLIN,
revents: 0,
}];
match filedescriptor::poll(&mut ready, Some(left)) {
Ok(0) => break,
Ok(_) => {}
Err(filedescriptor::Error::Poll(e)) if e.kind() == io::ErrorKind::Interrupted => {
continue;
}
Err(_) => break,
}
let mut buffer = [0u8; 256];
match tty.read(&mut buffer) {
Ok(0) | Err(_) => break,
Ok(read) => bytes.extend_from_slice(&buffer[..read]),
}
}
Some(Asked {
bytes,
answered: false,
})
}
#[cfg(any(unix, test))]
fn strip_answers(bytes: &[u8]) -> Vec<u8> {
let mut rest = Vec::new();
let mut at = 0;
while at < bytes.len() {
if !bytes[at..].starts_with(b"\x1b[?") {
rest.push(bytes[at]);
at += 1;
continue;
}
let mut end = at + 3;
while end < bytes.len() && (0x20..=0x3f).contains(&bytes[end]) {
end += 1;
}
match bytes.get(end) {
None => break,
Some(0x40..=0x7e) => at = end + 1,
Some(_) => at = end,
}
}
rest
}
#[cfg(any(unix, test))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct PrivateAnswer<'a> {
parameters: &'a [u8],
intermediates: &'a [u8],
last: u8,
}
#[cfg(any(unix, test))]
fn private_answers(bytes: &[u8]) -> impl Iterator<Item = PrivateAnswer<'_>> + '_ {
let mut at = 0;
std::iter::from_fn(move || {
while at + 3 <= bytes.len() {
if !bytes[at..].starts_with(b"\x1b[?") {
at += 1;
continue;
}
let start = at + 3;
let mut end = start;
while end < bytes.len() && (0x30..=0x3f).contains(&bytes[end]) {
end += 1;
}
let middle = end;
while end < bytes.len() && (0x20..=0x2f).contains(&bytes[end]) {
end += 1;
}
match bytes.get(end) {
None => return None,
Some(&last @ 0x40..=0x7e) => {
at = end + 1;
return Some(PrivateAnswer {
parameters: &bytes[start..middle],
intermediates: &bytes[middle..end],
last,
});
}
Some(_) => at = end,
}
}
None
})
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct Answers {
kitty: bool,
sync: bool,
pixels: bool,
}
impl Answers {
fn encode(self) -> u8 {
1 | (u8::from(self.kitty) << 1) | (u8::from(self.sync) << 2) | (u8::from(self.pixels) << 3)
}
fn decode(bits: u8) -> Answers {
Answers {
kitty: bits & 2 != 0,
sync: bits & 4 != 0,
pixels: bits & 8 != 0,
}
}
#[cfg(any(unix, test))]
fn read(bytes: &[u8]) -> Option<Answers> {
let mut answers = Answers::default();
for answer in private_answers(bytes) {
match (answer.last, answer.intermediates) {
(b'c', b"") => return Some(answers),
(b'u', b"") => answers.kitty = true,
(b'y', b"$") => {
if let Some(state) = answer.parameters.strip_prefix(b"2026;") {
answers.sync = matches!(state, b"1" | b"2" | b"3");
}
if let Some(state) = answer.parameters.strip_prefix(b"1016;") {
answers.pixels = matches!(state, b"1" | b"2" | b"3");
}
}
_ => {}
}
}
None
}
}
#[cfg(unix)]
fn ask_start() -> (Answers, Vec<u8>) {
let query = format!("{ASK_SYNC}{ASK_PIXELS}\x1b[?u\x1b[c");
match ask_terminal(&query, |bytes| Answers::read(bytes).is_some()) {
Some(asked) => {
let answers = match asked.answered {
true => Answers::read(&asked.bytes).unwrap_or_default(),
false => Answers::default(),
};
(answers, strip_answers(&asked.bytes))
}
None => (Answers::default(), Vec::new()),
}
}
#[cfg(any(all(unix, any(feature = "termion", feature = "termwiz")), test))]
fn cursor_report(bytes: &[u8]) -> Option<(u16, std::ops::Range<usize>)> {
let digits = |from: usize| {
from + bytes[from..]
.iter()
.take_while(|b| b.is_ascii_digit())
.count()
};
let mut at = 0;
while let Some(found) = bytes[at..].windows(2).position(|w| w == b"\x1b[") {
let start = at + found;
let row_end = digits(start + 2);
if row_end > start + 2 && bytes.get(row_end) == Some(&b';') {
let end = digits(row_end + 1);
if end > row_end + 1 && bytes.get(end) == Some(&b'R') {
let row = std::str::from_utf8(&bytes[start + 2..row_end]).map(str::parse::<u16>);
if let Ok(Ok(row)) = row {
return Some((row.saturating_sub(1), start..end + 1));
}
}
}
at = start + 1;
}
None
}
#[cfg(all(unix, any(feature = "termion", feature = "termwiz")))]
fn ask_cursor_row() -> (Option<u16>, Vec<u8>) {
let Some(Asked { bytes, .. }) = ask_terminal("\x1b[6n", |bytes| cursor_report(bytes).is_some())
else {
return (None, Vec::new());
};
match cursor_report(&bytes) {
Some((row, at)) => {
let mut rest = bytes[..at.start].to_vec();
rest.extend_from_slice(&bytes[at.end..]);
(Some(row), rest)
}
None => (None, bytes),
}
}
fn cell_size() -> Option<(u16, u16)> {
let size = crossterm::terminal::window_size().ok()?;
cell_of(size.columns, size.rows, size.width, size.height)
}
fn cell_of(columns: u16, rows: u16, width: u16, height: u16) -> Option<(u16, u16)> {
if columns == 0 || rows == 0 || width < columns || height < rows {
return None;
}
Some((width / columns, height / rows))
}
thread_local! {
static CATCHING: Cell<usize> = const { Cell::new(0) };
}
pub fn catch_panic<R>(f: impl FnOnce() -> R + UnwindSafe) -> std::thread::Result<R> {
CATCHING.with(|depth| depth.set(depth.get() + 1));
let result = std::panic::catch_unwind(f);
CATCHING.with(|depth| depth.set(depth.get() - 1));
result
}
fn install_hook() {
HOOK.call_once(|| {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if CATCHING.with(Cell::get) > 0 && ACTIVE.load(Ordering::SeqCst) != 0 {
return;
}
let _ = restore(false);
previous(info);
}));
});
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SessionOptions {
pub alternate_screen: bool,
pub mouse: bool,
pub bracketed_paste: bool,
pub output: Output,
pub legacy_keys: bool,
pub backend: BackendKind,
pub synchronized_output: Option<bool>,
pub all_keys: bool,
}
#[derive(Clone)]
pub struct Waker(std::sync::Arc<WakeState>);
struct WakeState {
woken: std::sync::Mutex<bool>,
changed: std::sync::Condvar,
#[cfg(unix)]
pipe: Option<(
std::os::unix::net::UnixStream,
std::os::unix::net::UnixStream,
)>,
}
impl Default for Waker {
fn default() -> Waker {
Waker::new()
}
}
impl fmt::Debug for Waker {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Waker")
.field("woken", &*self.lock())
.finish()
}
}
impl Waker {
pub fn new() -> Waker {
#[cfg(unix)]
let pipe = std::os::unix::net::UnixStream::pair()
.ok()
.filter(|(a, b)| a.set_nonblocking(true).is_ok() && b.set_nonblocking(true).is_ok());
Waker(std::sync::Arc::new(WakeState {
woken: std::sync::Mutex::new(false),
changed: std::sync::Condvar::new(),
#[cfg(unix)]
pipe,
}))
}
fn lock(&self) -> std::sync::MutexGuard<'_, bool> {
self.0.woken.lock().unwrap_or_else(|e| e.into_inner())
}
pub fn wake(&self) {
let mut woken = self.lock();
if std::mem::replace(&mut *woken, true) {
return;
}
#[cfg(unix)]
if let Some((_, writer)) = &self.0.pipe {
use std::io::Write as _;
let _ = (&*writer).write(&[1]);
}
self.0.changed.notify_all();
}
pub fn take(&self) -> bool {
let mut woken = self.lock();
#[cfg(unix)]
if let Some((reader, _)) = &self.0.pipe {
use std::io::Read as _;
let mut drained = [0u8; 16];
while matches!((&*reader).read(&mut drained), Ok(n) if n > 0) {}
}
std::mem::replace(&mut *woken, false)
}
pub fn wait(&self, timeout: Duration) -> bool {
{
let woken = self.lock();
let _ = self
.0
.changed
.wait_timeout_while(woken, timeout, |woken| !*woken)
.unwrap_or_else(|e| e.into_inner());
}
self.take()
}
#[cfg(unix)]
pub(crate) fn fd(&self) -> Option<std::os::unix::io::RawFd> {
use std::os::unix::io::AsRawFd;
self.0.pipe.as_ref().map(|(reader, _)| reader.as_raw_fd())
}
}
const WAKE_SLICE: Duration = Duration::from_millis(4);
const KEYS_SLICE: Duration = Duration::from_millis(20);
#[cfg(unix)]
pub(crate) type WakeFd = std::os::unix::io::RawFd;
#[cfg(not(unix))]
pub(crate) type WakeFd = ();
#[cfg(unix)]
fn input_before_wake(
tty: std::os::unix::io::RawFd,
wake: std::os::unix::io::RawFd,
wait: Duration,
) -> io::Result<bool> {
use filedescriptor::{POLLIN, poll, pollfd};
let mut ready = [
pollfd {
fd: tty,
events: POLLIN,
revents: 0,
},
pollfd {
fd: wake,
events: POLLIN,
revents: 0,
},
];
match poll(&mut ready, Some(wait)) {
Ok(_) => Ok(ready[0].revents != 0 && ready[1].revents == 0),
Err(filedescriptor::Error::Poll(e)) if e.kind() == io::ErrorKind::Interrupted => Ok(false),
Err(e) => Err(io::Error::other(e.to_string())),
}
}
pub trait Backend {
fn size(&self) -> (u16, u16);
fn read(&mut self, timeout: Option<Duration>) -> io::Result<Option<Event>>;
fn write(&mut self, text: &str) -> io::Result<()>;
fn synchronized_output(&self) -> bool {
false
}
fn write_frame(&mut self, frame: &str) -> io::Result<()> {
if frame.is_empty() {
Ok(())
} else if self.synchronized_output() {
self.write(&synchronized_update(frame))
} else {
self.write(frame)
}
}
fn elapsed(&self) -> Duration;
fn handoff(&mut self, command: &mut Command) -> io::Result<Option<i32>>;
fn alternate_screen(&self) -> bool;
fn origin(&self) -> u16 {
0
}
fn painted(&mut self, text: &str) {
let _ = text;
}
fn can_suspend(&self) -> bool {
false
}
fn suspend(&mut self) -> io::Result<()> {
Ok(())
}
fn take_resumed(&mut self) -> bool {
false
}
fn clipboard(&self) -> Result<(), String> {
Err("this backend has no clipboard".into())
}
fn copy(&mut self, text: &str) -> io::Result<()> {
let _ = text;
Ok(())
}
fn waker(&mut self) -> Option<Waker> {
None
}
fn releases_reported(&self) -> crate::Releases {
crate::Releases::None
}
}
enum Input {
Crossterm,
#[cfg(all(unix, feature = "termion"))]
Termion(with_termion::Reader),
#[cfg(feature = "termwiz")]
Termwiz {
held: crate::event::HeldButton,
probed: with_termwiz::Probed,
},
}
impl Input {
fn open(kind: BackendKind, output: Output) -> io::Result<Input> {
let _ = output;
Ok(match kind {
BackendKind::Crossterm => Input::Crossterm,
#[cfg(all(unix, feature = "termion"))]
BackendKind::Termion => Input::Termion(with_termion::Reader::open()?),
#[cfg(feature = "termwiz")]
BackendKind::Termwiz => Input::Termwiz {
held: Default::default(),
probed: with_termwiz::open(output)?,
},
})
}
}
pub struct Session {
options: SessionOptions,
start: Instant,
active: bool,
origin: u16,
clipboard: rich_ext::clipboard::Clipboard,
kitty: bool,
sync: bool,
cell: Option<(u16, u16)>,
input: Option<Input>,
waker: Option<Waker>,
typed: std::collections::VecDeque<Event>,
looked: Instant,
#[cfg(unix)]
tty: Option<std::fs::File>,
}
fn answered(ask: impl FnOnce() -> (Answers, Vec<u8>)) -> (Answers, Vec<u8>) {
match ANSWERS.load(Ordering::SeqCst) {
0 => {
let (answers, typed) = if query_reaches_terminal() {
ask()
} else {
(Answers::default(), Vec::new())
};
ANSWERS.store(answers.encode(), Ordering::SeqCst);
(answers, typed)
}
bits => (Answers::decode(bits), Vec::new()),
}
}
fn query_reaches_terminal() -> bool {
use std::io::IsTerminal;
io::stdout().is_terminal()
}
impl Session {
pub fn start(options: SessionOptions) -> io::Result<Session> {
if OWNED
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
return Err(io::Error::new(
io::ErrorKind::ResourceBusy,
"a terminal session is already running",
));
}
install_hook();
watch_signals();
let mut session = Session {
options,
start: Instant::now(),
active: false,
origin: 0,
clipboard: rich_ext::clipboard::Clipboard::detect(
&crate::clipboard::SessionEnvironment,
),
kitty: false,
sync: false,
cell: None,
input: None,
waker: None,
typed: Default::default(),
looked: Instant::now(),
#[cfg(unix)]
tty: None,
};
session.enter()?;
Ok(session)
}
fn enter(&mut self) -> io::Result<()> {
let library = self.options.backend;
ON_STDERR.store(self.options.output == Output::Stderr, Ordering::SeqCst);
LIBRARY.store(library.index(), Ordering::SeqCst);
if self.input.is_none() {
self.input = Some(Input::open(library, self.options.output)?);
}
library.raw_mode(true, true)?;
ACTIVE.fetch_or(RAW, Ordering::SeqCst);
self.active = true;
let mut out = String::new();
if self.options.alternate_screen {
ACTIVE.fetch_or(ALTERNATE, Ordering::SeqCst);
match library.alternate_screen(true, true) {
Some(entered) => entered?,
None => out.push_str(&library.alternate_sequence(true)),
}
out.push_str("\x1b[H");
}
let answers = if self.options.legacy_keys {
Answers::default()
} else {
self.answered()
};
self.kitty = answers.kitty;
if self.kitty {
out.push_str(match (library, self.options.all_keys) {
(BackendKind::Crossterm, true) => PUSH_KITTY_ALL,
_ => library.kitty_push(),
});
}
self.sync = self
.options
.synchronized_output
.or_else(sync_from_env)
.unwrap_or(answers.sync);
if self.sync {
ACTIVE.fetch_or(SYNC, Ordering::SeqCst);
}
let (mouse, paste) = self.capable();
if self.options.mouse && mouse {
out.push_str("\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h");
ACTIVE.fetch_or(MOUSE, Ordering::SeqCst);
self.cell = None;
if answers.pixels
&& pixels_allowed(std::env::var(PIXEL_MOUSE_ENV).ok().as_deref())
&& let Some(cell) = cell_size()
{
out.push_str("\x1b[?1016h");
ACTIVE.fetch_or(PIXELS, Ordering::SeqCst);
self.cell = Some(cell);
}
}
if self.options.bracketed_paste && paste {
out.push_str("\x1b[?2004h");
ACTIVE.fetch_or(PASTE, Ordering::SeqCst);
}
out.push_str("\x1b[?25l");
{
let _suspending = SUSPENDING.lock().unwrap_or_else(|e| e.into_inner());
if self.kitty {
ACTIVE.fetch_or(KITTY, Ordering::SeqCst);
}
self.options.output.write(&out)?;
}
if self.options.mouse
&& !self.options.alternate_screen
&& self.options.output == Output::Stdout
{
self.origin = self.cursor_row().unwrap_or(0);
}
Ok(())
}
fn answered(&mut self) -> Answers {
let (answers, typed) = match self.options.backend {
#[cfg(unix)]
BackendKind::Crossterm => answered(ask_start),
#[cfg(not(unix))]
BackendKind::Crossterm => answered(|| {
let kitty = crossterm::terminal::supports_keyboard_enhancement().unwrap_or(false);
(
Answers {
kitty,
..Answers::default()
},
Vec::new(),
)
}),
#[cfg(all(unix, feature = "termion"))]
BackendKind::Termion => (Answers::default(), Vec::new()),
#[cfg(all(unix, feature = "termwiz"))]
BackendKind::Termwiz => answered(ask_start),
#[cfg(all(not(unix), feature = "termwiz"))]
BackendKind::Termwiz => (Answers::default(), Vec::new()),
};
#[cfg(unix)]
if !typed.is_empty() {
match &mut self.input {
#[cfg(feature = "termwiz")]
Some(Input::Termwiz { probed, .. }) => with_termwiz::unread(probed, &typed),
_ => self.typed.extend(crate::event::from_legacy_bytes(&typed)),
}
}
#[cfg(not(unix))]
debug_assert!(typed.is_empty());
answers
}
fn capable(&self) -> (bool, bool) {
match &self.input {
#[cfg(feature = "termwiz")]
Some(Input::Termwiz { probed, .. }) => (probed.mouse, probed.paste),
_ => (true, true),
}
}
fn cursor_row(&mut self) -> Option<u16> {
match self.options.backend {
BackendKind::Crossterm => crossterm::cursor::position().ok().map(|(_, row)| row),
#[cfg(all(unix, any(feature = "termion", feature = "termwiz")))]
#[allow(unreachable_patterns)]
_ => {
let (row, rest) = ask_cursor_row();
match &mut self.input {
#[cfg(feature = "termion")]
Some(Input::Termion(reader)) => reader.unread(&rest),
#[cfg(feature = "termwiz")]
Some(Input::Termwiz { probed, .. }) => with_termwiz::unread(probed, &rest),
_ => {}
}
row
}
#[cfg(all(not(unix), feature = "termwiz"))]
_ => None,
}
}
fn measure(&self) -> io::Result<(u16, u16)> {
match &self.input {
#[cfg(all(unix, feature = "termion"))]
Some(Input::Termion(reader)) => reader.size(),
#[cfg(feature = "termwiz")]
Some(Input::Termwiz { .. }) => with_termwiz::size(),
_ => crossterm::terminal::size(),
}
}
fn in_cells(&mut self, event: Event) -> Event {
let Some(cell) = self.cell else {
return event;
};
match event {
Event::Mouse(mouse) => match mouse.reread_as_pixels(cell) {
Some(read) => Event::Mouse(read),
None => Event::Mouse(mouse),
},
Event::Resize { .. } => {
if let Some(size) = cell_size() {
self.cell = Some(size);
}
event
}
event => event,
}
}
fn next_event(&mut self, wait: Duration) -> io::Result<Option<Event>> {
let kitty = self.kitty;
#[cfg(unix)]
let wake: Option<WakeFd> = self.waker.as_ref().and_then(Waker::fd);
#[cfg(not(unix))]
let wake: Option<WakeFd> = None;
match &mut self.input {
#[cfg(all(unix, feature = "termion"))]
Some(Input::Termion(reader)) => reader.next(wait, wake),
#[cfg(feature = "termwiz")]
Some(Input::Termwiz { held, probed }) => Ok(with_termwiz::poll(probed, wait, wake)?
.and_then(|event| {
if kitty {
crate::event::from_termwiz_kitty(event, held)
} else {
crate::event::from_termwiz(event, held)
}
})),
_ => {
#[cfg(unix)]
if let Some(wake) = wake {
if !crossterm::event::poll(Duration::from_millis(1))? {
let tty = self.crossterm_tty()?;
if !input_before_wake(tty, wake, wait)? {
return Ok(None);
}
if !crossterm::event::poll(wait.min(Duration::from_millis(10)))? {
return Ok(None);
}
}
} else if !crossterm::event::poll(wait)? {
return Ok(None);
}
#[cfg(not(unix))]
{
let _ = wake;
if !crossterm::event::poll(wait)? {
return Ok(None);
}
}
let event = crossterm::event::read()?;
Ok(if kitty {
from_crossterm_kitty(event)
} else {
from_crossterm(event)
})
}
}
}
#[cfg(unix)]
fn crossterm_tty(&mut self) -> io::Result<std::os::unix::io::RawFd> {
use std::io::IsTerminal;
use std::os::unix::io::AsRawFd;
if io::stdin().is_terminal() {
return Ok(io::stdin().as_raw_fd());
}
if self.tty.is_none() {
self.tty = Some(std::fs::File::open("/dev/tty")?);
}
Ok(self.tty.as_ref().map_or(0, |tty| tty.as_raw_fd()))
}
pub fn leave(&mut self) -> io::Result<()> {
if !std::mem::take(&mut self.active) {
return Ok(());
}
let _suspending = SUSPENDING.lock().unwrap_or_else(|e| e.into_inner());
restore(true)
}
pub fn options(&self) -> SessionOptions {
self.options
}
pub fn kitty_keys(&self) -> bool {
self.kitty
}
pub fn releases_reported(&self) -> crate::Releases {
use crate::Releases;
match self.options.backend {
BackendKind::Crossterm if cfg!(windows) => Releases::All,
BackendKind::Crossterm if self.kitty && self.options.all_keys => Releases::All,
BackendKind::Crossterm if self.kitty => Releases::ExceptText,
#[allow(unreachable_patterns)]
_ => Releases::None,
}
}
pub fn synchronized_output(&self) -> bool {
self.sync
}
pub fn color_system(&self) -> Option<Option<rich::ColorSystem>> {
match &self.input {
#[cfg(feature = "termwiz")]
Some(Input::Termwiz { probed, .. }) => Some(probed.color),
_ => None,
}
}
fn repaint(&mut self) -> Event {
if self.options.mouse
&& !self.options.alternate_screen
&& self.options.output == Output::Stdout
{
self.origin = self.cursor_row().unwrap_or(0);
}
let (columns, rows) = self.measure().unwrap_or((80, 24));
Event::Resize { columns, rows }
}
}
impl Drop for Session {
fn drop(&mut self) {
let _ = self.leave();
if self.input.take().is_some() {
self.options.backend.release();
}
OWNED.store(false, Ordering::SeqCst);
}
}
impl Backend for Session {
fn size(&self) -> (u16, u16) {
self.measure().unwrap_or((80, 24))
}
fn read(&mut self, timeout: Option<Duration>) -> io::Result<Option<Event>> {
if let Some(event) = self.typed.pop_front() {
return Ok(Some(event));
}
let end = timeout.map(|timeout| Instant::now() + timeout);
loop {
if RESUMED.load(Ordering::SeqCst) {
return Ok(Some(self.repaint()));
}
if self.waker.as_ref().is_some_and(Waker::take) {
if self.looked.elapsed() < KEYS_SLICE {
return Ok(None);
}
self.looked = Instant::now();
return Ok(self.next_event(Duration::ZERO)?.map(|e| self.in_cells(e)));
}
let mut wait = match end {
Some(end) => end.saturating_duration_since(Instant::now()),
None => Duration::from_secs(3600),
}
.min(Duration::from_millis(250));
if self.waker.is_some() && !cfg!(unix) {
wait = wait.min(WAKE_SLICE);
}
self.looked = Instant::now();
if let Some(event) = self.next_event(wait)? {
return Ok(Some(self.in_cells(event)));
}
if end.is_some_and(|end| Instant::now() >= end) {
return Ok(None);
}
}
}
fn write(&mut self, text: &str) -> io::Result<()> {
self.options.output.write(text)
}
fn waker(&mut self) -> Option<Waker> {
Some(self.waker.get_or_insert_with(Waker::new).clone())
}
fn releases_reported(&self) -> crate::Releases {
Session::releases_reported(self)
}
fn synchronized_output(&self) -> bool {
self.sync
}
fn clipboard(&self) -> Result<(), String> {
let field = self.clipboard.field();
if field.value {
Ok(())
} else {
Err(field.reason.clone())
}
}
fn copy(&mut self, text: &str) -> io::Result<()> {
let sequence = rich_ext::clipboard::osc52(text)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error.to_string()))?;
self.options.output.write(&sequence)
}
fn elapsed(&self) -> Duration {
self.start.elapsed()
}
fn handoff(&mut self, command: &mut Command) -> io::Result<Option<i32>> {
self.leave()?;
let status = command.status();
self.enter()?;
Ok(status?.code())
}
fn alternate_screen(&self) -> bool {
self.options.alternate_screen
}
fn origin(&self) -> u16 {
self.origin
}
fn can_suspend(&self) -> bool {
cfg!(unix)
}
fn suspend(&mut self) -> io::Result<()> {
#[cfg(unix)]
{
suspend();
self.repaint();
}
Ok(())
}
fn take_resumed(&mut self) -> bool {
RESUMED.swap(false, Ordering::SeqCst)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backends_are_named() {
assert_eq!(BackendKind::default(), BackendKind::Crossterm);
assert_eq!(BackendKind::ALL[0], BackendKind::Crossterm);
for kind in BackendKind::ALL {
assert_eq!(BackendKind::from_name(kind.name()), Some(*kind));
assert_eq!(
BackendKind::from_name(&kind.name().to_uppercase()),
Some(*kind)
);
assert_eq!(kind.to_string(), kind.name());
}
assert_eq!(BackendKind::from_name("curses"), None);
assert_eq!(
BackendKind::ALL.len(),
1 + usize::from(cfg!(all(unix, feature = "termion")))
+ usize::from(cfg!(feature = "termwiz"))
);
}
#[test]
fn every_backend_leaves_the_same_modes() {
let all = RAW | ALTERNATE | MOUSE | PASTE | KITTY;
let expected =
"\x1b[<1u\x1b[?1006l\x1b[?1015l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?2004l\x1b[?25h";
for kind in BackendKind::ALL {
let out = undo(all, *kind);
assert!(out.starts_with(expected), "{kind}: {out:?}");
let rest = &out[expected.len()..];
assert!(rest == "\x1b[?1049l" || rest.is_empty(), "{kind}: {rest:?}");
assert_eq!(rest.is_empty(), kind.owns_alternate_screen());
}
}
#[test]
fn answers_are_stripped_from_what_was_typed() {
assert_eq!(strip_answers(b"\x1b[?2026;2$yq\x1b[?62c"), b"q");
assert_eq!(strip_answers(b"ab\x1b[?62"), b"ab");
assert_eq!(strip_answers(b"\x1b[A\x1b"), b"\x1b[A\x1b");
assert!(strip_answers(b"\x1b[?0u\x1b[?62c").is_empty());
}
#[test]
fn private_answers_are_found() {
let lasts = |bytes: &[u8]| -> Vec<u8> { private_answers(bytes).map(|a| a.last).collect() };
assert_eq!(lasts(b"x\x1b[?0u\x1b[?62;22c"), b"uc");
assert_eq!(lasts(b"\x1b[?62"), b"");
let decrqm: Vec<_> = private_answers(b"\x1b[?2026;2$y").collect();
assert_eq!(
decrqm,
[PrivateAnswer {
parameters: b"2026;2",
intermediates: b"$",
last: b'y',
}]
);
assert_eq!(lasts(b"\x1b[?2026\x1b[?62c"), b"c");
}
#[test]
fn the_start_up_answers_are_read() {
let read = |bytes: &[u8]| Answers::read(bytes);
let both = Answers {
kitty: true,
sync: true,
pixels: false,
};
let sync = Answers {
sync: true,
..Answers::default()
};
assert_eq!(read(b"\x1b[?2026;2$y\x1b[?0u\x1b[?62;22c"), Some(both));
assert_eq!(read(b"\x1b[?2026;1$y\x1b[?62c"), Some(sync));
assert_eq!(read(b"\x1b[?2026;3$y\x1b[?62c"), Some(sync));
assert_eq!(read(b"\x1b[?2026;0$y\x1b[?62c"), Some(Answers::default()));
assert_eq!(read(b"\x1b[?2026;4$y\x1b[?62c"), Some(Answers::default()));
assert_eq!(
read(b"\x1b[?1u\x1b[?62c"),
Some(Answers {
kitty: true,
..Answers::default()
})
);
assert_eq!(read(b"\x1b[?2004;1$y\x1b[?62c"), Some(Answers::default()));
assert_eq!(read(b""), None);
assert_eq!(read(b"\x1b[?2026;2$y\x1b[?0u"), None);
assert_eq!(read(b"\x1b[?2026;2$y\x1b[?6"), None);
assert_eq!(
read(b"j\x1b[A\x1b[?2026;2$yk\x1b[?0u\r\x1b[?62;22c"),
Some(both)
);
let pixels = Answers {
pixels: true,
..Answers::default()
};
let all = Answers {
pixels: true,
..both
};
assert_eq!(read(b"\x1b[?1016;2$y\x1b[?62c"), Some(pixels));
assert_eq!(read(b"\x1b[?1016;1$y\x1b[?62c"), Some(pixels));
assert_eq!(read(b"\x1b[?1016;0$y\x1b[?62c"), Some(Answers::default()));
assert_eq!(read(b"\x1b[?1016;4$y\x1b[?62c"), Some(Answers::default()));
assert_eq!(
read(b"\x1b[?2026;2$y\x1b[?1016;2$y\x1b[?0u\x1b[?62;22c"),
Some(all)
);
assert_eq!(read(b"\x1b[?2026;2$y\x1b[?62c"), Some(sync));
for answers in [Answers::default(), sync, both, pixels, all] {
assert_ne!(answers.encode(), 0);
assert_eq!(Answers::decode(answers.encode()), answers);
}
}
#[test]
fn the_cell_size_comes_from_the_window_size_in_pixels() {
assert_eq!(cell_of(80, 24, 800, 480), Some((10, 20)));
assert_eq!(cell_of(80, 24, 805, 490), Some((10, 20)));
assert_eq!(cell_of(80, 24, 0, 0), None);
assert_eq!(cell_of(0, 0, 800, 480), None);
assert_eq!(cell_of(80, 24, 40, 480), None);
}
#[test]
fn pixel_reports_are_allowed_unless_turned_off() {
assert!(pixels_allowed(None));
assert!(pixels_allowed(Some("1")));
assert!(!pixels_allowed(Some("0")));
assert!(!pixels_allowed(Some(" 0 ")));
}
#[test]
fn the_restore_turns_pixel_reports_off_before_the_mouse() {
for kind in BackendKind::ALL {
let out = undo(RAW | MOUSE | PIXELS, *kind);
assert!(out.starts_with("\x1b[?1016l\x1b[?1006l"), "{kind}: {out:?}");
assert!(!undo(RAW | MOUSE, *kind).contains("1016"));
}
}
#[test]
fn a_cursor_report_is_found_among_other_input() {
assert_eq!(cursor_report(b"\x1b[12;1R"), Some((11, 0..7)));
assert_eq!(cursor_report(b"\x1b[A\x1b[12;1R"), Some((11, 3..10)));
assert_eq!(
cursor_report(b"\x1b[<0;2;13M\x1b[3;40Rx"),
Some((2, 10..17))
);
assert_eq!(cursor_report(b"\x1b[1;\x1b[5;1R"), Some((4, 4..10)));
for bytes in [
&b"\x1b[12;"[..],
b"\x1b[12R",
b"\x1b[;1R",
b"\x1b[1;5C",
b"12;1R",
] {
assert_eq!(cursor_report(bytes), None, "{bytes:?}");
}
}
#[test]
fn the_environment_forces_synchronized_output() {
assert_eq!(sync_override(Some("0")), Some(false));
assert_eq!(sync_override(Some("1")), Some(true));
assert_eq!(sync_override(Some(" 1\n")), Some(true));
assert_eq!(sync_override(None), None);
assert_eq!(sync_override(Some("")), None);
assert_eq!(sync_override(Some("auto")), None);
}
#[test]
fn a_frame_is_one_synchronized_update_and_an_empty_one_nothing() {
assert_eq!(
synchronized_update("\x1b[2;1Hhi\x1b[?25l"),
"\x1b[?2026h\x1b[2;1Hhi\x1b[?25l\x1b[?2026l"
);
assert_eq!(synchronized_update(""), "");
}
#[test]
fn the_restore_ends_a_synchronized_update_first() {
for kind in BackendKind::ALL {
let out = undo(RAW | SYNC | MOUSE, *kind);
assert!(out.starts_with("\x1b[?2026l\x1b[?1006l"), "{kind}: {out:?}");
assert_eq!(out.matches("2026").count(), 1);
assert!(!undo(RAW | MOUSE, *kind).contains("2026"));
}
}
}