use std::cell::Cell;
use std::io::{self, Write};
use std::panic::UnwindSafe;
use std::process::Command;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::Once;
use std::time::{Duration, Instant};
use crate::event::{from_crossterm, Event};
const RAW: u8 = 1;
const ALTERNATE: u8 = 2;
const MOUSE: u8 = 4;
const PASTE: u8 = 8;
static ACTIVE: AtomicU8 = AtomicU8::new(0);
static HOOK: Once = Once::new();
static OWNED: AtomicBool = AtomicBool::new(false);
static ON_STDERR: AtomicBool = AtomicBool::new(false);
#[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()
}
}
}
}
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() -> io::Result<()> {
let active = ACTIVE.swap(0, Ordering::SeqCst);
if active == 0 {
return Ok(());
}
let out = undo(active);
let output = if ON_STDERR.load(Ordering::SeqCst) {
Output::Stderr
} else {
Output::Stdout
};
let written = output.write(&out);
if active & RAW != 0 {
crossterm::terminal::disable_raw_mode()?;
}
written
}
fn undo(active: u8) -> String {
let mut out = String::new();
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 {
out.push_str("\x1b[?1049l");
}
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();
let _ = signal_hook::low_level::emulate_default_handler(signal);
}
}
});
});
}
#[cfg(unix)]
fn restore_for_signal() {
let active = ACTIVE.swap(0, Ordering::SeqCst);
if active != 0 {
write_direct(&undo(active));
}
let raw_line = RAW_LINE.swap(false, Ordering::SeqCst);
if active & RAW != 0 || raw_line {
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);
restore_for_signal();
let _ = signal_hook::low_level::emulate_default_handler(signal_hook::consts::SIGTSTP);
if active & RAW != 0 || raw_line {
let _ = crossterm::terminal::enable_raw_mode();
}
RAW_LINE.store(raw_line, Ordering::SeqCst);
let mut out = String::new();
if active & ALTERNATE != 0 {
out.push_str("\x1b[?1049h\x1b[H");
}
if active & MOUSE != 0 {
out.push_str("\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h");
}
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 = if ON_STDERR.load(Ordering::SeqCst) {
Output::Stderr
} else {
Output::Stdout
};
let _ = output.write(text);
}
}
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();
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 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 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(())
}
}
pub struct Session {
options: SessionOptions,
start: Instant,
active: bool,
origin: u16,
clipboard: rich_ext::clipboard::Clipboard,
}
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,
),
};
session.enter()?;
Ok(session)
}
fn enter(&mut self) -> io::Result<()> {
ON_STDERR.store(self.options.output == Output::Stderr, Ordering::SeqCst);
crossterm::terminal::enable_raw_mode()?;
ACTIVE.fetch_or(RAW, Ordering::SeqCst);
self.active = true;
let mut out = String::new();
if self.options.alternate_screen {
out.push_str("\x1b[?1049h\x1b[H");
ACTIVE.fetch_or(ALTERNATE, Ordering::SeqCst);
}
if self.options.mouse {
out.push_str("\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h");
ACTIVE.fetch_or(MOUSE, Ordering::SeqCst);
}
if self.options.bracketed_paste {
out.push_str("\x1b[?2004h");
ACTIVE.fetch_or(PASTE, Ordering::SeqCst);
}
out.push_str("\x1b[?25l");
self.options.output.write(&out)?;
if self.options.mouse
&& !self.options.alternate_screen
&& self.options.output == Output::Stdout
{
self.origin = crossterm::cursor::position().map_or(0, |(_, row)| row);
}
Ok(())
}
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()
}
pub fn options(&self) -> SessionOptions {
self.options
}
fn repaint(&mut self) -> Event {
if self.options.mouse
&& !self.options.alternate_screen
&& self.options.output == Output::Stdout
{
self.origin = crossterm::cursor::position().map_or(0, |(_, row)| row);
}
let (columns, rows) = crossterm::terminal::size().unwrap_or((80, 24));
Event::Resize { columns, rows }
}
}
impl Drop for Session {
fn drop(&mut self) {
let _ = self.leave();
OWNED.store(false, Ordering::SeqCst);
}
}
impl Backend for Session {
fn size(&self) -> (u16, u16) {
crossterm::terminal::size().unwrap_or((80, 24))
}
fn read(&mut self, timeout: Option<Duration>) -> io::Result<Option<Event>> {
let end = timeout.map(|timeout| Instant::now() + timeout);
loop {
if RESUMED.load(Ordering::SeqCst) {
return Ok(Some(self.repaint()));
}
let wait = match end {
Some(end) => end.saturating_duration_since(Instant::now()),
None => Duration::from_secs(3600),
}
.min(Duration::from_millis(250));
if !crossterm::event::poll(wait)? {
if end.is_some_and(|end| Instant::now() >= end) {
return Ok(None);
}
continue;
}
if let Some(event) = from_crossterm(crossterm::event::read()?) {
return Ok(Some(event));
}
}
}
fn write(&mut self, text: &str) -> io::Result<()> {
self.options.output.write(text)
}
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)
}
}