feat: new ya pack -d subcommand to delete packages (#2181)

Co-authored-by: sxyazi <sxyazi@gmail.com>
This commit is contained in:
Valter Santos 2025-01-11 17:08:13 +13:00 committed by GitHub
parent 856f37b5b3
commit 5c88f26896
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 130 additions and 44 deletions

View file

@ -57,6 +57,9 @@ pub(super) struct CommandPack {
/// Add a package. /// Add a package.
#[arg(short = 'a', long)] #[arg(short = 'a', long)]
pub(super) add: Option<String>, pub(super) add: Option<String>,
/// Delete a package.
#[arg(short = 'd', long)]
pub(super) delete: Option<String>,
/// Install all packages. /// Install all packages.
#[arg(short = 'i', long)] #[arg(short = 'i', long)]
pub(super) install: bool, pub(super) install: bool,

View file

@ -73,6 +73,8 @@ async fn run() -> anyhow::Result<()> {
package::Package::load().await?.install(true).await?; package::Package::load().await?.install(true).await?;
} else if let Some(repo) = cmd.add { } else if let Some(repo) = cmd.add {
package::Package::load().await?.add(&repo).await?; package::Package::load().await?.add(&repo).await?;
} else if let Some(repo) = cmd.delete {
package::Package::load().await?.delete(&repo).await?;
} }
} }

View file

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

View file

@ -5,13 +5,13 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
use twox_hash::XxHash3_128; use twox_hash::XxHash3_128;
use yazi_fs::Xdg; use yazi_fs::Xdg;
#[derive(Default)] #[derive(Clone, Default)]
pub(crate) struct Dependency { pub(crate) struct Dependency {
pub(crate) use_: String, // owner/repo:child pub(crate) use_: String, // owner/repo:child
pub(crate) name: String, // child.yazi pub(crate) name: String, // child.yazi
pub(crate) parent: String, // owner/repo pub(crate) parent: String, // owner/repo
pub(crate) child: String, // child pub(crate) child: String, // child.yazi
pub(crate) rev: String, pub(crate) rev: String,
pub(crate) hash: String, pub(crate) hash: String,
@ -20,19 +20,30 @@ pub(crate) struct Dependency {
} }
impl Dependency { impl Dependency {
#[inline]
pub(super) fn local(&self) -> PathBuf { pub(super) fn local(&self) -> PathBuf {
Xdg::state_dir() Xdg::state_dir()
.join("packages") .join("packages")
.join(format!("{:x}", XxHash3_128::oneshot(self.remote().as_bytes()))) .join(format!("{:x}", XxHash3_128::oneshot(self.remote().as_bytes())))
} }
#[inline]
pub(super) fn remote(&self) -> String { pub(super) fn remote(&self) -> String {
// Support more Git hosting services in the future // Support more Git hosting services in the future
format!("https://github.com/{}.git", self.parent) 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<()> { pub(super) fn header(&self, s: &str) -> Result<()> {
use crossterm::style::{Attribute, Print, SetAttributes}; use crossterm::style::{Attribute, Print, SetAttributes};

View file

@ -2,7 +2,7 @@ use std::path::PathBuf;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use tokio::fs; 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 yazi_macro::outln;
use super::Dependency; use super::Dependency;
@ -13,17 +13,15 @@ impl Dependency {
self.header("Deploying package `{name}`")?; self.header("Deploying package `{name}`")?;
self.is_flavor = maybe_exists(&from.join("flavor.toml")).await; 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? { if maybe_exists(&to).await && self.hash != self.hash().await? {
bail!( bail!(
"The user has modified the contents of the `{}` package. For safety, the operation has been aborted. "You have modified the contents of the `{}` {}. For safety, the operation has been aborted.
Please manually delete it from your plugins/flavors directory and re-run the command.", Please manually delete it from `{}` and re-run the command.",
self.name 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()))?; .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::deploy_assets(from.join("assets"), to.join("assets")).await?;
self.hash = self.hash().await?;
outln!("Done!")?; outln!("Done!")?;
Ok(()) Ok(())
} }
async fn deploy_assets(from: PathBuf, to: PathBuf) -> Result<()> { 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 { match fs::read_dir(&from).await {
Ok(mut it) => { Ok(mut it) => {
fs::create_dir_all(&to).await?; 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()))?, Err(e) => Err(e).context(format!("failed to read `{}`", from.display()))?,
} }
Ok(()) Ok(())
} }
} }

View file

@ -1,18 +1,13 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use tokio::fs; use tokio::fs;
use twox_hash::XxHash3_128; use twox_hash::XxHash3_128;
use yazi_fs::{Xdg, ok_or_not_found}; use yazi_fs::ok_or_not_found;
use super::Dependency; use super::Dependency;
impl Dependency { impl Dependency {
pub(crate) async fn hash(&self) -> Result<String> { pub(crate) async fn hash(&self) -> Result<String> {
let dir = if self.is_flavor { let dir = self.target();
Xdg::config_dir().join(format!("flavors/{}", self.name))
} else {
Xdg::config_dir().join(format!("plugins/{}", self.name))
};
let files = if self.is_flavor { let files = if self.is_flavor {
&[ &[
"LICENSE", "LICENSE",

View file

@ -5,7 +5,7 @@ use super::{Dependency, Git};
impl Dependency { impl Dependency {
pub(super) async fn install(&mut self) -> Result<()> { pub(super) async fn install(&mut self) -> Result<()> {
self.header("Installing package `{name}`")?; self.header("Fetching package `{name}`")?;
let path = self.local(); let path = self.local();
if !must_exists(&path).await { if !must_exists(&path).await {

View file

@ -1,6 +1,6 @@
#![allow(clippy::module_inception)] #![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 anyhow::Context;
use yazi_fs::Xdg; use yazi_fs::Xdg;

View file

@ -25,11 +25,12 @@ impl Package {
pub(crate) async fn add(&mut self, use_: &str) -> Result<()> { pub(crate) async fn add(&mut self, use_: &str) -> Result<()> {
let mut dep = Dependency::from_str(use_)?; let mut dep = Dependency::from_str(use_)?;
if self.plugins.iter().any(|d| d.parent == dep.parent && d.child == dep.child) { if let Some(d) = self.identical(&dep) {
bail!("Plugin `{}` already exists in package.toml", dep.name); bail!(
} "{} `{}` already exists in package.toml",
if self.flavors.iter().any(|d| d.parent == dep.parent && d.child == dep.child) { if d.is_flavor { "Flavor" } else { "Plugin" },
bail!("Flavor `{}` already exists in package.toml", dep.name); dep.name
)
} }
dep.add().await?; dep.add().await?;
@ -43,6 +44,22 @@ impl Package {
create_and_seal(&Self::toml(), s.as_bytes()).await.context("Failed to write package.toml") 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<()> { pub(crate) async fn install(&mut self, upgrade: bool) -> Result<()> {
for d in &mut self.plugins { for d in &mut self.plugins {
if upgrade { if upgrade {
@ -167,6 +184,11 @@ impl Package {
#[inline] #[inline]
fn toml() -> PathBuf { Xdg::config_dir().join("package.toml") } 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 { impl<'de> Deserialize<'de> for Package {