diff --git a/yazi-cli/src/args.rs b/yazi-cli/src/args.rs index b74082eee..c309e2014 100644 --- a/yazi-cli/src/args.rs +++ b/yazi-cli/src/args.rs @@ -57,6 +57,9 @@ pub(super) struct CommandPack { /// Add a package. #[arg(short = 'a', long)] pub(super) add: Option, + /// Delete a package. + #[arg(short = 'd', long)] + pub(super) delete: Option, /// Install all packages. #[arg(short = 'i', long)] pub(super) install: bool, diff --git a/yazi-cli/src/main.rs b/yazi-cli/src/main.rs index 168f7f7cc..e1470017a 100644 --- a/yazi-cli/src/main.rs +++ b/yazi-cli/src/main.rs @@ -73,6 +73,8 @@ async fn run() -> anyhow::Result<()> { package::Package::load().await?.install(true).await?; } else if let Some(repo) = cmd.add { package::Package::load().await?.add(&repo).await?; + } else if let Some(repo) = cmd.delete { + package::Package::load().await?.delete(&repo).await?; } } diff --git a/yazi-cli/src/package/delete.rs b/yazi-cli/src/package/delete.rs new file mode 100644 index 000000000..f9f2e9aeb --- /dev/null +++ b/yazi-cli/src/package/delete.rs @@ -0,0 +1,68 @@ +use anyhow::{Context, Result, bail}; +use tokio::fs; +use yazi_fs::{maybe_exists, ok_or_not_found, remove_dir_clean}; +use yazi_macro::outln; + +use super::Dependency; + +impl Dependency { + pub(super) async fn delete(&self) -> Result<()> { + self.header("Deleting package `{name}`")?; + + let dir = self.target(); + if !maybe_exists(&dir).await { + return Ok(outln!("Not found, skipping")?); + } + + if self.hash != self.hash().await? { + bail!( + "You have modified the contents of the `{}` {}. For safety, the operation has been aborted. +Please manually delete it from: {}", + self.name, + if self.is_flavor { "flavor" } else { "plugin" }, + dir.display() + ); + } + + let files = if self.is_flavor { + &["flavor.toml", "tmtheme.xml", "README.md", "preview.png", "LICENSE", "LICENSE-tmtheme"][..] + } else { + &["main.lua", "README.md", "LICENSE"][..] + }; + for p in files.iter().map(|&f| dir.join(f)) { + ok_or_not_found(fs::remove_file(&p).await) + .with_context(|| format!("failed to delete `{}`", p.display()))?; + } + + self.delete_assets().await?; + if ok_or_not_found(fs::remove_dir(&dir).await).is_ok() { + outln!("Done!")?; + } else { + outln!( + "Done! +For safety, user data has been preserved, please manually delete them within: {}", + dir.display() + )?; + } + + Ok(()) + } + + pub(super) async fn delete_assets(&self) -> Result<()> { + let assets = self.target().join("assets"); + match fs::read_dir(&assets).await { + Ok(mut it) => { + while let Some(entry) = it.next_entry().await? { + fs::remove_file(entry.path()) + .await + .with_context(|| format!("failed to remove `{}`", entry.path().display()))?; + } + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => Err(e).context(format!("failed to read `{}`", assets.display()))?, + }; + + remove_dir_clean(&assets).await; + Ok(()) + } +} diff --git a/yazi-cli/src/package/dependency.rs b/yazi-cli/src/package/dependency.rs index 78e2b5a5b..64b4273c1 100644 --- a/yazi-cli/src/package/dependency.rs +++ b/yazi-cli/src/package/dependency.rs @@ -5,13 +5,13 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use twox_hash::XxHash3_128; use yazi_fs::Xdg; -#[derive(Default)] +#[derive(Clone, Default)] pub(crate) struct Dependency { pub(crate) use_: String, // owner/repo:child pub(crate) name: String, // child.yazi pub(crate) parent: String, // owner/repo - pub(crate) child: String, // child + pub(crate) child: String, // child.yazi pub(crate) rev: String, pub(crate) hash: String, @@ -20,19 +20,30 @@ pub(crate) struct Dependency { } impl Dependency { - #[inline] pub(super) fn local(&self) -> PathBuf { Xdg::state_dir() .join("packages") .join(format!("{:x}", XxHash3_128::oneshot(self.remote().as_bytes()))) } - #[inline] pub(super) fn remote(&self) -> String { // Support more Git hosting services in the future format!("https://github.com/{}.git", self.parent) } + pub(super) fn target(&self) -> PathBuf { + if self.is_flavor { + Xdg::config_dir().join(format!("flavors/{}", self.name)) + } else { + Xdg::config_dir().join(format!("plugins/{}", self.name)) + } + } + + #[inline] + pub(super) fn identical(&self, other: &Self) -> bool { + self.parent == other.parent && self.child == other.child + } + pub(super) fn header(&self, s: &str) -> Result<()> { use crossterm::style::{Attribute, Print, SetAttributes}; diff --git a/yazi-cli/src/package/deploy.rs b/yazi-cli/src/package/deploy.rs index 1f4469e2e..4e92dcfb0 100644 --- a/yazi-cli/src/package/deploy.rs +++ b/yazi-cli/src/package/deploy.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use anyhow::{Context, Result, bail}; use tokio::fs; -use yazi_fs::{Xdg, copy_and_seal, maybe_exists, remove_dir_clean}; +use yazi_fs::{copy_and_seal, maybe_exists}; use yazi_macro::outln; use super::Dependency; @@ -13,17 +13,15 @@ impl Dependency { self.header("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/{}", self.name)) - } else { - Xdg::config_dir().join(format!("plugins/{}", self.name)) - }; + let to = self.target(); if maybe_exists(&to).await && self.hash != self.hash().await? { bail!( - "The user has modified the contents of the `{}` package. For safety, the operation has been aborted. -Please manually delete it from your plugins/flavors directory and re-run the command.", - self.name + "You have modified the contents of the `{}` {}. For safety, the operation has been aborted. +Please manually delete it from `{}` and re-run the command.", + self.name, + if self.is_flavor { "flavor" } else { "plugin" }, + to.display() ); } @@ -51,28 +49,16 @@ Please manually delete it from your plugins/flavors directory and re-run the com .with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()))?; } + self.delete_assets().await?; Self::deploy_assets(from.join("assets"), to.join("assets")).await?; + self.hash = self.hash().await?; outln!("Done!")?; + Ok(()) } async fn deploy_assets(from: PathBuf, to: PathBuf) -> Result<()> { - use std::io::ErrorKind::NotFound; - - match fs::read_dir(&to).await { - Ok(mut it) => { - while let Some(entry) = it.next_entry().await? { - fs::remove_file(entry.path()) - .await - .with_context(|| format!("failed to remove `{}`", entry.path().display()))?; - } - } - Err(e) if e.kind() == NotFound => {} - Err(e) => Err(e).context(format!("failed to read `{}`", to.display()))?, - }; - - remove_dir_clean(&to).await; match fs::read_dir(&from).await { Ok(mut it) => { fs::create_dir_all(&to).await?; @@ -83,10 +69,9 @@ Please manually delete it from your plugins/flavors directory and re-run the com })?; } } - Err(e) if e.kind() == NotFound => {} + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} Err(e) => Err(e).context(format!("failed to read `{}`", from.display()))?, } - Ok(()) } } diff --git a/yazi-cli/src/package/hash.rs b/yazi-cli/src/package/hash.rs index 2cf97c60c..761b72b24 100644 --- a/yazi-cli/src/package/hash.rs +++ b/yazi-cli/src/package/hash.rs @@ -1,18 +1,13 @@ use anyhow::{Context, Result}; use tokio::fs; use twox_hash::XxHash3_128; -use yazi_fs::{Xdg, ok_or_not_found}; +use yazi_fs::ok_or_not_found; use super::Dependency; impl Dependency { pub(crate) async fn hash(&self) -> Result { - let dir = if self.is_flavor { - Xdg::config_dir().join(format!("flavors/{}", self.name)) - } else { - Xdg::config_dir().join(format!("plugins/{}", self.name)) - }; - + let dir = self.target(); let files = if self.is_flavor { &[ "LICENSE", diff --git a/yazi-cli/src/package/install.rs b/yazi-cli/src/package/install.rs index e5cc18fb6..c1860c95d 100644 --- a/yazi-cli/src/package/install.rs +++ b/yazi-cli/src/package/install.rs @@ -5,7 +5,7 @@ use super::{Dependency, Git}; impl Dependency { pub(super) async fn install(&mut self) -> Result<()> { - self.header("Installing package `{name}`")?; + self.header("Fetching package `{name}`")?; let path = self.local(); if !must_exists(&path).await { diff --git a/yazi-cli/src/package/mod.rs b/yazi-cli/src/package/mod.rs index 3328a3744..7dde8fb2f 100644 --- a/yazi-cli/src/package/mod.rs +++ b/yazi-cli/src/package/mod.rs @@ -1,6 +1,6 @@ #![allow(clippy::module_inception)] -yazi_macro::mod_flat!(add dependency deploy git hash install package upgrade); +yazi_macro::mod_flat!(add delete dependency deploy git hash install package upgrade); use anyhow::Context; use yazi_fs::Xdg; diff --git a/yazi-cli/src/package/package.rs b/yazi-cli/src/package/package.rs index f6d503def..670dbab65 100644 --- a/yazi-cli/src/package/package.rs +++ b/yazi-cli/src/package/package.rs @@ -25,11 +25,12 @@ impl Package { pub(crate) async fn add(&mut self, use_: &str) -> Result<()> { let mut dep = Dependency::from_str(use_)?; - if self.plugins.iter().any(|d| d.parent == dep.parent && d.child == dep.child) { - bail!("Plugin `{}` already exists in package.toml", dep.name); - } - if self.flavors.iter().any(|d| d.parent == dep.parent && d.child == dep.child) { - bail!("Flavor `{}` already exists in package.toml", dep.name); + if let Some(d) = self.identical(&dep) { + bail!( + "{} `{}` already exists in package.toml", + if d.is_flavor { "Flavor" } else { "Plugin" }, + dep.name + ) } dep.add().await?; @@ -43,6 +44,22 @@ impl Package { create_and_seal(&Self::toml(), s.as_bytes()).await.context("Failed to write package.toml") } + pub(crate) async fn delete(&mut self, use_: &str) -> Result<()> { + let Some(dep) = self.identical(&Dependency::from_str(use_)?).cloned() else { + bail!("`{}` was not found in package.toml", use_) + }; + + dep.delete().await?; + if dep.is_flavor { + self.flavors.retain(|d| !d.identical(&dep)); + } else { + self.plugins.retain(|d| !d.identical(&dep)); + } + + let s = toml::to_string_pretty(self)?; + create_and_seal(&Self::toml(), s.as_bytes()).await.context("Failed to write package.toml") + } + pub(crate) async fn install(&mut self, upgrade: bool) -> Result<()> { for d in &mut self.plugins { if upgrade { @@ -167,6 +184,11 @@ impl Package { #[inline] fn toml() -> PathBuf { Xdg::config_dir().join("package.toml") } + + #[inline] + fn identical(&self, other: &Dependency) -> Option<&Dependency> { + self.plugins.iter().chain(&self.flavors).find(|d| d.identical(other)) + } } impl<'de> Deserialize<'de> for Package {