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.
#[arg(short = 'a', long)]
pub(super) add: Option<String>,
/// Delete a package.
#[arg(short = 'd', long)]
pub(super) delete: Option<String>,
/// Install all packages.
#[arg(short = 'i', long)]
pub(super) install: bool,

View file

@ -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?;
}
}

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 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};

View file

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

View file

@ -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<String> {
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",

View file

@ -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 {

View file

@ -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;

View file

@ -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 {