feat: precache images and videos

This commit is contained in:
sxyazi 2023-07-16 21:20:31 +08:00
parent 4d002424a9
commit 0766e37100
No known key found for this signature in database
17 changed files with 457 additions and 209 deletions

62
Cargo.lock generated
View file

@ -54,9 +54,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.71" version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
[[package]] [[package]]
name = "async-channel" name = "async-channel"
@ -77,7 +77,7 @@ checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
@ -532,7 +532,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
@ -847,9 +847,9 @@ dependencies = [
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.8" version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]] [[package]]
name = "jpeg-decoder" name = "jpeg-decoder"
@ -1201,7 +1201,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
@ -1257,9 +1257,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.64" version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" checksum = "92de25114670a878b1261c79c9f8f729fb97e95bac93f6312f583c60dd6a1dfe"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -1316,9 +1316,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.29" version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" checksum = "5907a1b7c277254a8b15170f6e7c97cfa60ee7872a3217663bb81151e48184bb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -1458,15 +1458,15 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.13" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.14" version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]] [[package]]
name = "safemem" name = "safemem"
@ -1506,14 +1506,14 @@ checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.102" version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5062a995d481b2308b6064e9af76011f2921c35f97b0468811ed9f6cd91dfed" checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -1540,9 +1540,9 @@ dependencies = [
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.15" version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" checksum = "b824b6e687aff278cdbf3b36f07aa52d4bd4099699324d5da86a2ebce3aa00b3"
dependencies = [ dependencies = [
"libc", "libc",
"signal-hook-registry", "signal-hook-registry",
@ -1633,9 +1633,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.25" version = "2.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1688,7 +1688,7 @@ checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
@ -1793,7 +1793,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
@ -1844,9 +1844,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.19.13" version = "0.19.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f8751d9c1b03c6500c387e96f81f815a4f8e72d142d2d4a9ffa6fedd51ddee7" checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
dependencies = [ dependencies = [
"indexmap 2.0.0", "indexmap 2.0.0",
"serde", "serde",
@ -1946,7 +1946,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.25", "syn 2.0.26",
] ]
[[package]] [[package]]
@ -2018,9 +2018,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.10" version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
@ -2106,7 +2106,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.25", "syn 2.0.26",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2128,7 +2128,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.25", "syn 2.0.26",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]

View file

@ -1,4 +1,4 @@
use std::path::PathBuf; use std::{collections::BTreeMap, path::PathBuf};
use anyhow::Result; use anyhow::Result;
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
@ -15,15 +15,19 @@ pub enum Event {
Render(String), Render(String),
Resize(u16, u16), Resize(u16, u16),
// Manager
Cd(PathBuf), Cd(PathBuf),
Refresh, Refresh,
Files(FilesOp), Files(FilesOp),
Pages(usize),
Mimetype(BTreeMap<PathBuf, String>),
Hover, Hover,
Mimetype(PathBuf, String),
Preview(PathBuf, PreviewData), Preview(PathBuf, PreviewData),
// Input
Input(InputOpt, oneshot::Sender<Result<String>>), Input(InputOpt, oneshot::Sender<Result<String>>),
// Tasks
Open(Vec<PathBuf>), Open(Vec<PathBuf>),
Progress(u8, u32), Progress(u8, u32),
} }
@ -73,8 +77,11 @@ macro_rules! emit {
(Files($op:expr)) => { (Files($op:expr)) => {
$crate::core::Event::Files($op).emit(); $crate::core::Event::Files($op).emit();
}; };
(Mimetype($path:expr, $mime:expr)) => { (Pages($page:expr)) => {
$crate::core::Event::Mimetype($path, $mime).emit(); $crate::core::Event::Pages($page).emit();
};
(Mimetype($mimes:expr)) => {
$crate::core::Event::Mimetype($mimes).emit();
}; };
(Preview($path:expr, $data:expr)) => { (Preview($path:expr, $data:expr)) => {
$crate::core::Event::Preview($path, $data).emit(); $crate::core::Event::Preview($path, $data).emit();

23
src/core/external/ffmpegthumbnailer.rs vendored Normal file
View file

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

33
src/core/external/file.rs vendored Normal file
View file

@ -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<BTreeMap<PathBuf, String>> {
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)
}

20
src/core/external/jq.rs vendored Normal file
View file

@ -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<String> {
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())
}

View file

@ -1,9 +1,15 @@
mod fd; mod fd;
mod ffmpegthumbnailer;
mod file;
mod fzf; mod fzf;
mod jq;
mod rg; mod rg;
mod zoxide; mod zoxide;
pub use fd::*; pub use fd::*;
pub use ffmpegthumbnailer::*;
pub use file::*;
pub use fzf::*; pub use fzf::*;
pub use jq::*;
pub use rg::*; pub use rg::*;
pub use zoxide::*; pub use zoxide::*;

View file

@ -13,6 +13,7 @@ pub struct Folder {
offset: usize, offset: usize,
cursor: usize, cursor: usize,
pub page: usize,
pub hovered: Option<File>, pub hovered: Option<File>,
pub in_search: bool, pub in_search: bool,
} }
@ -24,6 +25,9 @@ impl Folder {
Self { cwd: cwd.to_path_buf(), in_search: true, ..Default::default() } 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 { pub fn update(&mut self, op: FilesOp) -> bool {
let b = match op { let b = match op {
FilesOp::Read(_, items) => self.files.update_read(items), FilesOp::Read(_, items) => self.files.update_read(items),
@ -37,6 +41,7 @@ impl Folder {
let len = self.files.len(); let len = self.files.len();
self.offset = self.offset.min(len); self.offset = self.offset.min(len);
self.cursor = self.cursor.min(len.saturating_sub(1)); 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()) { if let Some(h) = self.hovered.as_ref().map(|h| h.path()) {
self.hover(&h); self.hover(&h);
@ -46,8 +51,17 @@ impl Folder {
true true
} }
#[inline] pub fn set_page(&mut self, force: bool) -> bool {
pub fn limit() -> usize { tty_size().ws_row.saturating_sub(DIR_PADDING) as usize } 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 { pub fn next(&mut self, step: usize) -> bool {
let len = self.files.len(); let len = self.files.len();
@ -58,6 +72,7 @@ impl Folder {
let old = self.cursor; let old = self.cursor;
self.cursor = (self.cursor + step).min(len - 1); self.cursor = (self.cursor + step).min(len - 1);
self.hovered = self.files.duplicate(self.cursor); self.hovered = self.files.duplicate(self.cursor);
self.set_page(false);
let limit = Self::limit(); let limit = Self::limit();
if self.cursor >= (self.offset + limit).min(len).saturating_sub(5) { if self.cursor >= (self.offset + limit).min(len).saturating_sub(5) {
@ -71,6 +86,7 @@ impl Folder {
let old = self.cursor; let old = self.cursor;
self.cursor = self.cursor.saturating_sub(step); self.cursor = self.cursor.saturating_sub(step);
self.hovered = self.files.duplicate(self.cursor); self.hovered = self.files.duplicate(self.cursor);
self.set_page(false);
if self.cursor < self.offset + 5 { if self.cursor < self.offset + 5 {
self.offset = self.offset.saturating_sub(old - self.cursor); self.offset = self.offset.saturating_sub(old - self.cursor);
@ -88,7 +104,8 @@ impl Folder {
false false
} }
pub fn paginate(&self) -> &Slice<PathBuf, File> { #[inline]
pub fn window(&self) -> &Slice<PathBuf, File> {
let end = (self.offset + Self::limit()).min(self.files.len()); let end = (self.offset + Self::limit()).min(self.files.len());
self.files.get_range(self.offset..end).unwrap() self.files.get_range(self.offset..end).unwrap()
} }
@ -127,20 +144,6 @@ impl Folder {
false false
} }
#[inline]
pub fn has_selected(&self) -> bool { self.files.iter().any(|(_, item)| item.is_selected) }
pub fn selected(&self) -> Option<Vec<PathBuf>> {
let v = self
.files
.iter()
.filter(|(_, item)| item.is_selected)
.map(|(path, _)| path.clone())
.collect::<Vec<_>>();
if v.is_empty() { None } else { Some(v) }
}
pub fn hover(&mut self, path: &Path) -> bool { pub fn hover(&mut self, path: &Path) -> bool {
if matches!(self.hovered, Some(ref h) if h.path == path) { if matches!(self.hovered, Some(ref h) if h.path == path) {
return false; return false;
@ -168,7 +171,29 @@ impl Folder {
self.files.iter().position(|(p, _)| p == path) self.files.iter().position(|(p, _)| p == path)
} }
pub fn paginate(&self) -> &Slice<PathBuf, File> {
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] #[inline]
pub fn has_selected(&self) -> bool { self.files.iter().any(|(_, item)| item.is_selected) }
pub fn selected(&self) -> Option<Vec<PathBuf>> {
let v = self
.files
.iter()
.filter(|(_, item)| item.is_selected)
.map(|(path, _)| path.clone())
.collect::<Vec<_>>();
if v.is_empty() { None } else { Some(v) }
}
pub fn rect_current(&self, path: &Path) -> Option<Rect> { pub fn rect_current(&self, path: &Path) -> Option<Rect> {
let pos = self.position(path)? - self.offset; let pos = self.position(path)? - self.offset;
let s = tty_size(); let s = tty_size();

View file

@ -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 tokio::fs;
use super::{PreviewData, Tab, Tabs, Watcher}; 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 { pub struct Manager {
tabs: Tabs, tabs: Tabs,
yanked: (bool, HashSet<PathBuf>), yanked: (bool, HashSet<PathBuf>),
watcher: Watcher, watcher: Watcher,
mimetype: HashMap<PathBuf, String>, pub mimetype: HashMap<PathBuf, String>,
} }
impl Manager { impl Manager {
@ -62,10 +62,8 @@ impl Manager {
self.active_mut().preview.go(&hovered.path, &mime); self.active_mut().preview.go(&hovered.path, &mime);
} else { } else {
tokio::spawn(async move { tokio::spawn(async move {
if let Ok(mime) = Precache::mimetype(&vec![hovered.path()]).await { if let Ok(mimes) = external::file(&[hovered.path()]).await {
if let Some(Some(mime)) = mime.first() { emit!(Mimetype(mimes));
emit!(Mimetype(hovered.path, mime.clone()));
}
} }
}); });
} }
@ -169,17 +167,12 @@ impl Manager {
self.current().selected().or_else(|| self.hovered().map(|h| vec![h.path()])).unwrap_or_default() self.current().selected().or_else(|| self.hovered().map(|h| vec![h.path()])).unwrap_or_default()
} }
#[inline] pub async fn mimetypes(&mut self, files: &[PathBuf]) -> Vec<Option<String>> {
pub fn mimetype(&self, file: &Path) -> Option<String> { self.mimetype.get(file).cloned() }
pub async fn mimetypes(&mut self, files: &Vec<PathBuf>) -> Vec<Option<String>> {
let todo = let todo =
files.iter().filter(|&p| !self.mimetype.contains_key(p)).cloned().collect::<Vec<_>>(); files.iter().filter(|&p| !self.mimetype.contains_key(p)).cloned().collect::<Vec<_>>();
if let Ok(mime) = Precache::mimetype(&todo).await {
let mut it = todo.iter().zip(mime); if let Ok(mimes) = external::file(&todo).await {
while let Some((p, Some(m))) = it.next() { self.mimetype.extend(mimes);
self.mimetype.insert(p.clone(), m);
}
} }
files.into_iter().map(|p| self.mimetype.get(p).cloned()).collect() files.into_iter().map(|p| self.mimetype.get(p).cloned()).collect()
@ -244,12 +237,16 @@ impl Manager {
true true
} }
pub fn update_mimetype(&mut self, path: PathBuf, mimetype: String) -> bool { pub fn update_mimetype(&mut self, mut mimes: BTreeMap<PathBuf, String>, tasks: &Tasks) -> bool {
if matches!(self.mimetype.get(&path), Some(m) if m == &mimetype) { mimes.retain(|f, m| self.mimetype.get(f) != Some(m));
if mimes.is_empty() {
return false; return false;
} }
self.mimetype.insert(path, mimetype); tasks.precache_image(&mimes);
tasks.precache_video(&mimes);
self.mimetype.extend(mimes);
self.preview(); self.preview();
true true
} }

View file

@ -6,7 +6,7 @@ use syntect::{easy::HighlightLines, highlighting::{Theme, ThemeSet}, parsing::Sy
use tokio::{fs, task::JoinHandle}; use tokio::{fs, task::JoinHandle};
use super::{ALL_RATIO, PREVIEW_BORDER, PREVIEW_PADDING, PREVIEW_RATIO}; 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<SyntaxSet> = OnceLock::new(); static SYNTECT_SYNTAX: OnceLock<SyntaxSet> = OnceLock::new();
static SYNTECT_THEME: OnceLock<Theme> = OnceLock::new(); static SYNTECT_THEME: OnceLock<Theme> = OnceLock::new();
@ -43,20 +43,15 @@ impl Preview {
handle.abort(); 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 { self.handle = Some(tokio::spawn(async move {
let result = if mime == "inode/directory" { let result = match MimeKind::new(&mime) {
Self::folder(&path).await MimeKind::Dir => Self::folder(&path).await,
} else if mime == "application/json" { MimeKind::JSON => Self::json(&path).await.map(PreviewData::Text),
Self::json(&path).await.map(PreviewData::Text) MimeKind::Text => Self::highlight(&path).await.map(PreviewData::Text),
} else if mime.starts_with("text/") || mime.ends_with("/xml") { MimeKind::Image => Self::image(&path).await.map(PreviewData::Image),
Self::highlight(&path).await.map(PreviewData::Text) MimeKind::Video => Self::video(&path).await.map(PreviewData::Image),
} else if mime.starts_with("image/") { MimeKind::Others => Err(anyhow!("Unsupported mimetype: {}", mime)),
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))
}; };
emit!(Preview(path, result.unwrap_or_default())); emit!(Preview(path, result.unwrap_or_default()));
@ -84,7 +79,7 @@ impl Preview {
pub async fn image(mut path: &Path) -> Result<Vec<u8>> { pub async fn image(mut path: &Path) -> Result<Vec<u8>> {
let cache = Precache::cache(path); let cache = Precache::cache(path);
if cache.exists() { if fs::metadata(&cache).await.is_ok() {
path = cache.as_path(); path = cache.as_path();
} }
@ -108,21 +103,16 @@ impl Preview {
} }
pub async fn video(path: &Path) -> Result<Vec<u8>> { pub async fn video(path: &Path) -> Result<Vec<u8>> {
Precache::video(path).await?;
let cache = Precache::cache(path); let cache = Precache::cache(path);
if fs::metadata(&cache).await.is_err() {
ffmpegthumbnailer(path, &cache).await?;
}
Self::image(&cache).await Self::image(&cache).await
} }
pub async fn json(path: &Path) -> Result<String> { pub async fn json(path: &Path) -> Result<String> {
Ok( Ok(jq(path).await?.lines().take(Self::size().1 as usize).collect::<Vec<_>>().join("\n"))
Precache::json(path)
.await?
.lines()
.take(Self::size().1 as usize)
.collect::<Vec<_>>()
.join("\n"),
)
} }
pub async fn highlight(path: &Path) -> Result<String> { pub async fn highlight(path: &Path) -> Result<String> {

View file

@ -1,94 +1,123 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::{bail, Result}; use anyhow::Result;
use image::imageops::FilterType; 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<PrecacheOp>,
tx: async_channel::Sender<PrecacheOp>,
sch: mpsc::UnboundedSender<TaskOp>,
}
#[derive(Debug)]
pub(super) enum PrecacheOp {
Image(PrecacheOpImage),
Video(PrecacheOpVideo),
}
#[derive(Debug)]
pub(super) struct PrecacheOpMime {
pub id: usize,
pub targets: Vec<PathBuf>,
}
#[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 { impl Precache {
pub fn cache(path: &Path) -> PathBuf { pub(super) fn new(sch: mpsc::UnboundedSender<TaskOp>) -> Self {
PathBuf::from(format!("/tmp/yazi/{:x}", md5::compute(path.to_string_lossy().as_bytes()))) let (tx, rx) = async_channel::unbounded();
Self { tx, rx, sch }
} }
pub async fn mimetype(files: &Vec<PathBuf>) -> Result<Vec<Option<String>>> { #[inline]
if files.is_empty() { pub(super) async fn recv(&self) -> Result<(usize, PrecacheOp)> {
return Ok(vec![]); Ok(match self.rx.recv().await? {
} PrecacheOp::Image(t) => (t.id, PrecacheOp::Image(t)),
PrecacheOp::Video(t) => (t.id, PrecacheOp::Video(t)),
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(),
)
} }
pub async fn json(path: &Path) -> Result<String> { pub(super) async fn work(&self, task: &mut PrecacheOp) -> Result<()> {
let output = Command::new("jq") match task {
.args(["-C", "--indent", &PREVIEW.tab_size.to_string(), "."]) PrecacheOp::Image(task) => {
.arg(path) let cache = Self::cache(&task.target);
.kill_on_drop(true) if fs::metadata(&cache).await.is_ok() {
.output() return Ok(self.sch.send(TaskOp::Adv(task.id, 1, 0))?);
.await?; }
if !output.status.success() { let img = fs::read(&task.target).await.map(|b| image::load_from_memory(&b));
bail!("failed to get json: {}", String::from_utf8_lossy(&output.stderr)); let img = if let Ok(Ok(img)) = img {
} img
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } else {
} return Ok(self.sch.send(TaskOp::Adv(task.id, 1, 0))?);
};
pub async fn image(path: &Path) -> Result<()> { let (w, h) = (PREVIEW.max_width, PREVIEW.max_height);
let cache = Self::cache(path); if img.width() <= w && img.height() <= h {
if cache.exists() { img.save(cache).ok();
return 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?)?; ffmpegthumbnailer(&task.target, &cache).await.ok();
let (w, h) = (PREVIEW.max_width, PREVIEW.max_height); self.sch.send(TaskOp::Adv(task.id, 1, 0))?;
}
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));
} }
Ok(()) 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<PathBuf>) -> 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<PathBuf>) -> 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()
}
} }

View file

@ -6,7 +6,7 @@ use parking_lot::RwLock;
use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, time::sleep}; use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, time::sleep};
use tracing::{info, trace}; 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}; use crate::{config::open::Opener, emit, misc::unique_path};
#[derive(Default)] #[derive(Default)]
@ -65,8 +65,9 @@ impl Running {
} }
pub struct Scheduler { pub struct Scheduler {
file: Arc<File>, file: Arc<File>,
process: Arc<Process>, precache: Arc<Precache>,
process: Arc<Process>,
todo: Sender<BoxFuture<'static, ()>>, todo: Sender<BoxFuture<'static, ()>>,
pub(super) running: Arc<RwLock<Running>>, pub(super) running: Arc<RwLock<Running>>,
@ -78,14 +79,15 @@ impl Scheduler {
let (prog_tx, prog_rx) = mpsc::unbounded_channel(); let (prog_tx, prog_rx) = mpsc::unbounded_channel();
let scheduler = Self { let scheduler = Self {
file: Arc::new(File::new(prog_tx.clone())), file: Arc::new(File::new(prog_tx.clone())),
process: Arc::new(Process::new(prog_tx)), precache: Arc::new(Precache::new(prog_tx.clone())),
process: Arc::new(Process::new(prog_tx)),
todo: todo_tx, todo: todo_tx,
running: Default::default(), running: Default::default(),
}; };
for _ in 0..3 { for _ in 0..5 {
scheduler.schedule_micro(todo_rx.clone()); scheduler.schedule_micro(todo_rx.clone());
} }
for _ in 0..5 { for _ in 0..5 {
@ -107,6 +109,7 @@ impl Scheduler {
fn schedule_macro(&self, rx: Receiver<BoxFuture<'static, ()>>) { fn schedule_macro(&self, rx: Receiver<BoxFuture<'static, ()>>) {
let file = self.file.clone(); let file = self.file.clone();
let precache = self.precache.clone();
let process = self.process.clone(); let process = self.process.clone();
let running = self.running.clone(); let running = self.running.clone();
@ -132,6 +135,17 @@ impl Scheduler {
trace!("Finished task {:?}", task); 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() => { Ok((id, mut task)) = process.recv() => {
if !running.read().exists(id) { if !running.read().exists(id) {
trace!("Skipping task {:?} as it was removed", task); trace!("Skipping task {:?} as it was removed", task);
@ -365,4 +379,31 @@ impl Scheduler {
.boxed() .boxed()
}); });
} }
pub(super) fn precache_mime(&self, targets: Vec<PathBuf>) {
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<PathBuf>) {
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<PathBuf>) {
let name = format!("Video");
let id = self.running.write().add(name);
self.precache.video(id, targets).ok();
}
} }

View file

@ -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 tracing::trace;
use super::{Scheduler, TASKS_PADDING, TASKS_PERCENT}; 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)] #[derive(Clone, Debug)]
pub struct Task { pub struct Task {
@ -173,6 +173,46 @@ impl Tasks {
false false
} }
#[inline]
pub fn precache_mime(&self, targets: Vec<&File>, mimetype: &HashMap<PathBuf, String>) -> bool {
let targets = targets
.into_iter()
.filter(|f| f.meta.is_file() && !mimetype.contains_key(&f.path))
.map(|f| f.path.clone())
.collect::<Vec<_>>();
if !targets.is_empty() {
self.scheduler.precache_mime(targets);
}
false
}
pub fn precache_image(&self, mimetype: &BTreeMap<PathBuf, String>) -> bool {
let targets = mimetype
.into_iter()
.filter(|(_, m)| MimeKind::new(m) == MimeKind::Image)
.map(|(p, _)| p.clone())
.collect::<Vec<_>>();
if !targets.is_empty() {
self.scheduler.precache_image(targets);
}
false
}
pub fn precache_video(&self, mimetype: &BTreeMap<PathBuf, String>) -> bool {
let targets = mimetype
.into_iter()
.filter(|(_, m)| MimeKind::new(m) == MimeKind::Video)
.map(|(p, _)| p.clone())
.collect::<Vec<_>>();
if !targets.is_empty() {
self.scheduler.precache_video(targets);
}
false
}
pub fn update_progress(&mut self, percent: u8, left: u32) -> bool { pub fn update_progress(&mut self, percent: u8, left: u32) -> bool {
if self.progress.0 == percent { if self.progress.0 == percent {
return false; return false;

View file

@ -68,25 +68,3 @@ pub fn optinal_bool(s: &str) -> Option<bool> {
None None
} }
} }
pub fn valid_mimetype(str: &str) -> bool {
let parts = str.split('/').collect::<Vec<_>>();
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()
}

49
src/misc/mime.rs Normal file
View file

@ -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::<Vec<_>>();
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
}
}
}

View file

@ -3,6 +3,7 @@ mod chars;
mod defer; mod defer;
mod fns; mod fns;
mod fs; mod fs;
mod mime;
mod tty; mod tty;
pub use buffer::*; pub use buffer::*;
@ -10,4 +11,5 @@ pub use chars::*;
pub use defer::*; pub use defer::*;
pub use fns::*; pub use fns::*;
pub use fs::*; pub use fs::*;
pub use mime::*;
pub use tty::*; pub use tty::*;

View file

@ -66,6 +66,7 @@ impl App {
} }
fn dispatch_resize(&mut self) { fn dispatch_resize(&mut self) {
self.cx.manager.current_mut().set_page(true);
self.cx.manager.preview(); self.cx.manager.preview();
emit!(Render); emit!(Render);
} }
@ -89,13 +90,19 @@ impl App {
emit!(Render); emit!(Render);
} }
} }
Event::Hover => { Event::Pages(page) => {
if manager.preview() { 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); emit!(Render);
} }
} }
Event::Mimetype(file, mime) => { Event::Hover => {
if manager.update_mimetype(file, mime) { if manager.preview() {
emit!(Render); emit!(Render);
} }
} }

View file

@ -28,10 +28,11 @@ impl<'a> Folder<'a> {
#[inline] #[inline]
fn file_style(&self, file: &File) -> Style { fn file_style(&self, file: &File) -> Style {
let mimetype = &self.cx.manager.mimetype;
THEME THEME
.filetypes .filetypes
.iter() .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()) .map(|x| x.style.get())
.unwrap_or_else(|| Style::new()) .unwrap_or_else(|| Style::new())
} }
@ -39,7 +40,7 @@ impl<'a> Folder<'a> {
impl<'a> Widget for Folder<'a> { impl<'a> Widget for Folder<'a> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let page = self.folder.paginate(); let page = self.folder.window();
let items = page let items = page
.iter() .iter()