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();
}
});
}