use super::super::images::{ImagePlacement, PreparedImage};
use std::{
cell::RefCell,
io::{self, IsTerminal, Write},
sync::Arc,
};
const FIRST_IMAGE_ID: usize = 1_900_000_000;
const IMAGE_SLOTS: usize = 16;
static GRAPHICS_ACTIVE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
thread_local! {
static GRAPHICS: RefCell<GraphicsState> = const { RefCell::new(GraphicsState::new()) };
}
struct RetainedPlacement {
slot: usize,
id: usize,
placement: ImagePlacement,
}
struct GraphicsState {
images: Vec<Arc<PreparedImage>>,
placements: Vec<RetainedPlacement>,
}
impl GraphicsState {
const fn new() -> Self {
Self {
images: Vec::new(),
placements: Vec::new(),
}
}
fn image_slot(
&mut self,
out: &mut impl Write,
placement: &ImagePlacement,
visible: &[ImagePlacement],
) -> io::Result<Option<usize>> {
if let Some(index) = self
.images
.iter()
.position(|image| Arc::ptr_eq(image, &placement.image))
{
return Ok(Some(index));
}
let index = if self.images.len() < IMAGE_SLOTS {
self.images.len()
} else {
let Some(index) = self
.images
.iter()
.position(|image| !visible.iter().any(|p| Arc::ptr_eq(image, &p.image)))
else {
return Ok(None);
};
write!(out, "\x1b_Ga=d,d=I,i={},q=2;\x1b\\", FIRST_IMAGE_ID + index)?;
index
};
upload(out, FIRST_IMAGE_ID + index, &placement.image)?;
if index == self.images.len() {
self.images.push(Arc::clone(&placement.image));
} else {
self.images[index] = Arc::clone(&placement.image);
}
Ok(Some(index))
}
fn draw(&mut self, out: &mut impl Write, rows: &[ImagePlacement]) -> io::Result<()> {
let grouped = group_contiguous_rows(rows);
let mut next = Vec::<RetainedPlacement>::new();
for placement in &grouped {
let Some(slot) = self.image_slot(out, placement, &grouped)? else {
continue;
};
let id = next.iter().filter(|p| p.slot == slot).count() + 1;
next.push(RetainedPlacement {
slot,
id,
placement: placement.clone(),
});
}
let mut changed = false;
for old in &self.placements {
if !next.iter().any(|p| p.slot == old.slot && p.id == old.id) {
write!(
out,
"\x1b_Ga=d,d=i,i={},p={},q=2;\x1b\\",
FIRST_IMAGE_ID + old.slot,
old.id
)?;
changed = true;
}
}
let mut saved_cursor = false;
for placement in &next {
if self.placements.iter().any(|old| {
old.slot == placement.slot
&& old.id == placement.id
&& same_placement(&old.placement, &placement.placement)
}) {
continue;
}
if !saved_cursor {
crossterm::queue!(out, crossterm::cursor::SavePosition)?;
saved_cursor = true;
}
place(
out,
FIRST_IMAGE_ID + placement.slot,
placement.id,
&placement.placement,
)?;
changed = true;
}
if saved_cursor {
crossterm::queue!(out, crossterm::cursor::RestorePosition)?;
}
if changed {
out.flush()?;
}
self.placements = next;
Ok(())
}
}
fn same_placement(left: &ImagePlacement, right: &ImagePlacement) -> bool {
Arc::ptr_eq(&left.image, &right.image)
&& left.area == right.area
&& left.source_y == right.source_y
&& left.source_height == right.source_height
}
fn group_contiguous_rows(rows: &[ImagePlacement]) -> Vec<ImagePlacement> {
let mut grouped = Vec::<ImagePlacement>::new();
for row in rows {
if let Some(previous) = grouped.last_mut()
&& Arc::ptr_eq(&previous.image, &row.image)
&& previous.area.x == row.area.x
&& previous.area.width == row.area.width
&& previous.area.y.checked_add(previous.area.height) == Some(row.area.y)
&& previous.source_y.checked_add(previous.source_height) == Some(row.source_y)
&& u64::from(previous.source_height) * u64::from(row.area.height)
== u64::from(row.source_height) * u64::from(previous.area.height)
&& let Some(height) = previous.area.height.checked_add(row.area.height)
&& let Some(source_height) = previous.source_height.checked_add(row.source_height)
{
previous.area.height = height;
previous.source_height = source_height;
} else {
grouped.push(row.clone());
}
}
grouped
}
pub(in crate::tui) fn detect_cell_pixels() -> Option<(u16, u16)> {
let term = std::env::var("TERM").unwrap_or_default();
let program = std::env::var("TERM_PROGRAM").unwrap_or_default();
if !io::stdout().is_terminal()
|| !io::stdin().is_terminal()
|| !supported_terminal(
&term,
&program,
std::env::var_os("TMUX").is_some(),
std::env::var_os("STY").is_some(),
)
{
return None;
}
current_cell_pixels()
}
pub(in crate::tui) fn current_cell_pixels() -> Option<(u16, u16)> {
let size = crossterm::terminal::window_size().ok()?;
let width = size.width.checked_div(size.columns)?;
let height = size.height.checked_div(size.rows)?;
(width > 0 && height > 0).then_some((width, height))
}
fn supported_terminal(term: &str, program: &str, tmux: bool, screen: bool) -> bool {
!tmux && !screen && (term == "xterm-kitty" || program == "ghostty")
}
pub(in crate::tui) fn draw_images(placements: &[ImagePlacement]) -> io::Result<()> {
if !placements.is_empty() {
GRAPHICS_ACTIVE.store(true, std::sync::atomic::Ordering::Relaxed);
}
GRAPHICS.with_borrow_mut(|graphics| graphics.draw(&mut io::stdout().lock(), placements))
}
fn upload(out: &mut impl Write, id: usize, image: &PreparedImage) -> io::Result<()> {
let mut chunks = image.encoded.as_bytes().chunks(4096).peekable();
let mut first = true;
while let Some(chunk) = chunks.next() {
if first {
write!(
out,
"\x1b_Ga=t,t=d,f=32,i={id},s={},v={},q=2,m={};",
image.width,
image.height,
usize::from(chunks.peek().is_some())
)?;
first = false;
} else {
write!(out, "\x1b_Gq=2,m={};", usize::from(chunks.peek().is_some()))?;
}
out.write_all(chunk)?;
out.write_all(b"\x1b\\")?;
}
Ok(())
}
fn place(
out: &mut impl Write,
id: usize,
placement_id: usize,
placement: &ImagePlacement,
) -> io::Result<()> {
crossterm::queue!(
out,
crossterm::cursor::MoveTo(placement.area.x, placement.area.y)
)?;
write!(
out,
"\x1b_Ga=p,i={id},p={placement_id},q=2,C=1,c={},r={},x=0,y={},w={},h={};\x1b\\",
placement.area.width,
placement.area.height,
placement.source_y,
placement.image.width,
placement.source_height
)
}
fn delete_image_data(out: &mut impl Write) {
for index in 0..IMAGE_SLOTS {
let _ = write!(out, "\x1b_Ga=d,d=I,i={},q=2;\x1b\\", FIRST_IMAGE_ID + index);
}
}
pub(in crate::tui) fn cleanup() {
if !GRAPHICS_ACTIVE.swap(false, std::sync::atomic::Ordering::Relaxed) {
return;
}
let mut out = io::stdout().lock();
delete_image_data(&mut out);
let _ = out.flush();
let _ = GRAPHICS.try_with(|graphics| {
if let Ok(mut graphics) = graphics.try_borrow_mut() {
*graphics = GraphicsState::new();
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unchanged_frame_writes_nothing_and_scroll_reuses_uploaded_image() {
let mut graphics = GraphicsState::new();
let rows = preview_rows();
let first = draw_text(&mut graphics, &rows);
assert_eq!(first.matches("a=t,").count(), 1);
assert_eq!(first.matches("a=p,").count(), 1);
assert!(first.contains("c=20,r=3,x=0,y=20,w=100,h=30;"));
assert!(draw_text(&mut graphics, &rows).is_empty());
let mut scrolled = rows[1..].to_vec();
for row in &mut scrolled {
row.area.y -= 1;
}
let scroll = draw_text(&mut graphics, &scrolled);
assert!(!scroll.contains("a=t,"));
assert!(!scroll.contains("a=d,"));
assert_eq!(scroll.matches("a=p,").count(), 1);
assert!(scroll.contains("c=20,r=2,x=0,y=30,w=100,h=20;"));
}
#[test]
fn empty_frame_deletes_disappeared_placement_but_retains_upload() {
let mut graphics = GraphicsState::new();
let rows = preview_rows();
draw_text(&mut graphics, &rows);
assert_eq!(
draw_text(&mut graphics, &[]),
format!("\x1b_Ga=d,d=i,i={FIRST_IMAGE_ID},p=1,q=2;\x1b\\")
);
assert!(draw_text(&mut graphics, &[]).is_empty());
let restored = draw_text(&mut graphics, &rows);
assert!(!restored.contains("a=t,"));
assert_eq!(restored.matches("a=p,").count(), 1);
}
#[test]
fn gaps_are_not_merged_and_disappearing_image_does_not_redraw_neighbor() {
let mut graphics = GraphicsState::new();
let rows = preview_rows();
let other = preview_rows();
let visible = vec![rows[0].clone(), rows[2].clone(), other[0].clone()];
assert_eq!(
draw_text(&mut graphics, &visible).matches("a=p,").count(),
3
);
let remaining = draw_text(&mut graphics, &other[..1]);
assert_eq!(remaining.matches("a=d,").count(), 2);
assert!(!remaining.contains("a=p,"));
assert!(!remaining.contains("a=t,"));
}
#[test]
fn full_upload_cache_evicts_only_an_image_absent_from_next_frame() {
let mut graphics = GraphicsState::new();
let mut rows: Vec<_> = (0..IMAGE_SLOTS).map(|_| preview_rows().remove(0)).collect();
draw_text(&mut graphics, &rows);
rows[0] = preview_rows().remove(0);
let replaced = draw_text(&mut graphics, &rows);
assert_eq!(replaced.matches("a=t,").count(), 1);
assert_eq!(replaced.matches("a=p,").count(), 1);
assert_eq!(replaced.matches("a=d,").count(), 1);
assert!(replaced.contains(&format!("a=d,d=I,i={FIRST_IMAGE_ID},")));
assert!(draw_text(&mut graphics, &rows).is_empty());
}
fn preview_rows() -> Vec<ImagePlacement> {
let image = Arc::new(PreparedImage {
width: 100,
height: 80,
encoded: "AAAA".into(),
});
(0..3)
.map(|row| ImagePlacement {
image: Arc::clone(&image),
area: ratatui::layout::Rect::new(2, 3 + row, 20, 1),
source_y: 20 + u32::from(row) * 10,
source_height: 10,
})
.collect()
}
fn draw_text(graphics: &mut GraphicsState, rows: &[ImagePlacement]) -> String {
let mut output = Vec::new();
graphics.draw(&mut output, rows).unwrap();
String::from_utf8(output).unwrap()
}
#[test]
fn detection_rejects_multiplexers_and_unknown_terminals() {
assert!(supported_terminal("xterm-kitty", "", false, false));
assert!(supported_terminal(
"xterm-256color",
"ghostty",
false,
false
));
assert!(!supported_terminal("xterm-kitty", "", true, false));
assert!(!supported_terminal("xterm-kitty", "", false, true));
assert!(!supported_terminal(
"xterm-256color",
"iTerm.app",
false,
false
));
}
#[test]
fn upload_chunks_are_bounded_and_do_not_request_terminal_file_access() {
let image = PreparedImage {
width: 20,
height: 100,
encoded: "A".repeat(10_668),
};
let mut out = Vec::new();
upload(&mut out, FIRST_IMAGE_ID, &image).unwrap();
let text = String::from_utf8(out).unwrap();
assert!(text.starts_with("\x1b_Ga=t,t=d,f=32,"));
let commands: Vec<_> = text.split("\x1b\\").filter(|s| !s.is_empty()).collect();
assert_eq!(commands.len(), 3);
for command in &commands {
assert!(command.split_once(';').unwrap().1.len() <= 4096);
}
assert!(commands.last().unwrap().starts_with("\x1b_Gq=2,m=0;"));
}
#[test]
fn scrolled_row_uses_source_crop_and_never_moves_cursor() {
let placement = ImagePlacement {
image: Arc::new(PreparedImage {
width: 100,
height: 80,
encoded: String::new(),
}),
area: ratatui::layout::Rect::new(2, 3, 20, 1),
source_y: 30,
source_height: 10,
};
let mut out = Vec::new();
place(&mut out, 1, 4, &placement).unwrap();
assert_eq!(
String::from_utf8(out).unwrap(),
"\x1b[4;3H\x1b_Ga=p,i=1,p=4,q=2,C=1,c=20,r=1,x=0,y=30,w=100,h=10;\x1b\\"
);
}
}
#[cfg(test)]
#[test]
fn image_cleanup_deletes_owned_ids_without_clearing_other_terminal_images() {
let mut output = Vec::new();
delete_image_data(&mut output);
let text = String::from_utf8(output).unwrap();
let commands: Vec<_> = text
.split("\x1b\\")
.filter(|command| !command.is_empty())
.collect();
assert_eq!(commands.len(), IMAGE_SLOTS);
for (index, command) in commands.iter().enumerate() {
assert_eq!(
*command,
format!("\x1b_Ga=d,d=I,i={},q=2;", FIRST_IMAGE_ID + index)
);
}
}