use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
const ENABLE_MOUSE_CAPTURE: &[u8] = b"\x1b[?1000h\x1b[?1006h";
const DISABLE_MOUSE_CAPTURE: &[u8] = b"\x1b[?1006l\x1b[?1000l";
static CAPTURE_ACTIVE: AtomicBool = AtomicBool::new(false);
pub(crate) enum MouseSink {
Stdout,
#[cfg(test)]
Shared(std::sync::Arc<std::sync::Mutex<Vec<u8>>>),
}
impl MouseSink {
fn emit(&self, bytes: &[u8]) {
match self {
Self::Stdout => {
let mut out = std::io::stdout();
let _ = out.write_all(bytes);
let _ = out.flush();
}
#[cfg(test)]
Self::Shared(buf) => {
let mut guard = buf.lock().unwrap_or_else(|p| p.into_inner());
guard.extend_from_slice(bytes);
}
}
}
}
pub(crate) struct MouseCaptureGuard {
sink: MouseSink,
}
impl MouseCaptureGuard {
pub(crate) fn enable(sink: MouseSink) -> Self {
sink.emit(ENABLE_MOUSE_CAPTURE);
CAPTURE_ACTIVE.store(true, Ordering::SeqCst);
Self { sink }
}
pub(crate) fn maybe(on: bool) -> Option<Self> {
on.then(|| Self::enable(MouseSink::Stdout))
}
}
impl Drop for MouseCaptureGuard {
fn drop(&mut self) {
self.sink.emit(DISABLE_MOUSE_CAPTURE);
CAPTURE_ACTIVE.store(false, Ordering::SeqCst);
}
}
pub fn install_panic_release_hook() {
use std::sync::Once;
static ONCE: Once = Once::new();
ONCE.call_once(|| {
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
prev(info);
}));
});
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
fn shared() -> (MouseSink, Arc<Mutex<Vec<u8>>>) {
let buf = Arc::new(Mutex::new(Vec::new()));
(MouseSink::Shared(buf.clone()), buf)
}
#[serial_test::serial(mouse_capture_active)]
#[test]
fn guard_enables_on_construct_and_disables_on_drop() {
let (sink, buf) = shared();
{
let _guard = MouseCaptureGuard::enable(sink);
let enabled = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
assert!(
enabled.contains("\u{1b}[?1000h"),
"enable turns on button reporting: {enabled:?}"
);
assert!(
enabled.contains("\u{1b}[?1006h"),
"enable turns on SGR ext: {enabled:?}"
);
assert!(
!enabled.contains("\u{1b}[?1002") && !enabled.contains("\u{1b}[?1003"),
"no drag / any-motion tracking: {enabled:?}"
);
assert!(!enabled.contains("\u{1b}[?1006l"), "not yet disabled");
}
let after = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
assert!(
after.contains("\u{1b}[?1006l") && after.contains("\u{1b}[?1000l"),
"drop emitted disable: {after:?}"
);
}
#[test]
fn maybe_false_emits_nothing() {
assert!(MouseCaptureGuard::maybe(false).is_none());
}
#[serial_test::serial(mouse_capture_active)]
#[test]
fn guard_release_survives_a_panic_unwind() {
let (sink, buf) = shared();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _guard = MouseCaptureGuard::enable(sink);
panic!("turn blew up mid-frame");
}));
assert!(result.is_err(), "panic propagated");
let after = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
assert!(
after.contains("\u{1b}[?1006l"),
"capture released during unwind: {after:?}"
);
}
#[serial_test::serial(mouse_capture_active)]
#[test]
fn caught_sink_panic_does_not_release_capture_or_clear_flag() {
crate::install_panic_release_hook();
let (sink, buf) = shared();
let guard = MouseCaptureGuard::enable(sink);
assert!(
CAPTURE_ACTIVE.load(Ordering::SeqCst),
"capture flag set while the turn's guard is alive"
);
let recovered = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
panic!("sink.finish() blew up (rule-7 teardown-miss)");
}));
assert!(recovered.is_err(), "the sink panic was caught & recovered");
let mid = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
assert!(
!mid.contains("\u{1b}[?1006l") && !mid.contains("\u{1b}[?1000l"),
"no disable emitted while recovering mid-turn: {mid:?}"
);
assert!(
CAPTURE_ACTIVE.load(Ordering::SeqCst),
"capture flag stays set after a recovered panic"
);
drop(guard);
let after = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
assert!(
after.contains("\u{1b}[?1006l"),
"guard Drop releases on scope exit: {after:?}"
);
assert!(
!CAPTURE_ACTIVE.load(Ordering::SeqCst),
"flag cleared once the guard drops"
);
}
}