use std::collections::{HashMap, HashSet};
use std::io::Write;
use std::sync::{Arc, Weak};
use std::time::{Duration, Instant};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use super::clients::frame_bytes;
use super::{ClientId, Server};
use crate::graphics::paint::{self, Drawn, Motion, Painted};
use crate::graphics::quantize::{self, Indexed};
use crate::graphics::{self, Picture};
use crate::layout::PaneId;
const BUDGET: usize = 4 << 20;
const FITTING: usize = 4;
const PREPARING: usize = 2;
const STILL: Duration = Duration::from_millis(100);
#[derive(Default)]
pub(super) struct ClientImages {
answered: bool,
name: Option<String>,
capable: bool,
says_sixel: bool,
sixel: bool,
cell: Option<(u16, u16)>,
sent: HashMap<u32, u64>,
version: Option<u64>,
painted: Painted,
motion: Motion,
}
impl ClientImages {
pub(super) fn forget(&mut self) {
self.sent.clear();
self.version = None;
self.painted.clear();
}
fn settle(&mut self) {
self.capable |= self.answered && self.name.as_deref().is_some_and(graphics::draws_placeholders);
self.sixel = self.says_sixel && (self.sixel || self.cell.is_some());
}
fn shows(&self) -> (bool, bool) {
(self.capable, self.sixel)
}
fn draws_sixel(&self) -> bool {
self.sixel && !self.capable
}
fn shows_images(&self) -> bool {
self.capable || self.sixel
}
}
#[derive(Default)]
pub(super) struct Fitted {
entries: HashMap<FitKey, Fit>,
pending: HashSet<FitKey>,
bytes: usize,
clock: u64,
}
type FitKey = (usize, u32, u32);
struct Fit {
picture: Weak<Picture>,
image: Arc<Indexed>,
used: u64,
}
enum Ready {
Now(Arc<Indexed>),
Later,
Start,
Never,
}
impl Fitted {
fn key(picture: &Arc<Picture>, width: u32, height: u32) -> FitKey {
(Arc::as_ptr(picture) as usize, width, height)
}
fn ready(&mut self, picture: &Arc<Picture>, width: u32, height: u32, budget: usize) -> Ready {
self.clock += 1;
let key = Fitted::key(picture, width, height);
if let Some(fit) = self.entries.get_mut(&key) {
fit.used = self.clock;
return Ready::Now(fit.image.clone());
}
if width as usize * height as usize > budget {
return Ready::Never;
}
if self.pending.contains(&key) || self.pending.len() >= FITTING {
return Ready::Later;
}
self.pending.insert(key);
Ready::Start
}
fn done(&mut self, key: FitKey, picture: Weak<Picture>, image: Option<Indexed>, budget: usize) {
self.pending.remove(&key);
if picture.strong_count() == 0 {
return;
}
let image = Arc::new(image.unwrap_or(Indexed { width: 0, height: 0, palette: Vec::new(), pixels: Vec::new() }));
self.clock += 1;
self.bytes += image.bytes();
if let Some(old) = self.entries.insert(key, Fit { picture, image, used: self.clock }) {
self.bytes -= old.image.bytes();
}
self.trim(budget);
}
fn trim(&mut self, budget: usize) {
let mut bytes = self.bytes;
self.entries.retain(|_, f| {
let alive = f.picture.strong_count() > 0;
if !alive {
bytes -= f.image.bytes();
}
alive
});
while bytes > budget && self.entries.len() > 1 {
let Some(oldest) = self.entries.iter().min_by_key(|(_, f)| f.used).map(|(k, _)| *k) else { break };
if let Some(f) = self.entries.remove(&oldest) {
bytes -= f.image.bytes();
}
}
self.bytes = bytes;
}
}
impl Server {
pub(super) fn probe_graphics(&mut self, id: ClientId) {
if !self.env.graphics.enabled() {
return;
}
self.images.entry(id).or_default();
if let Some(c) = self.clients.get(&id) {
let _ = c.out.try_send(frame_bytes(graphics::probe().as_bytes()));
}
}
pub(super) fn probe_cell_size(&mut self, id: ClientId) {
if self.env.graphics.enabled()
&& let Some(c) = self.clients.get(&id)
{
let _ = c.out.try_send(frame_bytes(b"\x1b[16t"));
if let Some(ci) = self.images.get_mut(&id).filter(|ci| ci.draws_sixel()) {
ci.cell = None;
}
}
}
pub(super) fn graphics_reply(&mut self, id: ClientId, reply: &str) -> bool {
let answer = graphics::probe_answer(reply);
let name = graphics::terminal_name(reply);
let sixel = graphics::sixel_answer(reply);
if answer.is_some() || name.is_some() || sixel.is_some() {
let Some(ci) = self.images.get_mut(&id) else { return true };
let before = ci.shows();
if answer == Some(true) {
ci.answered = true;
}
if name.is_some() {
ci.name = name;
}
if let Some(sixel) = sixel {
ci.says_sixel = sixel;
}
ci.settle();
if ci.shows() != before {
self.images_shown_otherwise(id);
}
return true;
}
if let Some((w, h)) = graphics::cell_size_answer(reply).filter(|(w, h)| *w > 0 && *h > 0) {
if let Some(ci) = self.images.get_mut(&id) {
let (before, redraw) = (ci.shows(), ci.draws_sixel() && ci.cell != Some((w, h)));
ci.cell = Some((w, h));
ci.settle();
if ci.shows() != before {
self.images_shown_otherwise(id);
} else if redraw {
self.mark_dirty_client(id);
}
}
if self.primary.is_none_or(|p| p == id) && self.env.graphics.cell() != (w, h) {
self.env.graphics.set_cell(w, h);
for p in self.panes.values_mut() {
p.refresh_pixel_size();
}
}
return true;
}
false
}
pub(super) fn graphics_farewell(&mut self, id: ClientId) {
let Some(ci) = self.images.get_mut(&id) else { return };
let mut out = Vec::new();
for kid in ci.sent.keys() {
graphics::delete(*kid, &mut out);
}
ci.forget();
if let Some(c) = self.clients.get(&id)
&& !out.is_empty()
{
let _ = c.out.try_send(frame_bytes(&out));
}
}
pub(super) fn graphics_client_gone(&mut self, id: ClientId) {
if self.images.remove(&id).is_some() {
self.update_availability();
}
if !self.images.values().any(ClientImages::draws_sixel) {
self.fitted = Fitted::default();
}
if let Some(cell) = self.primary.and_then(|p| self.images.get(&p)).and_then(|ci| ci.cell) {
self.env.graphics.set_cell(cell.0, cell.1);
}
}
fn images_shown_otherwise(&mut self, id: ClientId) {
if let Some(ci) = self.images.get_mut(&id) {
ci.forget();
}
self.update_availability();
self.mark_dirty_client(id);
}
fn update_availability(&mut self) {
let any =
self.images.iter().any(|(id, ci)| ci.shows_images() && self.clients.get(id).is_some_and(|c| c.attached));
self.env.graphics.set_available(any);
}
pub(super) fn can_show_images(&self, id: ClientId) -> bool {
self.images.get(&id).is_some_and(ClientImages::shows_images)
}
fn live_images(&self) -> HashMap<u32, graphics::Live> {
self.panes.values().flat_map(|p| p.images()).collect()
}
pub(super) fn graphics_updates(&mut self, id: ClientId) -> (Vec<u8>, bool) {
let mut out = Vec::new();
let version = self.env.graphics.version();
let Some(ci) = self.images.get(&id) else { return (out, false) };
if !ci.capable || ci.version == Some(version) {
return (out, false);
}
let live = self.live_images();
let Some(ci) = self.images.get_mut(&id) else { return (out, false) };
let more = graphics::update_terminal(&mut ci.sent, live, BUDGET, &mut out);
if !more {
ci.version = Some(version);
}
(out, more)
}
pub(super) fn paint_images(&mut self, id: ClientId, buf: &mut Buffer) -> (Vec<u8>, Vec<u8>, Option<Instant>) {
let (mut erase, mut draw, mut wake) = (Vec::new(), Vec::new(), None);
let Some(ci) = self.images.get(&id).filter(|ci| ci.draws_sixel()) else { return (erase, draw, wake) };
let area = buf.area;
let (within, cell) = match ci.cell {
Some(cell) => (Rect { height: area.height.saturating_sub(1), ..area }, cell),
None => (Rect::default(), (0, 0)),
};
let parts = paint::take_parts(buf, within);
let buf: &Buffer = buf;
let live = if parts.is_empty() { HashMap::new() } else { self.live_images() };
let wanted: Vec<Drawn> = parts
.into_iter()
.filter_map(|part| {
let (_, picture, placements) = live.get(&part.kid)?;
let &(_, cols, rows) =
placements.iter().find(|p| p.0 == part.placement).or_else(|| placements.first())?;
Some(Drawn { part, picture: Arc::as_ptr(picture) as usize, cells: (cols, rows), cell })
})
.collect();
let (Some(c), Some(ci)) = (self.clients.get_mut(&id), self.images.get_mut(&id)) else {
return (erase, draw, wake);
};
let moving = ci.motion.track(&wanted, Instant::now(), STILL);
if wanted.is_empty() && ci.painted.is_empty() {
return (erase, draw, wake);
}
if c.encoder.redraws_all(area) {
ci.painted.clear();
}
for stale in ci.painted.stale(&wanted) {
paint::erase(stale, &mut erase);
c.encoder.redraw(stale, buf);
}
let todo = ci.painted.update(wanted, |r| c.encoder.rewrites(buf, r));
let budget = self.env.graphics.quota() / 4;
let mut start = Vec::new();
for d in todo {
if let Some(&at) = moving.get(&d.part.kid) {
ci.painted.forget(&d);
wake = Some(wake.map_or(at, |w: Instant| w.min(at)));
continue;
}
let Some((_, picture, _)) = live.get(&d.part.kid) else { continue };
let (bw, bh) = (u32::from(d.cells.0) * u32::from(cell.0), u32::from(d.cells.1) * u32::from(cell.1));
let image = match self.fitted.ready(picture, bw, bh, budget) {
Ready::Now(image) => image,
Ready::Never => continue,
Ready::Later => {
ci.painted.forget(&d);
continue;
}
Ready::Start => {
ci.painted.forget(&d);
start.push((picture.clone(), bw, bh));
continue;
}
};
let (x, y, w, h) = paint::pixels(&d.part, cell);
let _ = write!(draw, "\x1b[{};{}H", d.part.area.y + 1, d.part.area.x + 1);
graphics::sixel::encode(&image, x, y, w, h, &mut draw);
}
for (picture, width, height) in start {
self.fit_later(picture, width, height);
}
(erase, draw, wake)
}
pub(super) fn prepare_images(&mut self) {
while self.preparing < PREPARING {
let Some((id, work)) = self.panes.iter_mut().find_map(|(id, p)| Some((*id, p.take_image_work()?))) else {
return;
};
self.preparing += 1;
if !self.background(work, move |server, prepared| server.image_prepared(id, prepared)) {
self.preparing -= 1;
if let Some(p) = self.panes.get_mut(&id) {
p.image_prepared(Err("ENOMEM:no thread to prepare the image".into()));
}
return;
}
}
}
fn image_prepared(&mut self, id: PaneId, prepared: graphics::Prepared) {
self.preparing -= 1;
if let Some(p) = self.panes.get_mut(&id) {
p.image_prepared(prepared);
self.mark_dirty_pane(id);
}
self.prepare_images();
}
fn fit_later(&mut self, picture: Arc<Picture>, width: u32, height: u32) {
let key = Fitted::key(&picture, width, height);
let weak = Arc::downgrade(&picture);
let started = self.background(
move || picture.decode().ok().map(|i| quantize::fit(&i.into_rgba8(), width, height)),
move |server, image| {
let budget = server.env.graphics.quota() / 4;
server.fitted.done(key, weak, image, budget);
server.mark_dirty();
},
);
if !started {
self.fitted.pending.remove(&key);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn says_sixel(ci: &mut ClientImages) {
ci.says_sixel = true;
ci.settle();
}
fn tells_cell(ci: &mut ClientImages) {
ci.cell = Some((10, 20));
ci.settle();
}
#[test]
fn sixel_is_drawn_once_both_answers_came_in_either_order() {
for answers in [[says_sixel as fn(&mut ClientImages), tells_cell], [tells_cell, says_sixel]] {
let mut ci = ClientImages::default();
answers[0](&mut ci);
assert!(!ci.draws_sixel(), "not on one answer");
answers[1](&mut ci);
assert!(ci.draws_sixel());
ci.cell = None;
ci.settle();
assert!(ci.draws_sixel());
}
}
}