use std::collections::VecDeque;
use std::fmt;
use std::io;
use std::sync::Arc;
use ratatui::backend::{Backend, ClearType, WindowSize};
use ratatui::buffer::Cell;
use ratatui::layout::{Position, Size};
use super::cell::EnhancedCell;
use super::output::OutputFormat;
#[derive(Clone, Debug)]
pub struct CaptureBackend {
cells: Vec<EnhancedCell>,
width: u16,
height: u16,
cursor_position: Position,
cursor_visible: bool,
current_frame: u64,
history: VecDeque<FrameSnapshot>,
history_capacity: usize,
}
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct FrameSnapshot {
pub frame: u64,
pub size: (u16, u16),
pub cursor: CursorSnapshot,
#[cfg_attr(
feature = "serialization",
serde(serialize_with = "serialize_arc_cells")
)]
#[cfg_attr(
feature = "serialization",
serde(deserialize_with = "deserialize_arc_cells")
)]
cells: Arc<[EnhancedCell]>,
}
#[cfg(feature = "serialization")]
fn serialize_arc_cells<S>(cells: &Arc<[EnhancedCell]>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeSeq;
let mut seq = serializer.serialize_seq(Some(cells.len()))?;
for cell in cells.iter() {
seq.serialize_element(cell)?;
}
seq.end()
}
#[cfg(feature = "serialization")]
fn deserialize_arc_cells<'de, D>(deserializer: D) -> Result<Arc<[EnhancedCell]>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let vec = Vec::<EnhancedCell>::deserialize(deserializer)?;
Ok(Arc::from(vec))
}
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct CursorSnapshot {
pub position: (u16, u16),
pub visible: bool,
}
impl FrameSnapshot {
pub fn cells(&self) -> &[EnhancedCell] {
&self.cells
}
pub fn row_content(&self, y: u16) -> String {
if y >= self.size.1 {
return String::new();
}
let start = (y as usize) * (self.size.0 as usize);
let end = start + self.size.0 as usize;
if end <= self.cells.len() {
self.cells[start..end].iter().map(|c| c.symbol()).collect()
} else {
String::new()
}
}
pub fn to_plain(&self) -> String {
let mut lines = Vec::with_capacity(self.size.1 as usize);
for y in 0..self.size.1 {
lines.push(self.row_content(y));
}
lines.join("\n")
}
pub fn to_ansi(&self) -> String {
use crate::backend::cell::SerializableColor;
let mut output = String::new();
for y in 0..self.size.1 {
let start = (y as usize) * (self.size.0 as usize);
let end = start + self.size.0 as usize;
if end <= self.cells.len() {
for cell in &self.cells[start..end] {
if cell.fg != SerializableColor::Reset {
output.push_str(&cell.fg.to_ansi_fg());
}
if cell.bg != SerializableColor::Reset {
output.push_str(&cell.bg.to_ansi_bg());
}
output.push_str(&cell.modifiers.to_ansi());
output.push_str(cell.symbol());
if cell.fg != SerializableColor::Reset
|| cell.bg != SerializableColor::Reset
|| !cell.modifiers.is_empty()
{
output.push_str("\x1b[0m");
}
}
}
if y + 1 < self.size.1 {
output.push('\n');
}
}
output
}
pub fn contains_text(&self, needle: &str) -> bool {
for y in 0..self.size.1 {
if self.row_content(y).contains(needle) {
return true;
}
}
false
}
}
impl CaptureBackend {
pub fn new(width: u16, height: u16) -> Self {
let size = (width as usize) * (height as usize);
Self {
cells: vec![EnhancedCell::new(); size],
width,
height,
cursor_position: Position::new(0, 0),
cursor_visible: true,
current_frame: 0,
history: VecDeque::new(),
history_capacity: 0,
}
}
pub fn with_history(width: u16, height: u16, history_capacity: usize) -> Self {
let mut backend = Self::new(width, height);
backend.history_capacity = history_capacity;
backend.history = VecDeque::with_capacity(history_capacity);
backend
}
pub fn current_frame(&self) -> u64 {
self.current_frame
}
pub fn cell(&self, x: u16, y: u16) -> Option<&EnhancedCell> {
if x < self.width && y < self.height {
Some(&self.cells[self.index_of(x, y)])
} else {
None
}
}
pub fn cell_mut(&mut self, x: u16, y: u16) -> Option<&mut EnhancedCell> {
if x < self.width && y < self.height {
let idx = self.index_of(x, y);
Some(&mut self.cells[idx])
} else {
None
}
}
pub fn cells(&self) -> &[EnhancedCell] {
&self.cells
}
pub fn row_content(&self, y: u16) -> String {
if y >= self.height {
return String::new();
}
let start = self.index_of(0, y);
let end = start + self.width as usize;
self.cells[start..end].iter().map(|c| c.symbol()).collect()
}
pub fn content_lines(&self) -> Vec<String> {
(0..self.height).map(|y| self.row_content(y)).collect()
}
pub fn find_text(&self, needle: &str) -> Vec<Position> {
let mut positions = Vec::new();
for y in 0..self.height {
let row = self.row_content(y);
for (x, _) in row.match_indices(needle) {
positions.push(Position::new(x as u16, y));
}
}
positions
}
pub fn contains_text(&self, needle: &str) -> bool {
!self.find_text(needle).is_empty()
}
pub fn snapshot(&self) -> FrameSnapshot {
FrameSnapshot {
frame: self.current_frame,
size: (self.width, self.height),
cursor: CursorSnapshot {
position: (self.cursor_position.x, self.cursor_position.y),
visible: self.cursor_visible,
},
cells: Arc::from(self.cells.as_slice()),
}
}
pub fn history(&self) -> &VecDeque<FrameSnapshot> {
&self.history
}
pub fn diff_from_previous(&self) -> Option<FrameDiff> {
self.history.back().map(|prev| self.diff_from(prev))
}
pub fn diff_from(&self, previous: &FrameSnapshot) -> FrameDiff {
let mut changed_cells = Vec::new();
for y in 0..self.height.min(previous.size.1) {
for x in 0..self.width.min(previous.size.0) {
let idx = self.index_of(x, y);
let prev_idx = (y as usize) * (previous.size.0 as usize) + (x as usize);
if idx < self.cells.len() && prev_idx < previous.cells.len() {
let current = &self.cells[idx];
let prev = &previous.cells[prev_idx];
if current != prev {
changed_cells.push(CellChange {
position: (x, y),
old: prev.clone(),
new: current.clone(),
});
}
}
}
}
FrameDiff {
from_frame: previous.frame,
to_frame: self.current_frame,
changed_cells,
size_changed: (self.width, self.height) != previous.size,
cursor_moved: (self.cursor_position.x, self.cursor_position.y)
!= previous.cursor.position,
}
}
pub fn render(&self, format: OutputFormat) -> String {
format.render(self)
}
pub fn to_ansi(&self) -> String {
self.render(OutputFormat::Ansi)
}
pub fn to_annotated_output(
&self,
registry: &crate::annotation::AnnotationRegistry,
) -> crate::annotation::AnnotatedOutput {
crate::annotation::AnnotatedOutput::from_backend_and_registry(self, registry)
}
#[cfg(feature = "serialization")]
pub fn to_semantic_json(&self, registry: &crate::annotation::AnnotationRegistry) -> String {
let output = self.to_annotated_output(registry);
serde_json::to_string_pretty(&output)
.unwrap_or_else(|e| format!("{{\"error\": \"serialization failed: {}\"}}", e))
}
#[cfg(feature = "serialization")]
pub fn to_json(&self) -> String {
self.render(OutputFormat::Json)
}
#[cfg(feature = "serialization")]
pub fn to_json_pretty(&self) -> String {
self.render(OutputFormat::JsonPretty)
}
fn index_of(&self, x: u16, y: u16) -> usize {
(y as usize) * (self.width as usize) + (x as usize)
}
fn save_to_history(&mut self) {
if self.history_capacity > 0 {
if self.history.len() >= self.history_capacity {
self.history.pop_front();
}
self.history.push_back(self.snapshot());
}
}
pub fn width(&self) -> u16 {
self.width
}
pub fn height(&self) -> u16 {
self.height
}
pub fn is_cursor_visible(&self) -> bool {
self.cursor_visible
}
pub fn cursor_position(&self) -> Position {
self.cursor_position
}
}
impl Backend for CaptureBackend {
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
for (x, y, cell) in content {
if x < self.width && y < self.height {
let idx = self.index_of(x, y);
self.cells[idx] = EnhancedCell::from_ratatui_cell(cell, self.current_frame);
}
}
Ok(())
}
fn hide_cursor(&mut self) -> io::Result<()> {
self.cursor_visible = false;
Ok(())
}
fn show_cursor(&mut self) -> io::Result<()> {
self.cursor_visible = true;
Ok(())
}
fn get_cursor_position(&mut self) -> io::Result<Position> {
Ok(self.cursor_position)
}
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
self.cursor_position = position.into();
Ok(())
}
fn clear(&mut self) -> io::Result<()> {
for cell in &mut self.cells {
cell.reset();
}
Ok(())
}
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
let cells_len = self.cells.len();
let (start, end) = match clear_type {
ClearType::All => (0, cells_len),
ClearType::AfterCursor => {
let start = self.index_of(self.cursor_position.x, self.cursor_position.y);
(start, cells_len)
}
ClearType::BeforeCursor => {
let end = self.index_of(self.cursor_position.x, self.cursor_position.y);
(0, end)
}
ClearType::CurrentLine => {
let start = self.index_of(0, self.cursor_position.y);
let end = start + self.width as usize;
(start, end)
}
ClearType::UntilNewLine => {
let start = self.index_of(self.cursor_position.x, self.cursor_position.y);
let end = self.index_of(0, self.cursor_position.y) + self.width as usize;
(start, end)
}
};
let end = end.min(cells_len);
for cell in &mut self.cells[start..end] {
cell.reset();
}
Ok(())
}
fn size(&self) -> io::Result<Size> {
Ok(Size::new(self.width, self.height))
}
fn window_size(&mut self) -> io::Result<WindowSize> {
Ok(WindowSize {
columns_rows: Size::new(self.width, self.height),
pixels: Size::new(self.width * 8, self.height * 16),
})
}
fn flush(&mut self) -> io::Result<()> {
self.save_to_history();
self.current_frame += 1;
Ok(())
}
}
impl fmt::Display for CaptureBackend {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.render(OutputFormat::Plain))
}
}
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct FrameDiff {
pub from_frame: u64,
pub to_frame: u64,
pub changed_cells: Vec<CellChange>,
pub size_changed: bool,
pub cursor_moved: bool,
}
impl FrameDiff {
pub fn has_changes(&self) -> bool {
!self.changed_cells.is_empty() || self.size_changed || self.cursor_moved
}
pub fn changed_count(&self) -> usize {
self.changed_cells.len()
}
}
impl fmt::Display for FrameDiff {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Frame {} → {} changes:", self.from_frame, self.to_frame)?;
if self.size_changed {
writeln!(f, " [Size changed]")?;
}
if self.cursor_moved {
writeln!(f, " [Cursor moved]")?;
}
for change in &self.changed_cells {
writeln!(
f,
" ({},{}) \"{}\" → \"{}\"",
change.position.0,
change.position.1,
change.old.symbol(),
change.new.symbol()
)?;
}
Ok(())
}
}
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct CellChange {
pub position: (u16, u16),
pub old: EnhancedCell,
pub new: EnhancedCell,
}
#[cfg(test)]
mod tests;