use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU16, Ordering};
use vte::{Params, Perform};
pub trait DirectInputTracker: Send + Sync {
fn requires_direct_input(&self) -> bool;
}
impl DirectInputTracker for PtyStateTracker {
fn requires_direct_input(&self) -> bool {
self.requires_app_routing()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MouseTrackingMode {
None,
X11Normal,
CellMotion,
AllMotion,
}
impl MouseTrackingMode {
fn from_u8(v: u8) -> Self {
match v {
1 => Self::X11Normal,
2 => Self::CellMotion,
3 => Self::AllMotion,
_ => Self::None,
}
}
#[expect(dead_code)]
fn to_u8(self) -> u8 {
match self {
Self::None => 0,
Self::X11Normal => 1,
Self::CellMotion => 2,
Self::AllMotion => 3,
}
}
}
#[derive(Debug)]
pub struct PtyStateTracker {
is_alt_screen_active: AtomicBool,
is_application_cursor_keys_active: AtomicBool,
mouse_tracking_mode: AtomicU8,
is_sgr_mouse_active: AtomicBool,
is_alt_scroll_mode_active: AtomicBool,
has_custom_margins: AtomicBool,
terminal_height: AtomicU16,
}
impl PtyStateTracker {
pub fn new(terminal_height: u16) -> Self {
Self {
is_alt_screen_active: AtomicBool::new(false),
is_application_cursor_keys_active: AtomicBool::new(false),
mouse_tracking_mode: AtomicU8::new(0),
is_sgr_mouse_active: AtomicBool::new(false),
is_alt_scroll_mode_active: AtomicBool::new(false),
has_custom_margins: AtomicBool::new(false),
terminal_height: AtomicU16::new(terminal_height),
}
}
pub fn requires_app_routing(&self) -> bool {
self.is_alt_screen_active.load(Ordering::Acquire)
|| self.mouse_tracking_mode.load(Ordering::Acquire) != 0
|| self.has_custom_margins.load(Ordering::Acquire)
}
pub fn is_alt_screen_active(&self) -> bool {
self.is_alt_screen_active.load(Ordering::Acquire)
}
pub fn is_application_cursor_keys_active(&self) -> bool {
self.is_application_cursor_keys_active
.load(Ordering::Acquire)
}
pub fn mouse_tracking_mode(&self) -> MouseTrackingMode {
MouseTrackingMode::from_u8(self.mouse_tracking_mode.load(Ordering::Acquire))
}
pub fn is_sgr_mouse_active(&self) -> bool {
self.is_sgr_mouse_active.load(Ordering::Acquire)
}
pub fn is_alt_scroll_mode_active(&self) -> bool {
self.is_alt_scroll_mode_active.load(Ordering::Acquire)
}
pub fn has_custom_margins(&self) -> bool {
self.has_custom_margins.load(Ordering::Acquire)
}
pub fn resize(&self, height: u16) {
self.terminal_height.store(height, Ordering::Release);
}
pub(crate) fn set_alt_screen(&self, active: bool) {
self.is_alt_screen_active.store(active, Ordering::Release);
}
pub(crate) fn set_application_cursor_keys(&self, active: bool) {
self.is_application_cursor_keys_active
.store(active, Ordering::Release);
}
pub(crate) fn set_sgr_mouse(&self, active: bool) {
self.is_sgr_mouse_active.store(active, Ordering::Release);
}
pub(crate) fn set_alt_scroll_mode(&self, active: bool) {
self.is_alt_scroll_mode_active
.store(active, Ordering::Release);
}
pub(crate) fn set_custom_margins(&self, active: bool) {
self.has_custom_margins.store(active, Ordering::Release);
}
pub(crate) fn update_mouse_tracking(&self, target_mode: u8, is_set: bool) {
if is_set {
self.mouse_tracking_mode
.store(target_mode, Ordering::Release);
} else {
let _ = self.mouse_tracking_mode.compare_exchange(
target_mode,
0,
Ordering::AcqRel,
Ordering::Relaxed,
);
}
}
pub(crate) fn reset_all(&self) {
self.is_alt_screen_active.store(false, Ordering::Release);
self.is_application_cursor_keys_active
.store(false, Ordering::Release);
self.mouse_tracking_mode.store(0, Ordering::Release);
self.is_sgr_mouse_active.store(false, Ordering::Release);
self.is_alt_scroll_mode_active
.store(false, Ordering::Release);
self.has_custom_margins.store(false, Ordering::Release);
}
}
pub(crate) struct PtyPerformAdapter {
tracker: std::sync::Arc<PtyStateTracker>,
}
impl PtyPerformAdapter {
pub fn new(tracker: std::sync::Arc<PtyStateTracker>) -> Self {
Self { tracker }
}
}
impl Perform for PtyPerformAdapter {
fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) {
if ignore {
return;
}
if intermediates.is_empty() && byte == b'c' {
self.tracker.reset_all();
}
}
fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) {
if ignore {
return;
}
if action == 'p' && intermediates == *b"!" {
self.tracker.reset_all();
return;
}
let is_dec_private = intermediates.first() == Some(&b'?');
match action {
'h' | 'l' if is_dec_private => {
let is_set = action == 'h';
for param_group in params.iter() {
for ¶m in param_group {
match param {
1 => self.tracker.set_application_cursor_keys(is_set),
47 | 1047 | 1049 => self.tracker.set_alt_screen(is_set),
1000 => self.tracker.update_mouse_tracking(1, is_set),
1002 => self.tracker.update_mouse_tracking(2, is_set),
1003 => self.tracker.update_mouse_tracking(3, is_set),
1006 => self.tracker.set_sgr_mouse(is_set),
1007 => self.tracker.set_alt_scroll_mode(is_set),
_ => {}
}
}
}
}
'r' => {
let top_param = params
.iter()
.next()
.and_then(|g| g.first().copied())
.unwrap_or(0);
let bottom_param = params
.iter()
.nth(1)
.and_then(|g| g.first().copied())
.unwrap_or(0);
let height = self.tracker.terminal_height.load(Ordering::Acquire);
let top = if top_param == 0 { 1 } else { top_param };
let bottom = if bottom_param == 0 {
height
} else {
bottom_param
};
let has_margins = top > 1 || bottom < height;
self.tracker.set_custom_margins(has_margins);
}
_ => {}
}
}
fn print(&mut self, _c: char) {}
fn execute(&mut self, _byte: u8) {}
fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _action: char) {}
fn put(&mut self, _byte: u8) {}
fn unhook(&mut self) {}
fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_tracker(height: u16) -> PtyStateTracker {
PtyStateTracker::new(height)
}
fn feed(tracker: &std::sync::Arc<PtyStateTracker>, bytes: &[u8]) {
let mut parser = vte::Parser::new();
let mut adapter = PtyPerformAdapter::new(tracker.clone());
parser.advance(&mut adapter, bytes);
}
#[test]
fn test_detects_alternate_screen() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1049h");
assert!(tracker.is_alt_screen_active());
assert!(tracker.requires_app_routing());
}
#[test]
fn test_detects_alternate_screen_exit() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1049h");
assert!(tracker.is_alt_screen_active());
feed(&tracker, b"\x1b[?1049l");
assert!(!tracker.is_alt_screen_active());
assert!(!tracker.requires_app_routing());
}
#[test]
fn test_detects_alternate_screen_1047() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1047h");
assert!(tracker.is_alt_screen_active());
}
#[test]
fn test_detects_alternate_screen_47() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?47h");
assert!(tracker.is_alt_screen_active());
}
#[test]
fn test_mouse_tracking_x11_normal() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1000h");
assert_eq!(tracker.mouse_tracking_mode(), MouseTrackingMode::X11Normal);
assert!(tracker.requires_app_routing());
}
#[test]
fn test_mouse_tracking_cell_motion() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1002h");
assert_eq!(tracker.mouse_tracking_mode(), MouseTrackingMode::CellMotion);
}
#[test]
fn test_mouse_tracking_all_motion() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1003h");
assert_eq!(tracker.mouse_tracking_mode(), MouseTrackingMode::AllMotion);
}
#[test]
fn test_mouse_tracking_cas_does_not_clobber() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1002h");
assert_eq!(tracker.mouse_tracking_mode(), MouseTrackingMode::CellMotion);
feed(&tracker, b"\x1b[?1001l");
assert_eq!(
tracker.mouse_tracking_mode(),
MouseTrackingMode::CellMotion,
"CAS must not clear mode 2 when resetting mode 1"
);
feed(&tracker, b"\x1b[?1002l");
assert_eq!(tracker.mouse_tracking_mode(), MouseTrackingMode::None);
}
#[test]
fn test_mouse_tracking_set_after_reset_different_mode() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1000h");
assert_eq!(tracker.mouse_tracking_mode(), MouseTrackingMode::X11Normal);
feed(&tracker, b"\x1b[?1003h");
assert_eq!(tracker.mouse_tracking_mode(), MouseTrackingMode::AllMotion);
feed(&tracker, b"\x1b[?1001l");
assert_eq!(
tracker.mouse_tracking_mode(),
MouseTrackingMode::AllMotion,
"CAS must not clear mode 3 when resetting mode 1"
);
}
#[test]
fn test_sgr_mouse() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1006h");
assert!(tracker.is_sgr_mouse_active());
feed(&tracker, b"\x1b[?1006l");
assert!(!tracker.is_sgr_mouse_active());
}
#[test]
fn test_alt_scroll_mode() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1007h");
assert!(tracker.is_alt_scroll_mode_active());
feed(&tracker, b"\x1b[?1007l");
assert!(!tracker.is_alt_scroll_mode_active());
}
#[test]
fn test_custom_margins_set() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[2;23r");
assert!(tracker.has_custom_margins());
}
#[test]
fn test_custom_margins_cleared() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[2;23r");
assert!(tracker.has_custom_margins());
feed(&tracker, b"\x1b[r");
assert!(!tracker.has_custom_margins());
}
#[test]
fn test_custom_margins_full_height_no_margins() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[1;24r");
assert!(!tracker.has_custom_margins());
}
#[test]
fn test_custom_margins_zero_bottom_param_normalized() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[1;0r");
assert!(!tracker.has_custom_margins());
}
#[test]
fn test_custom_margins_zero_top_param_normalized() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[;24r");
assert!(!tracker.has_custom_margins());
}
#[test]
fn test_resize_updates_height() {
let tracker = std::sync::Arc::new(make_tracker(24));
assert!(!tracker.requires_app_routing());
feed(&tracker, b"\x1b[2;23r");
assert!(tracker.has_custom_margins());
tracker.resize(10);
assert!(tracker.has_custom_margins());
feed(&tracker, b"\x1b[r");
assert!(
!tracker.has_custom_margins(),
"full-height reset at new size"
);
}
#[test]
fn test_ris_reset() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1049h\x1b[?1002h\x1b[2;23r");
assert!(tracker.requires_app_routing());
feed(&tracker, b"\x1bc");
assert!(!tracker.is_alt_screen_active());
assert_eq!(tracker.mouse_tracking_mode(), MouseTrackingMode::None);
assert!(!tracker.has_custom_margins());
assert!(!tracker.requires_app_routing());
}
#[test]
fn test_decstr_reset() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1049h\x1b[?1002h\x1b[2;23r");
assert!(tracker.requires_app_routing());
feed(&tracker, b"\x1b[!p");
assert!(!tracker.is_alt_screen_active());
assert_eq!(tracker.mouse_tracking_mode(), MouseTrackingMode::None);
assert!(!tracker.has_custom_margins());
assert!(!tracker.requires_app_routing());
}
#[test]
fn test_requires_app_routing_false_by_default() {
let tracker = std::sync::Arc::new(make_tracker(24));
assert!(!tracker.requires_app_routing());
}
#[test]
fn test_requires_app_routing_true_for_alt_screen() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1049h");
assert!(tracker.requires_app_routing());
}
#[test]
fn test_requires_app_routing_true_for_mouse_tracking() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1002h");
assert!(tracker.requires_app_routing());
}
#[test]
fn test_requires_app_routing_true_for_margins() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[2;23r");
assert!(tracker.requires_app_routing());
}
#[test]
fn test_application_cursor_keys_enabled() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1h");
assert!(tracker.is_application_cursor_keys_active());
}
#[test]
fn test_application_cursor_keys_disabled() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1h");
assert!(tracker.is_application_cursor_keys_active());
feed(&tracker, b"\x1b[?1l");
assert!(!tracker.is_application_cursor_keys_active());
}
#[test]
fn test_application_cursor_keys_reset_by_ris() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1h\x1b[?1049h");
assert!(tracker.is_application_cursor_keys_active());
feed(&tracker, b"\x1bc");
assert!(!tracker.is_application_cursor_keys_active());
}
#[test]
fn test_application_cursor_keys_reset_by_decstr() {
let tracker = std::sync::Arc::new(make_tracker(24));
feed(&tracker, b"\x1b[?1h");
assert!(tracker.is_application_cursor_keys_active());
feed(&tracker, b"\x1b[!p");
assert!(!tracker.is_application_cursor_keys_active());
}
}