use std::io::Write;
const HIDE_CURSOR: &str = "\x1b[?25l";
const SHOW_CURSOR: &str = "\x1b[?25h";
const CLEAR_BELOW: &str = "\x1b[J";
fn cursor_up(n: usize) -> String {
format!("\x1b[{n}A")
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Target {
Stdout,
Stderr,
}
impl Target {
fn write(self, text: &str) {
let mut out: Box<dyn Write> = match self {
Target::Stdout => Box::new(std::io::stdout()),
Target::Stderr => Box::new(std::io::stderr()),
};
let _ = out.write_all(text.as_bytes());
let _ = out.flush();
}
}
pub struct Region {
painted: usize,
target: Target,
}
impl Region {
pub fn new(target: Target) -> Self {
target.write(HIDE_CURSOR);
restore_cursor_on_interrupt(target);
Self { painted: 0, target }
}
pub fn show(&mut self, scrollback: &[String], live: &[String]) {
let mut out = String::new();
if self.painted > 0 {
out.push_str(&cursor_up(self.painted));
}
out.push_str(CLEAR_BELOW);
for line in scrollback {
out.push_str(line);
out.push('\n');
}
for line in live {
out.push_str(line);
out.push('\n');
}
self.painted = live.len();
self.target.write(&out);
}
}
impl Drop for Region {
fn drop(&mut self) {
self.target.write(SHOW_CURSOR);
}
}
fn restore_cursor_on_interrupt(target: Target) {
if !claim_install() {
return;
}
tokio::spawn(async move {
wait_for_interrupt().await;
target.write(SHOW_CURSOR);
std::process::exit(130);
});
}
fn claim_install() -> bool {
static INSTALLED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
!INSTALLED.swap(true, std::sync::atomic::Ordering::Relaxed)
}
#[cfg(unix)]
async fn wait_for_interrupt() {
use tokio::signal::unix::{signal, SignalKind};
let mut term = match signal(SignalKind::terminate()) {
Ok(s) => s,
Err(_) => {
let _ = tokio::signal::ctrl_c().await;
return;
}
};
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
_ = term.recv() => {}
}
}
#[cfg(not(unix))]
async fn wait_for_interrupt() {
let _ = tokio::signal::ctrl_c().await;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_interrupt_handler_is_claimed_once() {
assert!(claim_install(), "the first caller installs");
assert!(!claim_install(), "the second must not");
assert!(!claim_install());
}
#[test]
fn the_cursor_moves_are_what_they_claim() {
assert_eq!(cursor_up(3), "\u{1b}[3A");
assert_eq!(CLEAR_BELOW, "\u{1b}[J");
assert_eq!(HIDE_CURSOR, "\u{1b}[?25l");
assert_eq!(SHOW_CURSOR, "\u{1b}[?25h");
}
}