use crate::ssp::NEVER;
use crate::terminal::{TerminalScreen, ECHO_TIMEOUT_MS};
#[derive(Default)]
struct Callbacks {
title: String,
icon: String,
bell_count: u64,
host_replies: Vec<u8>,
}
impl vt100::Callbacks for Callbacks {
fn set_window_title(&mut self, _: &mut vt100::Screen, t: &[u8]) {
self.title = String::from_utf8_lossy(t).into_owned();
}
fn set_window_icon_name(&mut self, _: &mut vt100::Screen, n: &[u8]) {
self.icon = String::from_utf8_lossy(n).into_owned();
}
fn audible_bell(&mut self, _: &mut vt100::Screen) {
self.bell_count += 1;
}
fn unhandled_csi(
&mut self,
screen: &mut vt100::Screen,
i1: Option<u8>,
i2: Option<u8>,
params: &[&[u16]],
c: char,
) {
let p0 = params.first().and_then(|p| p.first()).copied().unwrap_or(0);
match (i1, i2, c) {
(None, _, 'n') => match p0 {
6 => {
let (row, col) = screen.cursor_position();
self.host_replies
.extend_from_slice(format!("\x1b[{};{}R", row + 1, col + 1).as_bytes());
}
5 => self.host_replies.extend_from_slice(b"\x1b[0n"), _ => {}
},
(Some(b'?'), _, 'n') if p0 == 6 => {
let (row, col) = screen.cursor_position();
self.host_replies
.extend_from_slice(format!("\x1b[?{};{}R", row + 1, col + 1).as_bytes());
}
(None, _, 'c') => self.host_replies.extend_from_slice(b"\x1b[?62;1;6c"),
(Some(b'>'), _, 'c') => self.host_replies.extend_from_slice(b"\x1b[>1;10;0c"),
(Some(b'?'), Some(b'$'), 'p') => {
let status = match p0 {
2004 => {
if screen.bracketed_paste() {
1
} else {
2
}
}
_ => 0u16,
};
self.host_replies
.extend_from_slice(format!("\x1b[?{p0};{status}$y").as_bytes());
}
_ => {}
}
}
}
pub struct ServerTerminal {
parser: vt100::Parser<Callbacks>,
scrollback: usize,
echo_ack: u64,
input_history: Vec<(u64, u64)>,
exit_code: Option<u32>,
echo_timeout_ms: u64,
}
impl ServerTerminal {
pub fn new(rows: u16, cols: u16, scrollback: usize) -> Self {
Self {
parser: vt100::Parser::new_with_callbacks(rows, cols, scrollback, Callbacks::default()),
scrollback,
echo_ack: 0,
input_history: Vec::new(),
exit_code: None,
echo_timeout_ms: ECHO_TIMEOUT_MS,
}
}
pub fn set_echo_timeout_ms(&mut self, ms: u64) {
self.echo_timeout_ms = ms;
}
pub fn set_exit_code(&mut self, code: u32) {
self.exit_code = Some(code);
}
pub fn process(&mut self, bytes: &[u8]) {
self.parser.process(bytes);
}
pub fn take_host_replies(&mut self) -> Vec<u8> {
std::mem::take(&mut self.parser.callbacks_mut().host_replies)
}
pub fn resize(&mut self, rows: u16, cols: u16) {
self.parser.screen_mut().set_size(rows, cols);
}
pub fn size(&self) -> (u16, u16) {
self.parser.screen().size()
}
pub fn register_input_frame(&mut self, n: u64, now: u64) {
if self.input_history.last().is_none_or(|(f, _)| n > *f) {
self.input_history.push((n, now));
}
}
pub fn set_echo_ack(&mut self, now: u64) -> bool {
let cutoff = now.saturating_sub(self.echo_timeout_ms);
let mut newest = self.echo_ack;
for &(frame, ts) in &self.input_history {
if ts <= cutoff {
newest = newest.max(frame);
}
}
self.input_history.retain(|&(frame, _)| frame >= newest);
let changed = self.echo_ack != newest;
self.echo_ack = newest;
changed
}
pub fn echo_ack_wait_time(&self, now: u64) -> u64 {
let Some(&(_, arrived)) = self.input_history.get(1) else {
return NEVER;
};
let fire_at = arrived + self.echo_timeout_ms;
fire_at.saturating_sub(now)
}
pub fn echo_ack(&self) -> u64 {
self.echo_ack
}
pub fn title(&self) -> &str {
&self.parser.callbacks().title
}
pub fn icon_name(&self) -> &str {
&self.parser.callbacks().icon
}
pub fn bell_count(&self) -> u64 {
self.parser.callbacks().bell_count
}
pub fn scrollback(&self) -> usize {
self.scrollback
}
pub fn snapshot(&self) -> TerminalScreen {
TerminalScreen {
screen: self.parser.screen().clone(),
echo_ack: self.echo_ack,
title: self.parser.callbacks().title.clone(),
exit_code: self.exit_code,
parser: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn answers_cursor_position_report() {
let mut t = ServerTerminal::new(24, 80, 0);
t.process(b"\x1b[5;3H"); t.process(b"\x1b[6n"); assert_eq!(t.take_host_replies(), b"\x1b[5;3R"); assert!(t.take_host_replies().is_empty());
}
#[test]
fn answers_device_attributes() {
let mut t = ServerTerminal::new(24, 80, 0);
t.process(b"\x1b[c"); assert_eq!(t.take_host_replies(), b"\x1b[?62;1;6c");
t.process(b"\x1b[>c"); assert_eq!(t.take_host_replies(), b"\x1b[>1;10;0c");
}
#[test]
fn echo_ack_debounces() {
let mut t = ServerTerminal::new(24, 80, 0);
t.register_input_frame(5, 1000);
assert!(!t.set_echo_ack(1010));
assert_eq!(t.echo_ack(), 0);
assert!(t.set_echo_ack(1050));
assert_eq!(t.echo_ack(), 5);
}
#[test]
fn echo_ack_honors_injected_timeout() {
let mut t = ServerTerminal::new(24, 80, 0);
t.set_echo_timeout_ms(10);
t.register_input_frame(5, 1000);
assert!(
!t.set_echo_ack(1005),
"still inside the injected 10ms window"
);
assert_eq!(t.echo_ack(), 0);
assert!(t.set_echo_ack(1011), "past the injected 10ms window");
assert_eq!(t.echo_ack(), 5);
let mut d = ServerTerminal::new(24, 80, 0);
d.register_input_frame(5, 1000);
assert!(
!d.set_echo_ack(1011),
"the 50ms default has not elapsed yet"
);
}
#[test]
fn echo_ack_is_monotonic_and_takes_newest() {
let mut t = ServerTerminal::new(24, 80, 0);
t.register_input_frame(3, 1000);
t.register_input_frame(7, 1005);
t.set_echo_ack(1100); assert_eq!(t.echo_ack(), 7);
}
#[test]
fn title_and_bell_captured() {
let mut t = ServerTerminal::new(24, 80, 0);
t.process(b"\x1b]2;my-title\x07\x07");
assert_eq!(t.title(), "my-title");
assert_eq!(t.bell_count(), 1);
assert_eq!(t.snapshot().title(), "my-title");
}
}