use crate::protocol::{self, ServerMsg};
use retach::screen::{AnsiRenderer, Screen};
use std::sync::{Arc, Mutex as StdMutex};
use tokio::io::AsyncWriteExt;
pub(super) const RENDER_THROTTLE: std::time::Duration = std::time::Duration::from_millis(16);
pub(super) fn prepend_passthrough(passthrough: Vec<Vec<u8>>, render_data: Vec<u8>) -> Vec<u8> {
if passthrough.is_empty() {
return render_data;
}
let total: usize = passthrough.iter().map(|c| c.len()).sum::<usize>() + render_data.len();
let mut combined = Vec::with_capacity(total);
for chunk in passthrough {
combined.extend_from_slice(&chunk);
}
combined.extend_from_slice(&render_data);
combined
}
pub(super) fn lock_mutex<'a, T>(
mutex: &'a StdMutex<T>,
label: &str,
) -> anyhow::Result<std::sync::MutexGuard<'a, T>> {
match mutex.lock() {
Ok(guard) => Ok(guard),
Err(poisoned) => {
tracing::warn!(label, "mutex poisoned, recovering inner guard");
Ok(poisoned.into_inner())
}
}
}
pub(super) fn store_dims(
dims: &StdMutex<retach::screen::TerminalSize>,
cols: u16,
rows: u16,
session_name: &str,
) {
match dims.lock() {
Ok(mut d) => *d = retach::screen::sanitize_dimensions(cols, rows),
Err(e) => {
tracing::warn!(session = %session_name, error = %e, "dims mutex poisoned during resize")
}
}
}
pub(super) async fn render_and_send(
screen: &Arc<StdMutex<Screen>>,
renderer: &mut AnsiRenderer,
writer: &mut tokio::net::unix::OwnedWriteHalf,
) -> anyhow::Result<()> {
let update = {
let screen = lock_mutex(screen, "screen")?;
renderer.render(&*screen, true)
};
let msg = protocol::encode(&ServerMsg::ScreenUpdate(update))?;
writer.write_all(&msg).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prepend_passthrough_empty() {
let render = b"render-data".to_vec();
let result = prepend_passthrough(vec![], render.clone());
assert_eq!(result, render);
}
#[test]
fn prepend_passthrough_single() {
let pt = vec![b"\x1b[3J".to_vec()];
let render = b"\x1b[?2026hcontent\x1b[?2026l".to_vec();
let result = prepend_passthrough(pt, render);
assert_eq!(&result[..4], b"\x1b[3J");
assert_eq!(&result[4..], b"\x1b[?2026hcontent\x1b[?2026l");
}
#[test]
fn prepend_passthrough_multiple() {
let pt = vec![vec![0x07], b"\x1b[3J".to_vec()];
let render = b"screen".to_vec();
let result = prepend_passthrough(pt, render);
assert_eq!(result, b"\x07\x1b[3Jscreen");
}
#[test]
fn lock_mutex_recovers_from_poison() {
let m = std::sync::Arc::new(StdMutex::new(42u32));
let m2 = m.clone();
let _ = std::thread::spawn(move || {
let _g = m2.lock().unwrap();
panic!("poison the mutex");
})
.join();
assert!(m.lock().is_err(), "mutex should be poisoned");
let guard = lock_mutex(&m, "test").expect("lock_mutex should recover from poison");
assert_eq!(*guard, 42);
}
#[test]
fn store_dims_writes_sanitized_value() {
let dims = StdMutex::new(retach::screen::TerminalSize { cols: 80, rows: 24 });
store_dims(&dims, 100, 40, "test");
let got = *dims.lock().unwrap();
let expected = retach::screen::sanitize_dimensions(100, 40);
assert_eq!(got.cols, expected.cols);
assert_eq!(got.rows, expected.rows);
}
#[test]
fn store_dims_recovers_from_poison() {
let dims = std::sync::Arc::new(StdMutex::new(retach::screen::TerminalSize {
cols: 80,
rows: 24,
}));
let d2 = dims.clone();
let _ = std::thread::spawn(move || {
let _g = d2.lock().unwrap();
panic!("poison");
})
.join();
store_dims(&dims, 100, 40, "test");
}
}