rmux-client 0.1.1

Blocking local client and attach-mode plumbing for the RMUX terminal multiplexer.
Documentation
use std::cell::RefCell;
use std::collections::{BTreeMap, VecDeque};
use std::io;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::rc::Rc;

use super::*;

const INPUT_HANDLE: u8 = 1;
const OUTPUT_HANDLE: u8 = 2;
const PRESERVED_INPUT_FLAG: u32 = 0x0100_0000;
const PRESERVED_OUTPUT_FLAG: u32 = 0x0200_0000;

#[test]
fn enter_applies_raw_input_and_vt_output_flags() -> Result<()> {
    let input_original =
        ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT | PRESERVED_INPUT_FLAG;
    let output_original = PRESERVED_OUTPUT_FLAG;
    let console = FakeConsole::new(Some(input_original), Some(output_original));

    let _guard = RawTerminalGuard::enter(console.clone())?;

    let expected_input = raw_input_mode(input_original);
    let expected_output = raw_output_mode(output_original);

    assert_eq!(console.mode(INPUT_HANDLE), Some(expected_input));
    assert_eq!(console.mode(OUTPUT_HANDLE), Some(expected_output));
    assert_eq!(
        console.set_calls(),
        vec![
            (INPUT_HANDLE, expected_input),
            (OUTPUT_HANDLE, expected_output)
        ]
    );
    Ok(())
}

#[test]
fn raw_modes_disable_console_host_interceptors() {
    let input_original = ENABLE_LINE_INPUT
        | ENABLE_ECHO_INPUT
        | ENABLE_PROCESSED_INPUT
        | ENABLE_QUICK_EDIT_MODE
        | ENABLE_INSERT_MODE
        | PRESERVED_INPUT_FLAG;
    let input_mode = raw_input_mode(input_original);

    assert_ne!(input_mode & ENABLE_EXTENDED_FLAGS, 0);
    assert_ne!(input_mode & ENABLE_VIRTUAL_TERMINAL_INPUT, 0);
    assert_eq!(input_mode & ENABLE_QUICK_EDIT_MODE, 0);
    assert_eq!(input_mode & ENABLE_INSERT_MODE, 0);
    assert_eq!(input_mode & ENABLE_LINE_INPUT, 0);
    assert_eq!(input_mode & ENABLE_ECHO_INPUT, 0);
    assert_eq!(input_mode & ENABLE_PROCESSED_INPUT, 0);
    assert_ne!(input_mode & PRESERVED_INPUT_FLAG, 0);

    let output_mode = raw_output_mode(PRESERVED_OUTPUT_FLAG);
    assert_ne!(output_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING, 0);
    assert_ne!(output_mode & DISABLE_NEWLINE_AUTO_RETURN, 0);
    assert_ne!(output_mode & PRESERVED_OUTPUT_FLAG, 0);
}

#[test]
fn console_control_handler_restores_for_process_exit_events() {
    assert!(should_restore_for_console_event(CTRL_C_EVENT));
    assert!(should_restore_for_console_event(CTRL_BREAK_EVENT));
    assert!(should_restore_for_console_event(CTRL_CLOSE_EVENT));
    assert!(should_restore_for_console_event(CTRL_LOGOFF_EVENT));
    assert!(should_restore_for_console_event(CTRL_SHUTDOWN_EVENT));
    assert!(!should_restore_for_console_event(u32::MAX));
}

#[test]
fn explicit_restore_and_drop_restore_original_modes() -> Result<()> {
    let input_original = ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT;
    let output_original = PRESERVED_OUTPUT_FLAG;
    let console = FakeConsole::new(Some(input_original), Some(output_original));

    {
        let guard = RawTerminalGuard::enter(console.clone())?;
        guard.restore()?;
        guard.restore()?;
        assert_eq!(console.mode(INPUT_HANDLE), Some(input_original));
        assert_eq!(console.mode(OUTPUT_HANDLE), Some(output_original));
    }

    assert_eq!(console.mode(INPUT_HANDLE), Some(input_original));
    assert_eq!(console.mode(OUTPUT_HANDLE), Some(output_original));
    Ok(())
}

#[test]
fn reapply_raw_mode_restores_raw_flags_after_explicit_restore() -> Result<()> {
    let input_original =
        ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT | PRESERVED_INPUT_FLAG;
    let output_original = PRESERVED_OUTPUT_FLAG;
    let console = FakeConsole::new(Some(input_original), Some(output_original));
    let guard = RawTerminalGuard::enter(console.clone())?;

    guard.restore()?;
    guard.reapply_raw_mode()?;

    assert_eq!(
        console.mode(INPUT_HANDLE),
        Some(raw_input_mode(input_original))
    );
    assert_eq!(
        console.mode(OUTPUT_HANDLE),
        Some(raw_output_mode(output_original))
    );
    Ok(())
}

#[test]
fn drop_restores_original_modes_after_panic() {
    let input_original = ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT;
    let output_original = PRESERVED_OUTPUT_FLAG;
    let console = FakeConsole::new(Some(input_original), Some(output_original));

    let panic_result = catch_unwind(AssertUnwindSafe(|| {
        let _guard = RawTerminalGuard::enter(console.clone()).expect("enter raw mode");
        panic!("intentional panic while raw mode is active");
    }));

    assert!(panic_result.is_err());
    assert_eq!(console.mode(INPUT_HANDLE), Some(input_original));
    assert_eq!(console.mode(OUTPUT_HANDLE), Some(output_original));
}

#[test]
fn enter_failure_rolls_back_already_changed_modes() {
    let input_original = ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT;
    let output_original = PRESERVED_OUTPUT_FLAG;
    let console = FakeConsole::new(Some(input_original), Some(output_original));
    console.fail_next_set_for(OUTPUT_HANDLE);

    let error = RawTerminalGuard::enter(console.clone()).expect_err("output set should fail");

    assert!(matches!(error, AttachError::Io(_)));
    assert_eq!(console.mode(INPUT_HANDLE), Some(input_original));
    assert_eq!(console.mode(OUTPUT_HANDLE), Some(output_original));
}

#[test]
fn flush_pending_input_uses_input_handle_when_present() -> Result<()> {
    let console = FakeConsole::new(Some(ENABLE_LINE_INPUT), None);
    let guard = RawTerminalGuard::enter(console.clone())?;

    guard.flush_pending_input()?;

    assert_eq!(console.flushed_handles(), vec![INPUT_HANDLE]);
    Ok(())
}

#[test]
fn flush_pending_input_ignores_redirected_input() -> Result<()> {
    let console = FakeConsole::new(None, Some(PRESERVED_OUTPUT_FLAG));
    let guard = RawTerminalGuard::enter(console.clone())?;

    guard.flush_pending_input()?;

    assert!(console.flushed_handles().is_empty());
    Ok(())
}

#[test]
fn resize_deduper_reports_only_real_size_changes() {
    let initial = Some(TerminalSize { cols: 80, rows: 24 });
    let mut deduper = ResizeDeduper::new(initial);

    assert_eq!(deduper.observe(initial), None);
    assert_eq!(deduper.observe(None), None);
    assert_eq!(
        deduper.observe(Some(TerminalSize {
            cols: 100,
            rows: 30
        })),
        Some(TerminalSize {
            cols: 100,
            rows: 30
        })
    );
    assert_eq!(
        deduper.observe(Some(TerminalSize {
            cols: 100,
            rows: 30
        })),
        None
    );
}

#[derive(Clone, Debug)]
struct FakeConsole {
    state: Rc<RefCell<FakeConsoleState>>,
}

impl FakeConsole {
    fn new(input_mode: Option<u32>, output_mode: Option<u32>) -> Self {
        let mut std_handles = BTreeMap::new();
        let mut modes = BTreeMap::new();
        if let Some(mode) = input_mode {
            std_handles.insert(STD_INPUT_HANDLE, INPUT_HANDLE);
            modes.insert(INPUT_HANDLE, mode);
        }
        if let Some(mode) = output_mode {
            std_handles.insert(STD_OUTPUT_HANDLE, OUTPUT_HANDLE);
            modes.insert(OUTPUT_HANDLE, mode);
        }

        Self {
            state: Rc::new(RefCell::new(FakeConsoleState {
                std_handles,
                modes,
                set_calls: Vec::new(),
                flushed_handles: Vec::new(),
                failing_set_handles: VecDeque::new(),
            })),
        }
    }

    fn mode(&self, handle: u8) -> Option<u32> {
        self.state.borrow().modes.get(&handle).copied()
    }

    fn set_calls(&self) -> Vec<(u8, u32)> {
        self.state.borrow().set_calls.clone()
    }

    fn flushed_handles(&self) -> Vec<u8> {
        self.state.borrow().flushed_handles.clone()
    }

    fn fail_next_set_for(&self, handle: u8) {
        self.state
            .borrow_mut()
            .failing_set_handles
            .push_back(handle);
    }
}

#[derive(Debug)]
struct FakeConsoleState {
    std_handles: BTreeMap<u32, u8>,
    modes: BTreeMap<u8, u32>,
    set_calls: Vec<(u8, u32)>,
    flushed_handles: Vec<u8>,
    failing_set_handles: VecDeque<u8>,
}

impl ConsoleApi for FakeConsole {
    type Handle = u8;

    fn std_handle(&self, handle_id: u32) -> Result<Option<Self::Handle>> {
        Ok(self.state.borrow().std_handles.get(&handle_id).copied())
    }

    fn get_console_mode(&self, handle: Self::Handle) -> Result<Option<u32>> {
        Ok(self.state.borrow().modes.get(&handle).copied())
    }

    fn set_console_mode(&self, handle: Self::Handle, mode: u32) -> Result<()> {
        let mut state = self.state.borrow_mut();
        if state.failing_set_handles.front() == Some(&handle) {
            state.failing_set_handles.pop_front();
            return Err(AttachError::Io(io::Error::other(
                "injected console mode failure",
            )));
        }
        state.modes.insert(handle, mode);
        state.set_calls.push((handle, mode));
        Ok(())
    }

    fn flush_console_input(&self, handle: Self::Handle) -> Result<()> {
        self.state.borrow_mut().flushed_handles.push(handle);
        Ok(())
    }
}