use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::path::Path;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use super::diagram;
use crate::fetch;
use crate::raster::{Picture, Raster};
use crate::ui::Span;
type Pic = Option<Arc<Picture>>;
pub enum Job {
Image(String),
Diagram {
lang: String,
src: String,
dark: bool,
},
}
pub fn diagram_key(lang: &str, src: &str, dark: bool) -> String {
let mut h = std::collections::hash_map::DefaultHasher::new();
(lang, src, dark).hash(&mut h);
format!("{lang}:{:x}", h.finish())
}
impl Job {
fn key(&self) -> String {
match self {
Job::Image(k) => k.clone(),
Job::Diagram { lang, src, dark } => diagram_key(lang, src, *dark),
}
}
fn quick(&self) -> bool {
!matches!(self, Job::Image(k) if fetch::is_url(k))
}
fn run(&self) -> Pic {
let load = || match self {
Job::Image(k) if fetch::is_url(k) => {
crate::raster::decode(&fetch::fetch_untrusted(k).ok()?, k, None).ok()
}
Job::Image(k) => {
crate::raster::decode(&fetch::read_source(k).ok()?, k, Path::new(k).parent()).ok()
}
Job::Diagram { lang, src, dark } => match diagram::render(lang, src, *dark) {
Some(svg) => crate::raster::decode_generated_svg(&svg).ok(),
None if lang == "mermaid" => mmdc(src, *dark),
None => None,
},
};
crate::app::quiet_panics(load).flatten().map(Arc::new)
}
}
pub type Typeset = Arc<Vec<(Arc<Raster>, String)>>;
pub struct Assets {
pics: HashMap<String, Pic>,
loading: HashSet<String>,
tx: Sender<(String, Pic)>,
rx: Receiver<(String, Pic)>,
valid_for: String,
pub scaled: HashMap<(String, u32, u32), Arc<Raster>>,
pub headings: HashMap<String, Option<Typeset>>,
pub code: HashMap<u64, Arc<Vec<Vec<Span>>>>,
}
impl Default for Assets {
fn default() -> Self {
let (tx, rx) = channel();
Assets {
pics: HashMap::new(),
loading: HashSet::new(),
tx,
rx,
valid_for: String::new(),
scaled: HashMap::new(),
headings: HashMap::new(),
code: HashMap::new(),
}
}
}
impl Assets {
pub fn get(&self, key: &str) -> Pic {
self.pics.get(key).cloned().flatten()
}
pub fn loading(&self) -> usize {
self.loading.len()
}
pub fn validate(&mut self, geometry: String) {
if self.valid_for != geometry {
self.valid_for = geometry;
self.scaled.clear();
self.headings.clear();
self.code.clear();
}
}
pub fn poll(&mut self) -> bool {
let mut any = false;
while let Ok(done) = self.rx.try_recv() {
self.finish(done);
any = true;
}
any
}
fn finish(&mut self, (key, pic): (String, Pic)) {
self.loading.remove(&key);
self.pics.insert(key, pic);
}
pub fn load(&mut self, jobs: Vec<Job>, wait: bool) {
const PATIENCE: Duration = Duration::from_millis(80);
let mut jobs: Vec<Job> = jobs
.into_iter()
.filter(|j| {
let k = j.key();
!self.pics.contains_key(&k) && self.loading.insert(k)
})
.collect();
if jobs.is_empty() {
return;
}
let maybe_svg = |j: &Job| matches!(j, Job::Image(k) if fetch::is_url(k) || k.to_ascii_lowercase().contains(".svg"));
if jobs.iter().any(maybe_svg) {
crate::raster::warm_fonts();
}
jobs.sort_by_key(Job::quick);
let mut quick: HashSet<String> = jobs.iter().filter(|j| j.quick()).map(Job::key).collect();
let workers = jobs.len().min(16);
let queue = Arc::new(Mutex::new(jobs));
for _ in 0..workers {
let (queue, tx) = (queue.clone(), self.tx.clone());
std::thread::spawn(move || {
while let Some(job) = queue.lock().ok().and_then(|mut q| q.pop()) {
if tx.send((job.key(), job.run())).is_err() {
break;
}
}
});
}
let deadline = Instant::now() + PATIENCE;
loop {
let done = if wait && !self.loading.is_empty() {
self.rx.recv().ok()
} else if !wait && !quick.is_empty() {
let left = deadline.saturating_duration_since(Instant::now());
self.rx.recv_timeout(left).ok()
} else {
None
};
let Some(done) = done else {
break;
};
quick.remove(&done.0);
self.finish(done);
}
}
}
fn mmdc(src: &str, dark: bool) -> Option<Picture> {
use std::os::unix::fs::DirBuilderExt;
use std::process::{Command, Stdio};
let mmdc = fetch::tool("mmdc")?;
let dir = std::env::temp_dir().join(format!("shw-mermaid-{}", crate::kitty::random_hex(12)));
std::fs::DirBuilder::new().mode(0o700).create(&dir).ok()?;
let (input, output) = (dir.join("diagram.mmd"), dir.join("diagram.png"));
let run = || {
crate::kitty::write_private_new(&input, src.as_bytes()).ok()?;
let status = Command::new(&mmdc)
.current_dir(&dir)
.arg("-i")
.arg(&input)
.arg("-o")
.arg(&output)
.args(["-b", "transparent", "-s", "2", "-q", "-t"])
.arg(if dark { "dark" } else { "default" })
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.ok()?;
let data = std::fs::read(&output).ok().filter(|_| status.success())?;
let mut pic = crate::raster::decode(&data, "diagram.png", None).ok()?;
pic.width = (pic.width / 2).max(1);
pic.height = (pic.height / 2).max(1);
Some(pic)
};
let pic = run();
let _ = std::fs::remove_dir_all(&dir);
pic
}