diff --git a/Cargo.lock b/Cargo.lock index 1119bb37..c9e7f55d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,9 +54,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" [[package]] name = "async-channel" @@ -77,7 +77,7 @@ checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] [[package]] @@ -532,7 +532,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] [[package]] @@ -847,9 +847,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jpeg-decoder" @@ -1201,7 +1201,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] [[package]] @@ -1257,9 +1257,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" +checksum = "92de25114670a878b1261c79c9f8f729fb97e95bac93f6312f583c60dd6a1dfe" dependencies = [ "unicode-ident", ] @@ -1316,9 +1316,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "5907a1b7c277254a8b15170f6e7c97cfa60ee7872a3217663bb81151e48184bb" dependencies = [ "proc-macro2", ] @@ -1458,15 +1458,15 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustversion" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "safemem" @@ -1506,14 +1506,14 @@ checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] [[package]] name = "serde_json" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5062a995d481b2308b6064e9af76011f2921c35f97b0468811ed9f6cd91dfed" +checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" dependencies = [ "itoa", "ryu", @@ -1540,9 +1540,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +checksum = "b824b6e687aff278cdbf3b36f07aa52d4bd4099699324d5da86a2ebce3aa00b3" dependencies = [ "libc", "signal-hook-registry", @@ -1633,9 +1633,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.25" +version = "2.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" +checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" dependencies = [ "proc-macro2", "quote", @@ -1688,7 +1688,7 @@ checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] [[package]] @@ -1793,7 +1793,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] [[package]] @@ -1844,9 +1844,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.13" +version = "0.19.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f8751d9c1b03c6500c387e96f81f815a4f8e72d142d2d4a9ffa6fedd51ddee7" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" dependencies = [ "indexmap 2.0.0", "serde", @@ -1946,7 +1946,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] [[package]] @@ -2018,9 +2018,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "unicode-normalization" @@ -2106,7 +2106,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", "wasm-bindgen-shared", ] @@ -2128,7 +2128,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/src/core/event.rs b/src/core/event.rs index a5103e0f..6ebfd549 100644 --- a/src/core/event.rs +++ b/src/core/event.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{collections::BTreeMap, path::PathBuf}; use anyhow::Result; use crossterm::event::KeyEvent; @@ -15,15 +15,19 @@ pub enum Event { Render(String), Resize(u16, u16), + // Manager Cd(PathBuf), Refresh, Files(FilesOp), + Pages(usize), + Mimetype(BTreeMap), Hover, - Mimetype(PathBuf, String), Preview(PathBuf, PreviewData), + // Input Input(InputOpt, oneshot::Sender>), + // Tasks Open(Vec), Progress(u8, u32), } @@ -73,8 +77,11 @@ macro_rules! emit { (Files($op:expr)) => { $crate::core::Event::Files($op).emit(); }; - (Mimetype($path:expr, $mime:expr)) => { - $crate::core::Event::Mimetype($path, $mime).emit(); + (Pages($page:expr)) => { + $crate::core::Event::Pages($page).emit(); + }; + (Mimetype($mimes:expr)) => { + $crate::core::Event::Mimetype($mimes).emit(); }; (Preview($path:expr, $data:expr)) => { $crate::core::Event::Preview($path, $data).emit(); diff --git a/src/core/external/ffmpegthumbnailer.rs b/src/core/external/ffmpegthumbnailer.rs new file mode 100644 index 00000000..f5663ca6 --- /dev/null +++ b/src/core/external/ffmpegthumbnailer.rs @@ -0,0 +1,23 @@ +use std::path::Path; + +use anyhow::{bail, Result}; +use tokio::process::Command; + +use crate::config::PREVIEW; + +pub async fn ffmpegthumbnailer(path: &Path, dest: &Path) -> Result<()> { + let output = Command::new("ffmpegthumbnailer") + .arg("-i") + .arg(path) + .arg("-o") + .arg(dest) + .args(["-q", "6", "-c", "jpeg", "-s", &PREVIEW.max_width.to_string()]) + .kill_on_drop(true) + .output() + .await?; + + if !output.status.success() { + bail!("failed to generate video thumbnail: {}", String::from_utf8_lossy(&output.stderr)); + } + Ok(()) +} diff --git a/src/core/external/file.rs b/src/core/external/file.rs new file mode 100644 index 00000000..ad5a1259 --- /dev/null +++ b/src/core/external/file.rs @@ -0,0 +1,33 @@ +use std::{collections::BTreeMap, path::PathBuf}; + +use anyhow::{bail, Result}; +use tokio::process::Command; + +use crate::misc::MimeKind; + +pub async fn file(files: &[PathBuf]) -> Result> { + if files.is_empty() { + bail!("no files to get mime types for"); + } + + let output = Command::new("file") + .args(["-bL", "--mime-type"]) + .args(files) + .kill_on_drop(true) + .output() + .await?; + + let output = String::from_utf8_lossy(&output.stdout); + let mimes = BTreeMap::from_iter( + files + .into_iter() + .zip(output.trim().lines()) + .filter(|(_, m)| MimeKind::valid(m)) + .map(|(f, m)| (f.clone(), m.to_string())), + ); + + if mimes.len() == 0 { + bail!("failed to get mime types"); + } + Ok(mimes) +} diff --git a/src/core/external/jq.rs b/src/core/external/jq.rs new file mode 100644 index 00000000..3c1beffc --- /dev/null +++ b/src/core/external/jq.rs @@ -0,0 +1,20 @@ +use std::path::Path; + +use anyhow::{bail, Result}; +use tokio::process::Command; + +use crate::config::PREVIEW; + +pub async fn jq(path: &Path) -> Result { + let output = Command::new("jq") + .args(["-C", "--indent", &PREVIEW.tab_size.to_string(), "."]) + .arg(path) + .kill_on_drop(true) + .output() + .await?; + + if !output.status.success() { + bail!("failed to get json: {}", String::from_utf8_lossy(&output.stderr)); + } + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} diff --git a/src/core/external/mod.rs b/src/core/external/mod.rs index b6dd9a96..1083e744 100644 --- a/src/core/external/mod.rs +++ b/src/core/external/mod.rs @@ -1,9 +1,15 @@ mod fd; +mod ffmpegthumbnailer; +mod file; mod fzf; +mod jq; mod rg; mod zoxide; pub use fd::*; +pub use ffmpegthumbnailer::*; +pub use file::*; pub use fzf::*; +pub use jq::*; pub use rg::*; pub use zoxide::*; diff --git a/src/core/manager/folder.rs b/src/core/manager/folder.rs index 534f1a00..83465f7f 100644 --- a/src/core/manager/folder.rs +++ b/src/core/manager/folder.rs @@ -13,6 +13,7 @@ pub struct Folder { offset: usize, cursor: usize, + pub page: usize, pub hovered: Option, pub in_search: bool, } @@ -24,6 +25,9 @@ impl Folder { Self { cwd: cwd.to_path_buf(), in_search: true, ..Default::default() } } + #[inline] + pub fn limit() -> usize { tty_size().ws_row.saturating_sub(DIR_PADDING) as usize } + pub fn update(&mut self, op: FilesOp) -> bool { let b = match op { FilesOp::Read(_, items) => self.files.update_read(items), @@ -37,6 +41,7 @@ impl Folder { let len = self.files.len(); self.offset = self.offset.min(len); self.cursor = self.cursor.min(len.saturating_sub(1)); + self.set_page(true); if let Some(h) = self.hovered.as_ref().map(|h| h.path()) { self.hover(&h); @@ -46,8 +51,17 @@ impl Folder { true } - #[inline] - pub fn limit() -> usize { tty_size().ws_row.saturating_sub(DIR_PADDING) as usize } + pub fn set_page(&mut self, force: bool) -> bool { + let limit = Self::limit(); + let new = if limit == 0 { 0 } else { self.cursor / limit }; + if !force && self.page == new { + return false; + } + + self.page = new; + emit!(Pages(new)); + true + } pub fn next(&mut self, step: usize) -> bool { let len = self.files.len(); @@ -58,6 +72,7 @@ impl Folder { let old = self.cursor; self.cursor = (self.cursor + step).min(len - 1); self.hovered = self.files.duplicate(self.cursor); + self.set_page(false); let limit = Self::limit(); if self.cursor >= (self.offset + limit).min(len).saturating_sub(5) { @@ -71,6 +86,7 @@ impl Folder { let old = self.cursor; self.cursor = self.cursor.saturating_sub(step); self.hovered = self.files.duplicate(self.cursor); + self.set_page(false); if self.cursor < self.offset + 5 { self.offset = self.offset.saturating_sub(old - self.cursor); @@ -88,7 +104,8 @@ impl Folder { false } - pub fn paginate(&self) -> &Slice { + #[inline] + pub fn window(&self) -> &Slice { let end = (self.offset + Self::limit()).min(self.files.len()); self.files.get_range(self.offset..end).unwrap() } @@ -127,20 +144,6 @@ impl Folder { false } - #[inline] - pub fn has_selected(&self) -> bool { self.files.iter().any(|(_, item)| item.is_selected) } - - pub fn selected(&self) -> Option> { - let v = self - .files - .iter() - .filter(|(_, item)| item.is_selected) - .map(|(path, _)| path.clone()) - .collect::>(); - - if v.is_empty() { None } else { Some(v) } - } - pub fn hover(&mut self, path: &Path) -> bool { if matches!(self.hovered, Some(ref h) if h.path == path) { return false; @@ -168,7 +171,29 @@ impl Folder { self.files.iter().position(|(p, _)| p == path) } + pub fn paginate(&self) -> &Slice { + let max = self.files.len().saturating_sub(1); + let limit = Self::limit(); + + let start = (self.page * limit).min(max); + let end = (start + limit).min(max); + self.files.get_range(start..end).unwrap() + } + #[inline] + pub fn has_selected(&self) -> bool { self.files.iter().any(|(_, item)| item.is_selected) } + + pub fn selected(&self) -> Option> { + let v = self + .files + .iter() + .filter(|(_, item)| item.is_selected) + .map(|(path, _)| path.clone()) + .collect::>(); + + if v.is_empty() { None } else { Some(v) } + } + pub fn rect_current(&self, path: &Path) -> Option { let pos = self.position(path)? - self.offset; let s = tty_size(); diff --git a/src/core/manager/manager.rs b/src/core/manager/manager.rs index 3e6b835c..fd997efc 100644 --- a/src/core/manager/manager.rs +++ b/src/core/manager/manager.rs @@ -1,16 +1,16 @@ -use std::{collections::{BTreeSet, HashMap, HashSet}, mem, path::{Path, PathBuf}}; +use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, mem, path::PathBuf}; use tokio::fs; use super::{PreviewData, Tab, Tabs, Watcher}; -use crate::{core::{files::{File, FilesOp}, input::{InputOpt, InputPos}, manager::Folder, tasks::{Precache, Tasks}}, emit}; +use crate::{core::{external::{self}, files::{File, FilesOp}, input::{InputOpt, InputPos}, manager::Folder, tasks::Tasks}, emit}; pub struct Manager { tabs: Tabs, yanked: (bool, HashSet), - watcher: Watcher, - mimetype: HashMap, + watcher: Watcher, + pub mimetype: HashMap, } impl Manager { @@ -62,10 +62,8 @@ impl Manager { self.active_mut().preview.go(&hovered.path, &mime); } else { tokio::spawn(async move { - if let Ok(mime) = Precache::mimetype(&vec![hovered.path()]).await { - if let Some(Some(mime)) = mime.first() { - emit!(Mimetype(hovered.path, mime.clone())); - } + if let Ok(mimes) = external::file(&[hovered.path()]).await { + emit!(Mimetype(mimes)); } }); } @@ -169,17 +167,12 @@ impl Manager { self.current().selected().or_else(|| self.hovered().map(|h| vec![h.path()])).unwrap_or_default() } - #[inline] - pub fn mimetype(&self, file: &Path) -> Option { self.mimetype.get(file).cloned() } - - pub async fn mimetypes(&mut self, files: &Vec) -> Vec> { + pub async fn mimetypes(&mut self, files: &[PathBuf]) -> Vec> { let todo = files.iter().filter(|&p| !self.mimetype.contains_key(p)).cloned().collect::>(); - if let Ok(mime) = Precache::mimetype(&todo).await { - let mut it = todo.iter().zip(mime); - while let Some((p, Some(m))) = it.next() { - self.mimetype.insert(p.clone(), m); - } + + if let Ok(mimes) = external::file(&todo).await { + self.mimetype.extend(mimes); } files.into_iter().map(|p| self.mimetype.get(p).cloned()).collect() @@ -244,12 +237,16 @@ impl Manager { true } - pub fn update_mimetype(&mut self, path: PathBuf, mimetype: String) -> bool { - if matches!(self.mimetype.get(&path), Some(m) if m == &mimetype) { + pub fn update_mimetype(&mut self, mut mimes: BTreeMap, tasks: &Tasks) -> bool { + mimes.retain(|f, m| self.mimetype.get(f) != Some(m)); + if mimes.is_empty() { return false; } - self.mimetype.insert(path, mimetype); + tasks.precache_image(&mimes); + tasks.precache_video(&mimes); + + self.mimetype.extend(mimes); self.preview(); true } diff --git a/src/core/manager/preview.rs b/src/core/manager/preview.rs index 5c212bd4..6dedc8e1 100644 --- a/src/core/manager/preview.rs +++ b/src/core/manager/preview.rs @@ -6,7 +6,7 @@ use syntect::{easy::HighlightLines, highlighting::{Theme, ThemeSet}, parsing::Sy use tokio::{fs, task::JoinHandle}; use super::{ALL_RATIO, PREVIEW_BORDER, PREVIEW_PADDING, PREVIEW_RATIO}; -use crate::{config::{PREVIEW, THEME}, core::{adapter::Kitty, files::{Files, FilesOp}, tasks::Precache}, emit, misc::{first_n_lines, tty_ratio, tty_size}}; +use crate::{config::{PREVIEW, THEME}, core::{adapter::Kitty, external::{ffmpegthumbnailer, jq}, files::{Files, FilesOp}, tasks::Precache}, emit, misc::{first_n_lines, tty_ratio, tty_size, MimeKind}}; static SYNTECT_SYNTAX: OnceLock = OnceLock::new(); static SYNTECT_THEME: OnceLock = OnceLock::new(); @@ -43,20 +43,15 @@ impl Preview { handle.abort(); } - let (path, mime) = (path.to_path_buf(), mime.to_string()); + let (path, mime) = (path.to_path_buf(), mime.to_owned()); self.handle = Some(tokio::spawn(async move { - let result = if mime == "inode/directory" { - Self::folder(&path).await - } else if mime == "application/json" { - Self::json(&path).await.map(PreviewData::Text) - } else if mime.starts_with("text/") || mime.ends_with("/xml") { - Self::highlight(&path).await.map(PreviewData::Text) - } else if mime.starts_with("image/") { - Self::image(&path).await.map(PreviewData::Image) - } else if mime.starts_with("video/") { - Self::video(&path).await.map(PreviewData::Image) - } else { - Err(anyhow!("Unsupported mimetype: {}", mime)) + let result = match MimeKind::new(&mime) { + MimeKind::Dir => Self::folder(&path).await, + MimeKind::JSON => Self::json(&path).await.map(PreviewData::Text), + MimeKind::Text => Self::highlight(&path).await.map(PreviewData::Text), + MimeKind::Image => Self::image(&path).await.map(PreviewData::Image), + MimeKind::Video => Self::video(&path).await.map(PreviewData::Image), + MimeKind::Others => Err(anyhow!("Unsupported mimetype: {}", mime)), }; emit!(Preview(path, result.unwrap_or_default())); @@ -84,7 +79,7 @@ impl Preview { pub async fn image(mut path: &Path) -> Result> { let cache = Precache::cache(path); - if cache.exists() { + if fs::metadata(&cache).await.is_ok() { path = cache.as_path(); } @@ -108,21 +103,16 @@ impl Preview { } pub async fn video(path: &Path) -> Result> { - Precache::video(path).await?; - let cache = Precache::cache(path); + if fs::metadata(&cache).await.is_err() { + ffmpegthumbnailer(path, &cache).await?; + } + Self::image(&cache).await } pub async fn json(path: &Path) -> Result { - Ok( - Precache::json(path) - .await? - .lines() - .take(Self::size().1 as usize) - .collect::>() - .join("\n"), - ) + Ok(jq(path).await?.lines().take(Self::size().1 as usize).collect::>().join("\n")) } pub async fn highlight(path: &Path) -> Result { diff --git a/src/core/tasks/precache.rs b/src/core/tasks/precache.rs index 8264a389..ec998a37 100644 --- a/src/core/tasks/precache.rs +++ b/src/core/tasks/precache.rs @@ -1,94 +1,123 @@ use std::path::{Path, PathBuf}; -use anyhow::{bail, Result}; +use anyhow::Result; use image::imageops::FilterType; -use tokio::{fs, process::Command}; +use tokio::{fs, sync::mpsc}; -use crate::{config::PREVIEW, misc::valid_mimetype}; +use super::TaskOp; +use crate::{config::PREVIEW, core::external::{self, ffmpegthumbnailer}, emit}; -#[derive(Default)] -pub struct Precache; +pub struct Precache { + rx: async_channel::Receiver, + tx: async_channel::Sender, + + sch: mpsc::UnboundedSender, +} + +#[derive(Debug)] +pub(super) enum PrecacheOp { + Image(PrecacheOpImage), + Video(PrecacheOpVideo), +} + +#[derive(Debug)] +pub(super) struct PrecacheOpMime { + pub id: usize, + pub targets: Vec, +} + +#[derive(Debug)] +pub(super) struct PrecacheOpImage { + pub id: usize, + pub target: PathBuf, +} + +#[derive(Debug)] +pub(super) struct PrecacheOpVideo { + pub id: usize, + pub target: PathBuf, +} impl Precache { - pub fn cache(path: &Path) -> PathBuf { - PathBuf::from(format!("/tmp/yazi/{:x}", md5::compute(path.to_string_lossy().as_bytes()))) + pub(super) fn new(sch: mpsc::UnboundedSender) -> Self { + let (tx, rx) = async_channel::unbounded(); + Self { tx, rx, sch } } - pub async fn mimetype(files: &Vec) -> Result>> { - if files.is_empty() { - return Ok(vec![]); - } - - let output = Command::new("file") - .args(["-bL", "--mime-type"]) - .args(files) - .kill_on_drop(true) - .output() - .await?; - - if !output.status.success() { - bail!("failed to get mimetype: {}", String::from_utf8_lossy(&output.stderr)); - } - - Ok( - String::from_utf8_lossy(&output.stdout) - .trim() - .lines() - .map(|s| if valid_mimetype(s) { Some(s.to_string()) } else { None }) - .collect(), - ) + #[inline] + pub(super) async fn recv(&self) -> Result<(usize, PrecacheOp)> { + Ok(match self.rx.recv().await? { + PrecacheOp::Image(t) => (t.id, PrecacheOp::Image(t)), + PrecacheOp::Video(t) => (t.id, PrecacheOp::Video(t)), + }) } - pub async fn json(path: &Path) -> Result { - let output = Command::new("jq") - .args(["-C", "--indent", &PREVIEW.tab_size.to_string(), "."]) - .arg(path) - .kill_on_drop(true) - .output() - .await?; + pub(super) async fn work(&self, task: &mut PrecacheOp) -> Result<()> { + match task { + PrecacheOp::Image(task) => { + let cache = Self::cache(&task.target); + if fs::metadata(&cache).await.is_ok() { + return Ok(self.sch.send(TaskOp::Adv(task.id, 1, 0))?); + } - if !output.status.success() { - bail!("failed to get json: {}", String::from_utf8_lossy(&output.stderr)); - } - Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) - } + let img = fs::read(&task.target).await.map(|b| image::load_from_memory(&b)); + let img = if let Ok(Ok(img)) = img { + img + } else { + return Ok(self.sch.send(TaskOp::Adv(task.id, 1, 0))?); + }; - pub async fn image(path: &Path) -> Result<()> { - let cache = Self::cache(path); - if cache.exists() { - return Ok(()); - } + let (w, h) = (PREVIEW.max_width, PREVIEW.max_height); + if img.width() <= w && img.height() <= h { + img.save(cache).ok(); + } else { + img.resize(w, h, FilterType::Triangle).save(cache).ok(); + } + self.sch.send(TaskOp::Adv(task.id, 1, 0))?; + } + PrecacheOp::Video(task) => { + let cache = Self::cache(&task.target); + if fs::metadata(&cache).await.is_ok() { + return Ok(self.sch.send(TaskOp::Adv(task.id, 1, 0))?); + } - let img = image::load_from_memory(&fs::read(path).await?)?; - let (w, h) = (PREVIEW.max_width, PREVIEW.max_height); - - let img = if img.width() > w || img.height() > h { - img.resize(w, h, FilterType::Triangle) - } else { - img - }; - Ok(img.save(cache)?) - } - - pub async fn video(path: &Path) -> Result<()> { - let cache = Self::cache(path); - if cache.exists() { - return Ok(()); - } - - let output = Command::new("ffmpegthumbnailer") - .arg("-i") - .arg(path) - .arg("-o") - .arg(cache) - .args(["-q", "6", "-c", "jpeg", "-s", &PREVIEW.max_width.to_string()]) - .kill_on_drop(true) - .output() - .await?; - - if !output.status.success() { - bail!("failed to generate video thumbnail: {}", String::from_utf8_lossy(&output.stderr)); + ffmpegthumbnailer(&task.target, &cache).await.ok(); + self.sch.send(TaskOp::Adv(task.id, 1, 0))?; + } } Ok(()) } + + fn done(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Done(id))?) } + + pub(super) async fn mime(&self, task: PrecacheOpMime) -> Result<()> { + self.sch.send(TaskOp::New(task.id, 0))?; + if let Ok(mimes) = external::file(&task.targets).await { + emit!(Mimetype(mimes)); + } + + self.sch.send(TaskOp::Adv(task.id, 1, 0))?; + self.done(task.id) + } + + pub(super) fn image(&self, id: usize, targets: Vec) -> Result<()> { + for target in targets { + self.sch.send(TaskOp::New(id, 0))?; + self.tx.send_blocking(PrecacheOp::Image(PrecacheOpImage { id, target }))?; + } + self.done(id) + } + + pub(super) fn video(&self, id: usize, targets: Vec) -> Result<()> { + for target in targets { + self.sch.send(TaskOp::New(id, 0))?; + self.tx.send_blocking(PrecacheOp::Video(PrecacheOpVideo { id, target }))?; + } + self.done(id) + } + + #[inline] + pub fn cache(path: &Path) -> PathBuf { + format!("/tmp/yazi/{:x}", md5::compute(path.to_string_lossy().as_bytes())).into() + } } diff --git a/src/core/tasks/scheduler.rs b/src/core/tasks/scheduler.rs index 73d13199..c108ca86 100644 --- a/src/core/tasks/scheduler.rs +++ b/src/core/tasks/scheduler.rs @@ -6,7 +6,7 @@ use parking_lot::RwLock; use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, time::sleep}; use tracing::{info, trace}; -use super::{File, FileOpDelete, FileOpPaste, FileOpTrash, Process, ProcessOpOpen, Task, TaskOp, TaskStage}; +use super::{File, FileOpDelete, FileOpPaste, FileOpTrash, Precache, PrecacheOpMime, Process, ProcessOpOpen, Task, TaskOp, TaskStage}; use crate::{config::open::Opener, emit, misc::unique_path}; #[derive(Default)] @@ -65,8 +65,9 @@ impl Running { } pub struct Scheduler { - file: Arc, - process: Arc, + file: Arc, + precache: Arc, + process: Arc, todo: Sender>, pub(super) running: Arc>, @@ -78,14 +79,15 @@ impl Scheduler { let (prog_tx, prog_rx) = mpsc::unbounded_channel(); let scheduler = Self { - file: Arc::new(File::new(prog_tx.clone())), - process: Arc::new(Process::new(prog_tx)), + file: Arc::new(File::new(prog_tx.clone())), + precache: Arc::new(Precache::new(prog_tx.clone())), + process: Arc::new(Process::new(prog_tx)), todo: todo_tx, running: Default::default(), }; - for _ in 0..3 { + for _ in 0..5 { scheduler.schedule_micro(todo_rx.clone()); } for _ in 0..5 { @@ -107,6 +109,7 @@ impl Scheduler { fn schedule_macro(&self, rx: Receiver>) { let file = self.file.clone(); + let precache = self.precache.clone(); let process = self.process.clone(); let running = self.running.clone(); @@ -132,6 +135,17 @@ impl Scheduler { trace!("Finished task {:?}", task); } } + Ok((id, mut task)) = precache.recv() => { + if !running.read().exists(id) { + trace!("Skipping task {:?} as it was removed", task); + continue; + } + if let Err(e) = precache.work(&mut task).await { + info!("Failed to work on task {:?}: {}", task, e); + } else { + trace!("Finished task {:?}", task); + } + } Ok((id, mut task)) = process.recv() => { if !running.read().exists(id) { trace!("Skipping task {:?} as it was removed", task); @@ -365,4 +379,31 @@ impl Scheduler { .boxed() }); } + + pub(super) fn precache_mime(&self, targets: Vec) { + let name = format!("Mimetype"); + let id = self.running.write().add(name); + + let _ = self.todo.send_blocking({ + let precache = self.precache.clone(); + async move { + precache.mime(PrecacheOpMime { id, targets }).await.ok(); + } + .boxed() + }); + } + + pub(super) fn precache_image(&self, targets: Vec) { + let name = format!("Image"); + let id = self.running.write().add(name); + + self.precache.image(id, targets).ok(); + } + + pub(super) fn precache_video(&self, targets: Vec) { + let name = format!("Video"); + let id = self.running.write().add(name); + + self.precache.video(id, targets).ok(); + } } diff --git a/src/core/tasks/tasks.rs b/src/core/tasks/tasks.rs index 502dac62..090c0f31 100644 --- a/src/core/tasks/tasks.rs +++ b/src/core/tasks/tasks.rs @@ -1,9 +1,9 @@ -use std::{collections::{BTreeMap, HashSet}, path::PathBuf, sync::Arc}; +use std::{collections::{BTreeMap, HashMap, HashSet}, path::PathBuf, sync::Arc}; use tracing::trace; use super::{Scheduler, TASKS_PADDING, TASKS_PERCENT}; -use crate::{config::OPEN, core::input::{InputOpt, InputPos}, emit, misc::tty_size}; +use crate::{config::OPEN, core::{files::File, input::{InputOpt, InputPos}}, emit, misc::{tty_size, MimeKind}}; #[derive(Clone, Debug)] pub struct Task { @@ -173,6 +173,46 @@ impl Tasks { false } + #[inline] + pub fn precache_mime(&self, targets: Vec<&File>, mimetype: &HashMap) -> bool { + let targets = targets + .into_iter() + .filter(|f| f.meta.is_file() && !mimetype.contains_key(&f.path)) + .map(|f| f.path.clone()) + .collect::>(); + + if !targets.is_empty() { + self.scheduler.precache_mime(targets); + } + false + } + + pub fn precache_image(&self, mimetype: &BTreeMap) -> bool { + let targets = mimetype + .into_iter() + .filter(|(_, m)| MimeKind::new(m) == MimeKind::Image) + .map(|(p, _)| p.clone()) + .collect::>(); + + if !targets.is_empty() { + self.scheduler.precache_image(targets); + } + false + } + + pub fn precache_video(&self, mimetype: &BTreeMap) -> bool { + let targets = mimetype + .into_iter() + .filter(|(_, m)| MimeKind::new(m) == MimeKind::Video) + .map(|(p, _)| p.clone()) + .collect::>(); + + if !targets.is_empty() { + self.scheduler.precache_video(targets); + } + false + } + pub fn update_progress(&mut self, percent: u8, left: u32) -> bool { if self.progress.0 == percent { return false; diff --git a/src/misc/fns.rs b/src/misc/fns.rs index 01fd722b..8cb708e4 100644 --- a/src/misc/fns.rs +++ b/src/misc/fns.rs @@ -68,25 +68,3 @@ pub fn optinal_bool(s: &str) -> Option { None } } - -pub fn valid_mimetype(str: &str) -> bool { - let parts = str.split('/').collect::>(); - if parts.len() != 2 { - return false; - } - - let b = match parts[0] { - "application" => true, - "audio" => true, - "example" => true, - "font" => true, - "image" => true, - "message" => true, - "model" => true, - "multipart" => true, - "text" => true, - "video" => true, - _ => false, - }; - b && !parts[1].is_empty() -} diff --git a/src/misc/mime.rs b/src/misc/mime.rs new file mode 100644 index 00000000..5bfc3f14 --- /dev/null +++ b/src/misc/mime.rs @@ -0,0 +1,49 @@ +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum MimeKind { + Dir, + JSON, + Text, + Image, + Video, + Others, +} + +impl MimeKind { + pub fn valid(s: &str) -> bool { + let parts = s.split('/').collect::>(); + if parts.len() != 2 { + return false; + } + + let b = match parts[0] { + "application" => true, + "audio" => true, + "example" => true, + "font" => true, + "image" => true, + "message" => true, + "model" => true, + "multipart" => true, + "text" => true, + "video" => true, + _ => false, + }; + b && !parts[1].is_empty() + } + + pub fn new(s: &str) -> Self { + if s == "inode/directory" { + Self::Dir + } else if s == "application/json" { + Self::JSON + } else if s.starts_with("text/") || s.ends_with("/xml") { + Self::Text + } else if s.starts_with("image/") { + Self::Image + } else if s.starts_with("video/") { + Self::Video + } else { + Self::Others + } + } +} diff --git a/src/misc/mod.rs b/src/misc/mod.rs index 8f37bc15..3d5de665 100644 --- a/src/misc/mod.rs +++ b/src/misc/mod.rs @@ -3,6 +3,7 @@ mod chars; mod defer; mod fns; mod fs; +mod mime; mod tty; pub use buffer::*; @@ -10,4 +11,5 @@ pub use chars::*; pub use defer::*; pub use fns::*; pub use fs::*; +pub use mime::*; pub use tty::*; diff --git a/src/ui/app.rs b/src/ui/app.rs index b1fdd79d..71322ea1 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -66,6 +66,7 @@ impl App { } fn dispatch_resize(&mut self) { + self.cx.manager.current_mut().set_page(true); self.cx.manager.preview(); emit!(Render); } @@ -89,13 +90,19 @@ impl App { emit!(Render); } } - Event::Hover => { - if manager.preview() { + Event::Pages(page) => { + if manager.current().page == page { + let targets = self.cx.manager.current().paginate().into_iter().map(|(_, f)| f).collect(); + self.cx.tasks.precache_mime(targets, &self.cx.manager.mimetype); + } + } + Event::Mimetype(mimes) => { + if manager.update_mimetype(mimes, &self.cx.tasks) { emit!(Render); } } - Event::Mimetype(file, mime) => { - if manager.update_mimetype(file, mime) { + Event::Hover => { + if manager.preview() { emit!(Render); } } diff --git a/src/ui/manager/folder.rs b/src/ui/manager/folder.rs index 7f5efaaa..74a07ea2 100644 --- a/src/ui/manager/folder.rs +++ b/src/ui/manager/folder.rs @@ -28,10 +28,11 @@ impl<'a> Folder<'a> { #[inline] fn file_style(&self, file: &File) -> Style { + let mimetype = &self.cx.manager.mimetype; THEME .filetypes .iter() - .find(|x| x.matches(&file.path, self.cx.manager.mimetype(&file.path), file.meta.is_dir())) + .find(|x| x.matches(&file.path, mimetype.get(&file.path).cloned(), file.meta.is_dir())) .map(|x| x.style.get()) .unwrap_or_else(|| Style::new()) } @@ -39,7 +40,7 @@ impl<'a> Folder<'a> { impl<'a> Widget for Folder<'a> { fn render(self, area: Rect, buf: &mut Buffer) { - let page = self.folder.paginate(); + let page = self.folder.window(); let items = page .iter()