From faa1d9f37b7900fc8fdaa40bd67d2c4885ef78ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Tue, 7 May 2024 13:42:45 +0800 Subject: [PATCH] feat: package manager (#985) --- Cargo.lock | 36 +++--- README.md | 1 + yazi-adaptor/Cargo.toml | 2 +- yazi-boot/Cargo.toml | 2 +- yazi-boot/src/boot.rs | 8 +- yazi-cli/Cargo.toml | 6 +- yazi-cli/src/args.rs | 15 +++ yazi-cli/src/main.rs | 13 ++ yazi-cli/src/package/add.rs | 20 ++++ yazi-cli/src/package/deploy.rs | 50 ++++++++ yazi-cli/src/package/git.rs | 55 +++++++++ yazi-cli/src/package/install.rs | 25 ++++ yazi-cli/src/package/mod.rs | 17 +++ yazi-cli/src/package/package.rs | 79 ++++++++++++ yazi-cli/src/package/parser.rs | 113 ++++++++++++++++++ yazi-cli/src/package/upgrade.rs | 9 ++ yazi-config/Cargo.toml | 2 +- yazi-core/Cargo.toml | 8 +- yazi-core/src/folder/files.rs | 4 +- yazi-core/src/manager/commands/bulk_rename.rs | 4 +- yazi-core/src/manager/commands/create.rs | 4 +- yazi-core/src/manager/commands/rename.rs | 4 +- yazi-dds/Cargo.toml | 6 +- yazi-fm/Cargo.toml | 2 +- yazi-plugin/Cargo.toml | 10 +- yazi-scheduler/Cargo.toml | 4 +- yazi-scheduler/src/file/file.rs | 4 +- yazi-shared/Cargo.toml | 6 +- yazi-shared/src/chars.rs | 10 ++ yazi-shared/src/fs/fns.rs | 6 +- yazi-shared/src/fs/path.rs | 4 +- 31 files changed, 471 insertions(+), 58 deletions(-) create mode 100644 yazi-cli/src/package/add.rs create mode 100644 yazi-cli/src/package/deploy.rs create mode 100644 yazi-cli/src/package/git.rs create mode 100644 yazi-cli/src/package/install.rs create mode 100644 yazi-cli/src/package/mod.rs create mode 100644 yazi-cli/src/package/package.rs create mode 100644 yazi-cli/src/package/parser.rs create mode 100644 yazi-cli/src/package/upgrade.rs diff --git a/Cargo.lock b/Cargo.lock index e34c9328..c8f84bf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -168,9 +168,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "better-panic" @@ -1086,9 +1086,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libredox" @@ -1408,9 +1408,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -1695,9 +1695,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.198" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" dependencies = [ "serde_derive", ] @@ -1714,9 +1714,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" dependencies = [ "proc-macro2", "quote", @@ -2257,9 +2257,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "url" @@ -2280,9 +2280,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uzers" -version = "0.11.3" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d283dc7e8c901e79e32d077866eaf599156cbf427fffa8289aecc52c5c3f63" +checksum = "7d85875e16d59b3b1549efce83ff8251a64923b03bef94add0a1862847448de4" dependencies = [ "libc", "log", @@ -2696,7 +2696,7 @@ version = "0.2.5" dependencies = [ "anyhow", "arc-swap", - "base64 0.22.0", + "base64 0.22.1", "color_quant", "crossterm", "futures", @@ -2735,10 +2735,14 @@ dependencies = [ "clap_complete", "clap_complete_fig", "clap_complete_nushell", + "crossterm", + "md-5", "serde_json", "tokio", + "toml_edit", "vergen", "yazi-dds", + "yazi-shared", ] [[package]] @@ -2842,7 +2846,7 @@ version = "0.2.5" dependencies = [ "ansi-to-tui", "anyhow", - "base64 0.22.0", + "base64 0.22.1", "clipboard-win", "crossterm", "futures", diff --git a/README.md b/README.md index 617e74b7..96ecbf0e 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Yazi (means "duck") is a terminal file manager written in Rust, based on non-blo - 🌟 **Built-in Code Highlighting and Image Decoding**: Combined with the pre-loading mechanism, greatly accelerates image and normal file loading. - 🔌 **Concurrent Plugin System**: UI plugins (rewriting most of the UI), functional plugins, custom previewer, and custom preloader; Just some pieces of Lua. - 📡 **Data Distribution Service**: Built on a client-server architecture (no additional server process required), integrated with a Lua-based publish-subscribe model, achieving cross-instance communication and state persistence. +- 📦 **Package Manager**: Install plugins and themes with one command, keeping them always up to date, or pin them to a specific version. - 🧰 Integration with fd, rg, fzf, zoxide - 💫 Vim-like input/select/which/notify component, auto-completion for cd paths - 🏷️ Multi-Tab Support, Cross-directory selection, Scrollable Preview (for videos, PDFs, archives, directories, code, etc.) diff --git a/yazi-adaptor/Cargo.toml b/yazi-adaptor/Cargo.toml index 1fa24f38..c0b04f71 100644 --- a/yazi-adaptor/Cargo.toml +++ b/yazi-adaptor/Cargo.toml @@ -15,7 +15,7 @@ yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies anyhow = "1.0.82" arc-swap = "1.7.1" -base64 = "0.22.0" +base64 = "0.22.1" color_quant = "1.1.0" crossterm = "0.27.0" futures = "0.3.30" diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index 0a60541a..0478716a 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -15,7 +15,7 @@ yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies clap = { version = "4.5.4", features = [ "derive" ] } -serde = { version = "1.0.198", features = [ "derive" ] } +serde = { version = "1.0.199", features = [ "derive" ] } [build-dependencies] clap = { version = "4.5.4", features = [ "derive" ] } diff --git a/yazi-boot/src/boot.rs b/yazi-boot/src/boot.rs index f2cab5ad..ae6b3900 100644 --- a/yazi-boot/src/boot.rs +++ b/yazi-boot/src/boot.rs @@ -148,7 +148,7 @@ impl Default for Boot { .map(|s| s.split(',').map(|s| s.to_owned()).collect()) .unwrap_or_default(); - let boot = Self { + Self { cwd, file, @@ -159,11 +159,7 @@ impl Default for Boot { plugin_dir: config_dir.join("plugins"), config_dir, state_dir: Xdg::state_dir(), - }; - - std::fs::create_dir_all(&boot.flavor_dir).ok(); - std::fs::create_dir_all(&boot.plugin_dir).ok(); - boot + } } } diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index c4ac33a9..b7d0ee19 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -9,13 +9,17 @@ homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" [dependencies] -yazi-dds = { path = "../yazi-dds", version = "0.2.5" } +yazi-dds = { path = "../yazi-dds", version = "0.2.5" } +yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies anyhow = "1.0.82" clap = { version = "4.5.4", features = [ "derive" ] } +crossterm = "0.27.0" +md-5 = "0.10.6" serde_json = "1.0.116" tokio = { version = "1.37.0", features = [ "full" ] } +toml_edit = "0.22.12" [build-dependencies] anyhow = "1.0.82" diff --git a/yazi-cli/src/args.rs b/yazi-cli/src/args.rs index 3f69cb08..1c447511 100644 --- a/yazi-cli/src/args.rs +++ b/yazi-cli/src/args.rs @@ -20,6 +20,8 @@ pub(super) enum Command { Pub(CommandPub), /// Publish a static message to all remote instances. PubStatic(CommandPubStatic), + /// Manage packages. + Pack(CommandPack), } #[derive(clap::Args)] @@ -79,3 +81,16 @@ impl CommandPubStatic { } } } + +#[derive(clap::Args)] +pub(super) struct CommandPack { + /// Add a package. + #[arg(short = 'a', long)] + pub(super) add: Option, + /// Install all packages. + #[arg(short = 'i', long)] + pub(super) install: bool, + /// Upgrade all packages. + #[arg(short = 'u', long)] + pub(super) upgrade: bool, +} diff --git a/yazi-cli/src/main.rs b/yazi-cli/src/main.rs index a8c74b03..7c1b7ee2 100644 --- a/yazi-cli/src/main.rs +++ b/yazi-cli/src/main.rs @@ -1,4 +1,5 @@ mod args; +mod package; use args::*; use clap::Parser; @@ -30,6 +31,18 @@ async fn main() -> anyhow::Result<()> { std::process::exit(1); } } + Command::Pack(cmd) => { + package::init(); + if cmd.install { + package::Package::install_from_config("plugin", false).await?; + package::Package::install_from_config("flavor", false).await?; + } else if cmd.upgrade { + package::Package::install_from_config("plugin", true).await?; + package::Package::install_from_config("flavor", true).await?; + } else if let Some(repo) = &cmd.add { + package::Package::add_to_config(repo).await?; + } + } } Ok(()) diff --git a/yazi-cli/src/package/add.rs b/yazi-cli/src/package/add.rs new file mode 100644 index 00000000..a4de93ec --- /dev/null +++ b/yazi-cli/src/package/add.rs @@ -0,0 +1,20 @@ +use anyhow::Result; +use yazi_shared::fs::must_exists; + +use super::{Git, Package}; + +impl Package { + pub(super) async fn add(&mut self) -> Result<()> { + self.output("Upgrading package `{name}`")?; + + let path = self.local(); + if !must_exists(&path).await { + Git::clone(&self.remote(), &path).await?; + } else { + Git::pull(&path).await?; + }; + + self.commit = Git::hash(&path).await?; + self.deploy().await + } +} diff --git a/yazi-cli/src/package/deploy.rs b/yazi-cli/src/package/deploy.rs new file mode 100644 index 00000000..dfa8b7e0 --- /dev/null +++ b/yazi-cli/src/package/deploy.rs @@ -0,0 +1,50 @@ +use anyhow::{bail, Context, Result}; +use tokio::fs; +use yazi_shared::{fs::{maybe_exists, must_exists}, Xdg}; + +use super::Package; + +const TRACKER: &str = "DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY"; + +impl Package { + pub(super) async fn deploy(&mut self) -> Result<()> { + let Some(name) = self.name().map(ToOwned::to_owned) else { bail!("Invalid package url") }; + let from = self.local().join(&self.child); + + self.output("Deploying package `{name}`")?; + self.is_flavor = maybe_exists(&from.join("flavor.toml")).await; + let to = if self.is_flavor { + Xdg::config_dir().join(format!("flavors/{name}")) + } else { + Xdg::config_dir().join(format!("plugins/{name}")) + }; + + let tracker = to.join(TRACKER); + if maybe_exists(&to).await && !must_exists(&tracker).await { + bail!( + "A user package with the same name `{name}` already exists. +For safety, please manually delete it from your plugin/flavor directory and re-run the command." + ); + } + + fs::create_dir_all(&to).await?; + fs::write(tracker, []).await?; + + let files = if self.is_flavor { + &["flavor.toml", "tmtheme.xml", "README.md", "preview.png", "LICENSE", "LICENSE-tmtheme"][..] + } else { + &["init.lua", "README.md", "LICENSE"][..] + }; + + for file in files { + let (from, to) = (from.join(file), to.join(file)); + + fs::copy(&from, &to) + .await + .with_context(|| format!("Failed to copy `{}` to `{}`", from.display(), to.display()))?; + } + + println!("Done!"); + Ok(()) + } +} diff --git a/yazi-cli/src/package/git.rs b/yazi-cli/src/package/git.rs new file mode 100644 index 00000000..d02a3d45 --- /dev/null +++ b/yazi-cli/src/package/git.rs @@ -0,0 +1,55 @@ +use std::path::Path; + +use anyhow::{bail, Context, Result}; +use tokio::process::Command; +use yazi_shared::strip_trailing_newline; + +pub(super) struct Git; + +impl Git { + pub(super) async fn clone(url: &str, path: &Path) -> Result<()> { + Self::exec(|c| c.args(["clone", url]).arg(path)).await + } + + pub(super) async fn fetch(path: &Path) -> Result<()> { + Self::exec(|c| c.current_dir(path).arg("fetch")).await + } + + pub(super) async fn checkout(path: &Path, commit: &str) -> Result<()> { + Self::exec(|c| c.current_dir(path).args(["checkout", commit])).await + } + + pub(super) async fn pull(path: &Path) -> Result<()> { + Self::fetch(path).await?; + Self::checkout(path, "origin/HEAD").await?; + Ok(()) + } + + pub(super) async fn hash(path: &Path) -> Result { + let output = Command::new("git") + .current_dir(path) + .args(["rev-parse", "--short", "HEAD"]) + .output() + .await + .context("Failed to get current commit hash")?; + + if !output.status.success() { + bail!("Getting commit hash failed: {}", output.status); + } + + Ok(strip_trailing_newline( + String::from_utf8(output.stdout).context("Failed to parse commit hash")?, + )) + } + + async fn exec(f: impl FnOnce(&mut Command) -> &mut Command) -> Result<()> { + let status = + f(&mut Command::new("git")).status().await.context("Failed to execute `git` command")?; + + if !status.success() { + bail!("`git` command failed: {status}"); + } + + Ok(()) + } +} diff --git a/yazi-cli/src/package/install.rs b/yazi-cli/src/package/install.rs new file mode 100644 index 00000000..a770fc79 --- /dev/null +++ b/yazi-cli/src/package/install.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use yazi_shared::fs::must_exists; + +use super::{Git, Package}; + +impl Package { + pub(super) async fn install(&mut self) -> Result<()> { + self.output("Installing package `{name}`")?; + + let path = self.local(); + if !must_exists(&path).await { + Git::clone(&self.remote(), &path).await?; + } else { + Git::fetch(&path).await?; + }; + + if self.commit.is_empty() { + self.commit = Git::hash(&path).await?; + } else { + Git::checkout(&path, self.commit.trim_start_matches('=')).await?; + } + + self.deploy().await + } +} diff --git a/yazi-cli/src/package/mod.rs b/yazi-cli/src/package/mod.rs new file mode 100644 index 00000000..2e7ec2a1 --- /dev/null +++ b/yazi-cli/src/package/mod.rs @@ -0,0 +1,17 @@ +#![allow(clippy::module_inception)] + +mod add; +mod deploy; +mod git; +mod install; +mod package; +mod parser; +mod upgrade; + +use git::*; +pub(super) use package::*; + +pub(super) fn init() { + let root = yazi_shared::Xdg::state_dir().join("packages"); + std::fs::create_dir_all(root).expect("Failed to create packages directory"); +} diff --git a/yazi-cli/src/package/package.rs b/yazi-cli/src/package/package.rs new file mode 100644 index 00000000..bee1db87 --- /dev/null +++ b/yazi-cli/src/package/package.rs @@ -0,0 +1,79 @@ +use std::{borrow::Cow, io::BufWriter, path::PathBuf}; + +use anyhow::Result; +use md5::{Digest, Md5}; +use yazi_shared::Xdg; + +pub(crate) struct Package { + pub(crate) repo: String, + pub(crate) child: String, + pub(crate) commit: String, + pub(super) is_flavor: bool, +} + +impl Package { + pub(super) fn new(url: &str, commit: Option<&str>) -> Self { + let mut parts = url.splitn(2, '#'); + + let mut repo = parts.next().unwrap_or_default().to_owned(); + let child = if let Some(s) = parts.next() { + format!("{s}.yazi") + } else { + repo.push_str(".yazi"); + String::new() + }; + + Self { repo, child, commit: commit.unwrap_or_default().to_owned(), is_flavor: false } + } + + #[inline] + pub(super) fn use_(&self) -> Cow { + if self.child.is_empty() { + self.repo.trim_end_matches(".yazi").into() + } else { + format!("{}#{}", self.repo, self.child.trim_end_matches(".yazi")).into() + } + } + + #[inline] + pub(super) fn name(&self) -> Option<&str> { + let s = if self.child.is_empty() { + self.repo.split('/').last().filter(|s| !s.is_empty()) + } else { + Some(self.child.as_str()) + }; + + s.filter(|s| s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-' | b'.'))) + } + + #[inline] + pub(super) fn local(&self) -> PathBuf { + Xdg::state_dir() + .join("packages") + .join(format!("{:x}", Md5::new_with_prefix(self.remote()).finalize())) + } + + #[inline] + pub(super) fn remote(&self) -> String { + // Support more Git hosting services in the future + format!("https://github.com/{}.git", self.repo) + } + + pub(super) fn output(&self, s: &str) -> Result<()> { + use crossterm::style::{Attribute, Print, SetAttributes}; + + crossterm::execute!( + BufWriter::new(std::io::stdout()), + Print("\n"), + SetAttributes(Attribute::Reverse.into()), + SetAttributes(Attribute::Bold.into()), + Print(" "), + Print(s.replacen("{name}", self.name().unwrap_or_default(), 1)), + Print(" "), + SetAttributes(Attribute::NoBold.into()), + SetAttributes(Attribute::NoReverse.into()), + Print("\n\n"), + )?; + Ok(()) + } +} diff --git a/yazi-cli/src/package/parser.rs b/yazi-cli/src/package/parser.rs new file mode 100644 index 00000000..28c63981 --- /dev/null +++ b/yazi-cli/src/package/parser.rs @@ -0,0 +1,113 @@ +use anyhow::{bail, Context, Result}; +use tokio::fs; +use toml_edit::{Array, DocumentMut, InlineTable, Item, Value}; +use yazi_shared::Xdg; + +use super::Package; + +impl Package { + pub(crate) async fn add_to_config(use_: &str) -> Result<()> { + let mut package = Self::new(use_, None); + let Some(name) = package.name() else { bail!("Invalid package `use`") }; + + let path = Xdg::config_dir().join("package.toml"); + let mut doc = Self::ensure_config(&fs::read_to_string(&path).await.unwrap_or_default())?; + + Self::ensure_unique(&doc, name)?; + package.add().await?; + + let mut table = InlineTable::new(); + table.insert("use", package.use_().as_ref().into()); + if !package.commit.is_empty() { + table.insert("commit", package.commit.into()); + } + + if package.is_flavor { + doc["flavor"]["deps"].as_array_mut().unwrap().push(table); + } else { + doc["plugin"]["deps"].as_array_mut().unwrap().push(table); + } + + fs::write(path, doc.to_string()).await?; + Ok(()) + } + + pub(crate) async fn install_from_config(section: &str, upgrade: bool) -> Result<()> { + let path = Xdg::config_dir().join("package.toml"); + let Ok(s) = fs::read_to_string(&path).await else { + return Ok(()); + }; + + let mut doc = s.parse::().context("Failed to parse package.toml")?; + let Some(deps) = doc.get_mut(section).and_then(|d| d.get_mut("deps")) else { + return Ok(()); + }; + + let deps = deps.as_array_mut().context("`deps` must be an array")?; + for dep in deps.iter_mut() { + let dep = dep.as_inline_table_mut().context("Dependency must be an inline table")?; + let use_ = dep.get("use").and_then(|d| d.as_str()).context("Missing `use` field")?; + let commit = dep.get("commit").and_then(|d| d.as_str()); + + let mut package = Package::new(use_, commit); + if upgrade { + package.upgrade().await?; + } else { + package.install().await?; + } + + if package.commit.is_empty() { + dep.remove("commit"); + } else { + dep.insert("commit", package.commit.into()); + } + } + + fs::write(path, doc.to_string()).await.context("Failed to write package.toml") + } + + fn ensure_config(s: &str) -> Result { + let mut doc = s.parse::().context("Failed to parse package.toml")?; + + doc + .entry("plugin") + .or_insert(toml_edit::table()) + .as_table_mut() + .context("Failed to get `plugin` table")? + .entry("deps") + .or_insert(Item::Value(Array::new().into())) + .as_array() + .context("Failed to get `deps` array")?; + + doc + .entry("flavor") + .or_insert(toml_edit::table()) + .as_table_mut() + .context("Failed to get `flavor` table")? + .entry("deps") + .or_insert(Item::Value(Array::new().into())) + .as_array() + .context("Failed to get `deps` array")?; + + Ok(doc) + } + + fn ensure_unique(doc: &DocumentMut, name: &str) -> Result<()> { + #[inline] + fn same(v: &Value, name: &str) -> bool { + v.as_inline_table() + .and_then(|t| t.get("use")) + .and_then(|v| v.as_str()) + .is_some_and(|s| Package::new(s, None).name() == Some(name)) + } + + if doc["plugin"]["deps"].as_array().unwrap().into_iter().any(|v| same(v, name)) { + bail!("Plugin `{name}` already exists in package.toml"); + } + if doc["flavor"]["deps"].as_array().unwrap().into_iter().any(|v| same(v, name)) { + bail!("Flavor `{name}` already exists in package.toml"); + } + + Ok(()) + } +} diff --git a/yazi-cli/src/package/upgrade.rs b/yazi-cli/src/package/upgrade.rs new file mode 100644 index 00000000..24c9df12 --- /dev/null +++ b/yazi-cli/src/package/upgrade.rs @@ -0,0 +1,9 @@ +use anyhow::Result; + +use super::Package; + +impl Package { + pub(super) async fn upgrade(&mut self) -> Result<()> { + if self.commit.starts_with('=') { Ok(()) } else { self.add().await } + } +} diff --git a/yazi-config/Cargo.toml b/yazi-config/Cargo.toml index 2ca80cff..764bc2c8 100644 --- a/yazi-config/Cargo.toml +++ b/yazi-config/Cargo.toml @@ -18,7 +18,7 @@ crossterm = "0.27.0" globset = "0.4.14" indexmap = "2.2.6" ratatui = "=0.26.1" -serde = { version = "1.0.198", features = [ "derive" ] } +serde = { version = "1.0.199", features = [ "derive" ] } shell-words = "1.1.0" toml = { version = "0.8.12", features = [ "preserve_order" ] } validator = { version = "0.18.1", features = [ "derive" ] } diff --git a/yazi-core/Cargo.toml b/yazi-core/Cargo.toml index edddfec7..c60bde23 100644 --- a/yazi-core/Cargo.toml +++ b/yazi-core/Cargo.toml @@ -24,18 +24,18 @@ bitflags = "2.5.0" crossterm = "0.27.0" futures = "0.3.30" notify = { version = "6.1.1", default-features = false, features = [ "macos_fsevent" ] } -parking_lot = "0.12.1" +parking_lot = "0.12.2" ratatui = "=0.26.1" regex = "1.10.4" scopeguard = "1.2.0" -serde = "1.0.198" +serde = "1.0.199" tokio = { version = "1.37.0", features = [ "full" ] } tokio-stream = "0.1.15" tokio-util = "0.7.10" -unicode-width = "0.1.11" +unicode-width = "0.1.12" # Logging tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] } [target."cfg(unix)".dependencies] -libc = "0.2.153" +libc = "0.2.154" diff --git a/yazi-core/src/folder/files.rs b/yazi-core/src/folder/files.rs index 9558fa6c..f1297a7b 100644 --- a/yazi-core/src/folder/files.rs +++ b/yazi-core/src/folder/files.rs @@ -2,7 +2,7 @@ use std::{collections::{HashMap, HashSet}, fs::Metadata, mem, ops::Deref, sync:: use tokio::{fs::{self, DirEntry}, select, sync::mpsc::{self, UnboundedReceiver}}; use yazi_config::{manager::SortBy, MANAGER}; -use yazi_shared::fs::{accessible, File, FilesOp, Url, FILES_TICKET}; +use yazi_shared::fs::{maybe_exists, File, FilesOp, Url, FILES_TICKET}; use super::{FilesSorter, Filter}; @@ -101,7 +101,7 @@ impl Files { Ok(m) if mtime == m.modified().ok() => {} Ok(m) => return Some(m), Err(e) => { - if accessible(url).await { + if maybe_exists(url).await { FilesOp::IOErr(url.clone(), e.kind()).emit(); } else if let Some(p) = url.parent_url() { FilesOp::Deleting(p, vec![url.clone()]).emit(); diff --git a/yazi-core/src/manager/commands/bulk_rename.rs b/yazi-core/src/manager/commands/bulk_rename.rs index b7f630e5..e57bd2b4 100644 --- a/yazi-core/src/manager/commands/bulk_rename.rs +++ b/yazi-core/src/manager/commands/bulk_rename.rs @@ -6,7 +6,7 @@ use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}}; use yazi_config::{OPEN, PREVIEW}; use yazi_dds::Pubsub; use yazi_proxy::{AppProxy, TasksProxy, HIDER, WATCHER}; -use yazi_shared::{fs::{accessible, max_common_root, File, FilesOp, Url}, term::Term}; +use yazi_shared::{fs::{max_common_root, maybe_exists, File, FilesOp, Url}, term::Term}; use crate::manager::Manager; @@ -84,7 +84,7 @@ impl Manager { for (o, n) in todo { let (old, new) = (root.join(&o), root.join(&n)); - if accessible(&new).await { + if maybe_exists(&new).await { failed.push((o, n, anyhow!("Destination already exists"))); } else if let Err(e) = fs::rename(&old, &new).await { failed.push((o, n, e.into())); diff --git a/yazi-core/src/manager/commands/create.rs b/yazi-core/src/manager/commands/create.rs index 6e41386a..2f3c4a01 100644 --- a/yazi-core/src/manager/commands/create.rs +++ b/yazi-core/src/manager/commands/create.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use tokio::fs; use yazi_config::popup::InputCfg; use yazi_proxy::{InputProxy, ManagerProxy}; -use yazi_shared::{event::Cmd, fs::{accessible, File, FilesOp, Url}}; +use yazi_shared::{event::Cmd, fs::{maybe_exists, File, FilesOp, Url}}; use crate::manager::Manager; @@ -26,7 +26,7 @@ impl Manager { }; let path = cwd.join(&name); - if !opt.force && accessible(&path).await { + if !opt.force && maybe_exists(&path).await { match InputProxy::show(InputCfg::overwrite()).recv().await { Some(Ok(c)) if c == "y" || c == "Y" => (), _ => return Ok(()), diff --git a/yazi-core/src/manager/commands/rename.rs b/yazi-core/src/manager/commands/rename.rs index d4d7d491..3892060b 100644 --- a/yazi-core/src/manager/commands/rename.rs +++ b/yazi-core/src/manager/commands/rename.rs @@ -5,7 +5,7 @@ use tokio::fs; use yazi_config::popup::InputCfg; use yazi_dds::Pubsub; use yazi_proxy::{InputProxy, ManagerProxy, WATCHER}; -use yazi_shared::{event::Cmd, fs::{accessible, File, FilesOp, Url}}; +use yazi_shared::{event::Cmd, fs::{maybe_exists, File, FilesOp, Url}}; use crate::manager::Manager; @@ -62,7 +62,7 @@ impl Manager { } let new = hovered.parent().unwrap().join(name); - if opt.force || !accessible(&new).await { + if opt.force || !maybe_exists(&new).await { Self::rename_do(tab, hovered, Url::from(new)).await.ok(); return; } diff --git a/yazi-dds/Cargo.toml b/yazi-dds/Cargo.toml index e7aa134b..e5ec8fec 100644 --- a/yazi-dds/Cargo.toml +++ b/yazi-dds/Cargo.toml @@ -19,12 +19,12 @@ yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies anyhow = "1.0.82" mlua = { version = "0.9.7", features = [ "lua54" ] } -parking_lot = "0.12.1" -serde = { version = "1.0.198", features = [ "derive" ] } +parking_lot = "0.12.2" +serde = { version = "1.0.199", features = [ "derive" ] } serde_json = "1.0.116" tokio = { version = "1.37.0", features = [ "full" ] } tokio-stream = "0.1.15" tokio-util = "0.7.10" [target."cfg(unix)".dependencies] -uzers = "0.11.3" +uzers = "0.12.0" diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index 0c1a1e41..409e37cf 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -41,7 +41,7 @@ tracing-appender = "0.2.3" tracing-subscriber = "0.3.18" [target."cfg(unix)".dependencies] -libc = "0.2.153" +libc = "0.2.154" signal-hook-tokio = { version = "0.3.1", features = [ "futures-v0_3" ] } [target.'cfg(all(not(target_os = "macos"), not(target_os = "windows")))'.dependencies] diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index 4c89ac79..4122648b 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -23,28 +23,28 @@ yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies ansi-to-tui = "3.1.0" anyhow = "1.0.82" -base64 = "0.22.0" +base64 = "0.22.1" crossterm = "0.27.0" futures = "0.3.30" md-5 = "0.10.6" mlua = { version = "0.9.7", features = [ "lua54", "serialize", "macros", "async" ] } -parking_lot = "0.12.1" +parking_lot = "0.12.2" ratatui = "=0.26.1" -serde = "1.0.198" +serde = "1.0.199" serde_json = "1.0.116" shell-escape = "0.1.5" shell-words = "1.1.0" syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } tokio = { version = "1.37.0", features = [ "full" ] } tokio-util = "0.7.10" -unicode-width = "0.1.11" +unicode-width = "0.1.12" yazi-prebuild = "0.1.2" # Logging tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] } [target."cfg(unix)".dependencies] -uzers = "0.11.3" +uzers = "0.12.0" [target."cfg(windows)".dependencies] clipboard-win = "5.3.1" diff --git a/yazi-scheduler/Cargo.toml b/yazi-scheduler/Cargo.toml index d35b11e1..85e8d918 100644 --- a/yazi-scheduler/Cargo.toml +++ b/yazi-scheduler/Cargo.toml @@ -19,7 +19,7 @@ yazi-shared = { path = "../yazi-shared", version = "0.2.5" } anyhow = "1.0.82" async-priority-channel = "0.2.0" futures = "0.3.30" -parking_lot = "0.12.1" +parking_lot = "0.12.2" scopeguard = "1.2.0" tokio = { version = "1.37.0", features = [ "full" ] } @@ -27,7 +27,7 @@ tokio = { version = "1.37.0", features = [ "full" ] } tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] } [target."cfg(unix)".dependencies] -libc = "0.2.153" +libc = "0.2.154" [target.'cfg(not(target_os = "android"))'.dependencies] trash = "4.1.0" diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 8b495e26..19189ebf 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -5,7 +5,7 @@ use futures::{future::BoxFuture, FutureExt}; use tokio::{fs, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; use tracing::warn; use yazi_config::TASKS; -use yazi_shared::fs::{accessible, calculate_size, copy_with_progress, path_relative_to, Url}; +use yazi_shared::fs::{calculate_size, copy_with_progress, maybe_exists, path_relative_to, Url}; use super::{FileOp, FileOpDelete, FileOpLink, FileOpPaste, FileOpTrash}; use crate::{TaskOp, TaskProg, LOW, NORMAL}; @@ -108,7 +108,7 @@ impl File { } FileOp::Delete(task) => { if let Err(e) = fs::remove_file(&task.target).await { - if e.kind() != NotFound && accessible(&task.target).await { + if e.kind() != NotFound && maybe_exists(&task.target).await { self.fail(task.id, format!("Delete task failed: {:?}, {e}", task))?; Err(e)? } diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 0fd111ff..b2a14a84 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -15,15 +15,15 @@ crossterm = "0.27.0" dirs = "5.0.1" filetime = "0.2.23" futures = "0.3.30" -parking_lot = "0.12.1" +parking_lot = "0.12.2" percent-encoding = "2.3.1" ratatui = "=0.26.1" regex = "1.10.4" -serde = { version = "1.0.198", features = [ "derive" ] } +serde = { version = "1.0.199", features = [ "derive" ] } tokio = { version = "1.37.0", features = [ "full" ] } # Logging tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] } [target."cfg(unix)".dependencies] -libc = "0.2.153" +libc = "0.2.154" diff --git a/yazi-shared/src/chars.rs b/yazi-shared/src/chars.rs index 83fa989b..33f55ea9 100644 --- a/yazi-shared/src/chars.rs +++ b/yazi-shared/src/chars.rs @@ -18,3 +18,13 @@ impl CharKind { } } } + +pub fn strip_trailing_newline(mut s: String) -> String { + if s.ends_with('\n') { + s.pop(); + } + if s.ends_with('\r') { + s.pop(); + } + s +} diff --git a/yazi-shared/src/fs/fns.rs b/yazi-shared/src/fs/fns.rs index 97934573..186f9b33 100644 --- a/yazi-shared/src/fs/fns.rs +++ b/yazi-shared/src/fs/fns.rs @@ -4,8 +4,10 @@ use anyhow::Result; use filetime::{set_file_mtime, FileTime}; use tokio::{fs, io, select, sync::{mpsc, oneshot}, time}; -pub async fn accessible(path: &Path) -> bool { - match fs::symlink_metadata(path).await { +pub async fn must_exists(p: impl AsRef) -> bool { fs::symlink_metadata(p).await.is_ok() } + +pub async fn maybe_exists(p: impl AsRef) -> bool { + match fs::symlink_metadata(p).await { Ok(_) => true, Err(e) => e.kind() != io::ErrorKind::NotFound, } diff --git a/yazi-shared/src/fs/path.rs b/yazi-shared/src/fs/path.rs index c145ca7c..91c07cb4 100644 --- a/yazi-shared/src/fs/path.rs +++ b/yazi-shared/src/fs/path.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, env, ffi::OsString, path::{Component, Path, PathBuf, MAIN_SEPARATOR}}; -use super::accessible; +use super::maybe_exists; use crate::fs::Url; #[inline] @@ -76,7 +76,7 @@ pub async fn unique_path(mut p: Url) -> Url { .unwrap_or_default(); let mut i = 0; - while accessible(&p).await { + while maybe_exists(&p).await { i += 1; let mut name = OsString::with_capacity(stem.len() + ext.len() + 5);