mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
feat: precache images and videos
This commit is contained in:
parent
4d002424a9
commit
0766e37100
17 changed files with 457 additions and 209 deletions
62
Cargo.lock
generated
62
Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
23
src/core/external/ffmpegthumbnailer.rs
vendored
Normal 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
33
src/core/external/file.rs
vendored
Normal 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
20
src/core/external/jq.rs
vendored
Normal 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())
|
||||
}
|
||||
6
src/core/external/mod.rs
vendored
6
src/core/external/mod.rs
vendored
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
49
src/misc/mime.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue