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]]
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",
]

View file

@ -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<PathBuf, String>),
Hover,
Mimetype(PathBuf, String),
Preview(PathBuf, PreviewData),
// Input
Input(InputOpt, oneshot::Sender<Result<String>>),
// Tasks
Open(Vec<PathBuf>),
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();

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 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::*;

View file

@ -13,6 +13,7 @@ pub struct Folder {
offset: usize,
cursor: usize,
pub page: usize,
pub hovered: Option<File>,
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<PathBuf, File> {
#[inline]
pub fn window(&self) -> &Slice<PathBuf, File> {
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<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 {
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<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]
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> {
let pos = self.position(path)? - self.offset;
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 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<PathBuf>),
watcher: Watcher,
mimetype: HashMap<PathBuf, String>,
watcher: Watcher,
pub mimetype: HashMap<PathBuf, String>,
}
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<String> { self.mimetype.get(file).cloned() }
pub async fn mimetypes(&mut self, files: &Vec<PathBuf>) -> Vec<Option<String>> {
pub async fn mimetypes(&mut self, files: &[PathBuf]) -> Vec<Option<String>> {
let todo =
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);
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<PathBuf, String>, 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
}

View file

@ -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<SyntaxSet> = OnceLock::new();
static SYNTECT_THEME: OnceLock<Theme> = 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<Vec<u8>> {
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<Vec<u8>> {
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<String> {
Ok(
Precache::json(path)
.await?
.lines()
.take(Self::size().1 as usize)
.collect::<Vec<_>>()
.join("\n"),
)
Ok(jq(path).await?.lines().take(Self::size().1 as usize).collect::<Vec<_>>().join("\n"))
}
pub async fn highlight(path: &Path) -> Result<String> {

View file

@ -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<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 {
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<TaskOp>) -> Self {
let (tx, rx) = async_channel::unbounded();
Self { tx, rx, sch }
}
pub async fn mimetype(files: &Vec<PathBuf>) -> Result<Vec<Option<String>>> {
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<String> {
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<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 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<File>,
process: Arc<Process>,
file: Arc<File>,
precache: Arc<Precache>,
process: Arc<Process>,
todo: Sender<BoxFuture<'static, ()>>,
pub(super) running: Arc<RwLock<Running>>,
@ -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<BoxFuture<'static, ()>>) {
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<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 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<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 {
if self.progress.0 == percent {
return false;

View file

@ -68,25 +68,3 @@ pub fn optinal_bool(s: &str) -> Option<bool> {
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 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::*;

View file

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

View file

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