diff --git a/Cargo.lock b/Cargo.lock index c9e7f55d..641a66cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2374,6 +2374,7 @@ dependencies = [ "parking_lot", "ratatui", "serde", + "serde_json", "signal-hook-tokio", "syntect", "tokio", diff --git a/Cargo.toml b/Cargo.toml index ae50afcd..5dfe7282 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ once_cell = "1.18.0" parking_lot = "0.12.1" ratatui = "0.21.0" serde = { version = "1.0.164", features = [ "derive" ] } +serde_json = "1.0.103" signal-hook-tokio = { version = "0.3.1", features = [ "futures-v0_3" ] } syntect = "5.0.0" tokio = { version = "1.28.2", features = [ "parking_lot", "macros", "rt-multi-thread", "sync", "fs", "process", "io-std", "io-util", "time" ] } diff --git a/README.md b/README.md index aa75975e..b4039aae 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Yazi ("duck" in Chinese) is a terminal file manager written in Rust, based on non-blocking async I/O. It aims to provide an efficient, user-friendly, and configurable file management experience. -⚠️ Note: Yazi is currently in active development and may be unstable. The API is subject to change without prior notice. Please use it with caution in a non-production environment. +⚠️ Note: Yazi is currently in active development and may be unstable. The API is subject to change without prior notice. ## Installation @@ -10,12 +10,22 @@ Before getting started, ensure that the following dependencies are installed on - nerd-fonts (required, for icons) - jq (optional, for JSON preview) +- unar (optional, for archive preview) - ffmpegthumbnailer (optional, for video thumbnails) - fd (optional, for file searching) - rg (optional, for file content searching) - fzf (optional, for directory jumping) - zoxide (optional, for directory jumping) +```bash +# Arch Linux +pacman -S ttf-nerd-fonts-symbols jq unarchiver ffmpegthumbnailer fd ripgrep fzf zoxide + +# macOS +brew install jq unar ffmpegthumbnailer fd ripgrep fzf zoxide +brew tap homebrew/cask-fonts && brew install --cask font-symbols-only-nerd-font +``` + Execute the following commands to clone the project and build Yazi: ```bash diff --git a/src/core/external/lsar.rs b/src/core/external/lsar.rs new file mode 100644 index 00000000..81aaff28 --- /dev/null +++ b/src/core/external/lsar.rs @@ -0,0 +1,65 @@ +use std::path::Path; + +use anyhow::{bail, Result}; +use serde::Deserialize; +use serde_json::Value; +use tokio::process::Command; +use tracing::info; + +#[derive(Debug)] +pub enum LsarAttr { + Posix(u16), + Windows(u16), + Dos(u8), +} + +#[derive(Debug, Deserialize)] +pub struct LsarFile { + #[serde(rename = "XADFileName")] + pub name: String, + #[serde(rename = "XADLastModificationDate")] + pub last_modified: String, + #[serde(rename = "XADFileSize")] + pub size: Option, + #[serde(rename = "XADCompressedSize")] + pub compressed_size: Option, + #[serde(rename = "XADCompressionName")] + pub compression_name: Option, + + #[serde(skip)] + pub attributes: Option, +} + +pub async fn lsar(path: &Path) -> Result> { + let output = + Command::new("lsar").args(["-j", "-jss"]).arg(path).kill_on_drop(true).output().await?; + + if !output.status.success() { + bail!("failed to get json: {}", String::from_utf8_lossy(&output.stderr)); + } + + #[derive(Deserialize)] + struct Outer { + #[serde(rename = "lsarContents")] + contents: Vec, + } + + let output = String::from_utf8_lossy(&output.stdout); + info!("lsar output: {}", output); + let contents = serde_json::from_str::(output.trim())?.contents; + + let mut files = Vec::with_capacity(contents.len()); + for content in contents { + let mut file = serde_json::from_value::(content.clone())?; + if let Some(p) = content.get("XADPosixPermissions").and_then(|p| p.as_u64()) { + file.attributes = Some(LsarAttr::Posix(p as u16)); + } else if let Some(a) = content.get("XADWindowsFileAttributes").and_then(|a| a.as_u64()) { + file.attributes = Some(LsarAttr::Windows(a as u16)); + } else if let Some(a) = content.get("XADDOSFileAttributes").and_then(|a| a.as_u64()) { + file.attributes = Some(LsarAttr::Dos(a as u8)); + } + + files.push(file); + } + Ok(files) +} diff --git a/src/core/external/mod.rs b/src/core/external/mod.rs index 1083e744..2a07cf73 100644 --- a/src/core/external/mod.rs +++ b/src/core/external/mod.rs @@ -3,6 +3,7 @@ mod ffmpegthumbnailer; mod file; mod fzf; mod jq; +mod lsar; mod rg; mod zoxide; @@ -11,5 +12,6 @@ pub use ffmpegthumbnailer::*; pub use file::*; pub use fzf::*; pub use jq::*; +pub use lsar::*; pub use rg::*; pub use zoxide::*; diff --git a/src/core/manager/manager.rs b/src/core/manager/manager.rs index fd997efc..462b9a88 100644 --- a/src/core/manager/manager.rs +++ b/src/core/manager/manager.rs @@ -3,7 +3,7 @@ use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, mem, path::PathBu use tokio::fs; use super::{PreviewData, Tab, Tabs, Watcher}; -use crate::{core::{external::{self}, files::{File, FilesOp}, input::{InputOpt, InputPos}, manager::Folder, tasks::Tasks}, emit}; +use crate::{core::{external, files::{File, FilesOp}, input::{InputOpt, InputPos}, manager::Folder, tasks::Tasks}, emit}; pub struct Manager { tabs: Tabs, diff --git a/src/core/manager/preview.rs b/src/core/manager/preview.rs index 6dedc8e1..2b92a532 100644 --- a/src/core/manager/preview.rs +++ b/src/core/manager/preview.rs @@ -6,7 +6,7 @@ use syntect::{easy::HighlightLines, highlighting::{Theme, ThemeSet}, parsing::Sy use tokio::{fs, task::JoinHandle}; use super::{ALL_RATIO, PREVIEW_BORDER, PREVIEW_PADDING, PREVIEW_RATIO}; -use crate::{config::{PREVIEW, THEME}, core::{adapter::Kitty, external::{ffmpegthumbnailer, jq}, files::{Files, FilesOp}, tasks::Precache}, emit, misc::{first_n_lines, tty_ratio, tty_size, MimeKind}}; +use crate::{config::{PREVIEW, THEME}, core::{adapter::Kitty, external, files::{Files, FilesOp}, tasks::Precache}, emit, misc::{first_n_lines, tty_ratio, tty_size, MimeKind}}; static SYNTECT_SYNTAX: OnceLock = OnceLock::new(); static SYNTECT_THEME: OnceLock = OnceLock::new(); @@ -51,6 +51,7 @@ impl Preview { 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::Archive => Self::archive(&path).await.map(PreviewData::Text), MimeKind::Others => Err(anyhow!("Unsupported mimetype: {}", mime)), }; @@ -105,14 +106,33 @@ impl Preview { pub async fn video(path: &Path) -> Result> { let cache = Precache::cache(path); if fs::metadata(&cache).await.is_err() { - ffmpegthumbnailer(path, &cache).await?; + external::ffmpegthumbnailer(path, &cache).await?; } Self::image(&cache).await } pub async fn json(path: &Path) -> Result { - Ok(jq(path).await?.lines().take(Self::size().1 as usize).collect::>().join("\n")) + Ok( + external::jq(path) + .await? + .lines() + .take(Self::size().1 as usize) + .collect::>() + .join("\n"), + ) + } + + pub async fn archive(path: &Path) -> Result { + Ok( + external::lsar(path) + .await? + .into_iter() + .take(Self::size().1 as usize) + .map(|f| f.name) + .collect::>() + .join("\n"), + ) } pub async fn highlight(path: &Path) -> Result { diff --git a/src/core/tasks/precache.rs b/src/core/tasks/precache.rs index a95d1ba4..d295ef86 100644 --- a/src/core/tasks/precache.rs +++ b/src/core/tasks/precache.rs @@ -5,7 +5,7 @@ use image::{imageops::FilterType, ImageFormat}; use tokio::{fs, sync::mpsc}; use super::TaskOp; -use crate::{config::PREVIEW, core::external::{self, ffmpegthumbnailer}, emit}; +use crate::{config::PREVIEW, core::external, emit}; pub struct Precache { rx: async_channel::Receiver, @@ -79,7 +79,7 @@ impl Precache { return Ok(self.sch.send(TaskOp::Adv(task.id, 1, 0))?); } - ffmpegthumbnailer(&task.target, &cache).await.ok(); + external::ffmpegthumbnailer(&task.target, &cache).await.ok(); self.sch.send(TaskOp::Adv(task.id, 1, 0))?; } } diff --git a/src/misc/mime.rs b/src/misc/mime.rs index 5bfc3f14..bdfb5009 100644 --- a/src/misc/mime.rs +++ b/src/misc/mime.rs @@ -5,6 +5,7 @@ pub enum MimeKind { Text, Image, Video, + Archive, Others, } @@ -42,6 +43,15 @@ impl MimeKind { Self::Image } else if s.starts_with("video/") { Self::Video + } else if s == "application/x-bzip" + || s == "application/x-bzip2" + || s == "application/gzip" + || s == "application/vnd.rar" + || s == "application/x-tar" + || s == "application/zip" + || s == "application/x-7z-compressed" + { + Self::Archive } else { Self::Others }