use std::collections::VecDeque;
use std::sync::atomic::{AtomicUsize, Ordering};
use alacritty_terminal::event::EventListener;
use alacritty_terminal::term::Config;
use alacritty_terminal::vte::ansi::{Processor, StdSyncHandler};
use alacritty_terminal::Term;
use crate::session::GridSize;
pub static CAP_BYTES: AtomicUsize = AtomicUsize::new(2 * 1024 * 1024);
#[derive(Clone, Copy)]
pub struct Quiet;
impl EventListener for Quiet {}
pub type ReplayTerm = Term<Quiet>;
const MERGE_MS: u64 = 30;
const MOMENT_GAP_MS: u64 = 150;
const CHECKPOINT_BYTES: usize = 128 * 1024;
#[derive(Clone, Debug)]
enum Event {
Output(Vec<u8>),
Resize(GridSize),
}
#[derive(Clone, Debug)]
struct Chunk {
ms: u64,
event: Event,
}
fn config() -> Config {
Config {
scrolling_history: 0,
..Config::default()
}
}
fn fresh(size: GridSize) -> Term<Quiet> {
Term::new(config(), &size, Quiet)
}
pub struct Recorder {
base: Term<Quiet>,
base_parser: Processor<StdSyncHandler>,
base_size: GridSize,
base_ms: u64,
chunks: VecDeque<Chunk>,
bytes: usize,
cap: usize,
}
impl Recorder {
pub fn new(size: GridSize) -> Self {
Self::with_cap(size, CAP_BYTES.load(Ordering::Relaxed))
}
pub fn with_cap(size: GridSize, cap_bytes: usize) -> Self {
Self {
base: fresh(size),
base_parser: Processor::new(),
base_size: size,
base_ms: crate::shell::tap::now_ms(),
chunks: VecDeque::new(),
bytes: 0,
cap: cap_bytes,
}
}
pub fn feed(&mut self, data: &[u8]) {
self.feed_at(crate::shell::tap::now_ms(), data);
}
fn feed_at(&mut self, ms: u64, data: &[u8]) {
if data.is_empty() || self.cap == 0 {
return;
}
self.bytes += data.len();
match self.chunks.back_mut() {
Some(Chunk {
ms: last,
event: Event::Output(buf),
}) if ms.saturating_sub(*last) < MERGE_MS && buf.len() < 64 * 1024 => {
buf.extend_from_slice(data);
*last = ms;
}
_ => self.chunks.push_back(Chunk {
ms,
event: Event::Output(data.to_vec()),
}),
}
self.trim();
}
pub fn resize(&mut self, size: GridSize) {
self.chunks.push_back(Chunk {
ms: crate::shell::tap::now_ms(),
event: Event::Resize(size),
});
}
pub fn reset(&mut self, size: GridSize, screen: &[u8]) {
self.base = fresh(size);
self.base_parser = Processor::new();
self.base_parser.advance(&mut self.base, screen);
self.base_size = size;
self.base_ms = crate::shell::tap::now_ms();
self.chunks.clear();
self.bytes = 0;
}
fn trim(&mut self) {
while self.bytes > self.cap {
let Some(chunk) = self.chunks.pop_front() else {
break;
};
match chunk.event {
Event::Output(data) => {
self.bytes -= data.len();
self.base_parser.advance(&mut self.base, &data);
}
Event::Resize(size) => {
self.base.resize(size);
self.base_size = size;
}
}
self.base_ms = chunk.ms;
}
}
pub fn timeline(&mut self) -> Timeline {
let start = crate::mux::snapshot::snapshot(&mut self.base, &config(), Quiet, None);
Timeline::build(
start,
self.base_size,
self.base_ms,
self.chunks.iter().cloned().collect(),
)
}
}
struct Checkpoint {
chunk: usize,
screen: Vec<u8>,
size: GridSize,
}
pub struct Timeline {
chunks: Vec<Chunk>,
moments: Vec<usize>,
checkpoints: Vec<Checkpoint>,
start_ms: u64,
}
impl Timeline {
fn build(start: Vec<u8>, start_size: GridSize, start_ms: u64, chunks: Vec<Chunk>) -> Self {
let mut moments = vec![0];
for (i, c) in chunks.iter().enumerate() {
let next = chunks.get(i + 1).map(|n| n.ms);
let paused = next.is_none_or(|n| n.saturating_sub(c.ms) >= MOMENT_GAP_MS);
if matches!(c.event, Event::Output(_)) && paused {
moments.push(i + 1);
}
}
let mut checkpoints = vec![Checkpoint {
chunk: 0,
screen: start.clone(),
size: start_size,
}];
let mut term = fresh(start_size);
let mut size = start_size;
let mut parser: Processor<StdSyncHandler> = Processor::new();
parser.advance(&mut term, &start);
let mut since = 0;
for (i, c) in chunks.iter().enumerate() {
match &c.event {
Event::Output(data) => {
parser.advance(&mut term, data);
since += data.len();
}
Event::Resize(s) => {
term.resize(*s);
size = *s;
}
}
if since >= CHECKPOINT_BYTES {
since = 0;
let screen = crate::mux::snapshot::snapshot(&mut term, &config(), Quiet, None);
checkpoints.push(Checkpoint {
chunk: i + 1,
screen,
size,
});
parser = Processor::new();
}
}
Self {
chunks,
moments,
checkpoints,
start_ms,
}
}
pub fn len(&self) -> usize {
self.moments.len()
}
pub fn time_ms(&self, moment: usize) -> u64 {
let n = self.moments[moment.min(self.moments.len() - 1)];
if n == 0 {
self.start_ms
} else {
self.chunks[n - 1].ms
}
}
pub fn render(&self, moment: usize) -> Term<Quiet> {
let n = self.moments[moment.min(self.moments.len() - 1)];
let cp = self
.checkpoints
.iter()
.rev()
.find(|c| c.chunk <= n)
.expect("the start is a checkpoint");
let mut term = fresh(cp.size);
let mut parser: Processor<StdSyncHandler> = Processor::new();
parser.advance(&mut term, &cp.screen);
let mut parser: Processor<StdSyncHandler> = Processor::new();
for c in &self.chunks[cp.chunk..n] {
match &c.event {
Event::Output(data) => parser.advance(&mut term, data),
Event::Resize(s) => term.resize(*s),
}
}
term
}
}
#[cfg(test)]
mod tests {
use super::*;
use alacritty_terminal::grid::Dimensions;
use alacritty_terminal::index::{Column, Line};
fn size() -> GridSize {
GridSize { cols: 20, rows: 3 }
}
fn row(term: &Term<Quiet>, line: i32) -> String {
let r = &term.grid()[Line(line)];
(0..term.columns())
.map(|c| r[Column(c)].c)
.collect::<String>()
.trim_end()
.to_string()
}
#[test]
fn moments_are_pauses_and_replay_shows_each_screen() {
let mut rec = Recorder::with_cap(size(), 1 << 20);
rec.feed_at(1000, b"first");
rec.feed_at(1010, b" part");
rec.feed_at(2000, b"\x1b[2J\x1b[Hsecond");
rec.feed_at(3000, b"\x1b[2J\x1b[Hthird");
let t = rec.timeline();
assert_eq!(t.len(), 4, "start + three pauses");
assert_eq!(row(&t.render(1), 0), "first part");
assert_eq!(row(&t.render(2), 0), "second");
assert_eq!(row(&t.render(3), 0), "third");
assert_eq!(t.time_ms(2), 2000);
}
#[test]
fn trimmed_output_lives_on_in_the_base() {
let mut rec = Recorder::with_cap(size(), 64 * 1024);
rec.feed_at(1000, b"kept on screen");
let filler = b"\x1b[3;1Hxxxxxxxxxx".repeat(2500);
rec.feed_at(2001, &filler);
rec.feed_at(3000, &filler);
rec.feed_at(4000, &filler);
assert!(rec.bytes <= 64 * 1024);
let t = rec.timeline();
assert_eq!(row(&t.render(0), 0), "kept on screen");
}
#[test]
fn checkpoints_and_resizes_replay_the_same_screen() {
let mut rec = Recorder::with_cap(size(), 4 << 20);
let mut ms = 1000;
for i in 0..2000 {
ms += 200;
rec.feed_at(
ms,
format!("\r\nline {i:04}{}", "\x1b[0m".repeat(20)).as_bytes(),
);
}
rec.resize(GridSize { cols: 30, rows: 4 });
rec.feed_at(ms + 500, b"\r\nafter resize");
let t = rec.timeline();
assert!(t.checkpoints.len() > 1);
let last = t.render(t.len() - 1);
assert_eq!(last.columns(), 30);
assert_eq!(last.screen_lines(), 4);
assert!((0..4).any(|l| row(&last, l) == "after resize"));
let mid = t.render(1500);
let text: String = (0..3).map(|l| row(&mid, l)).collect::<Vec<_>>().join("\n");
assert!(text.contains("line 1499"), "{text}");
}
}