diff --git a/Cargo.lock b/Cargo.lock index dcf5bc88..5bf0d1ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2770,6 +2770,15 @@ dependencies = [ "windows", ] +[[package]] +name = "twox-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7b17f197b3050ba473acf9181f7b1d3b66d1cf7356c6cc57886662276e65908" +dependencies = [ + "rand", +] + [[package]] name = "typeid" version = "1.0.2" @@ -3364,11 +3373,11 @@ dependencies = [ "clap_complete_fig", "clap_complete_nushell", "crossterm", - "md-5", "serde", "serde_json", "tokio", "toml", + "twox-hash", "vergen-gitcl", "yazi-boot", "yazi-dds", @@ -3537,6 +3546,7 @@ dependencies = [ "tokio-stream", "tokio-util", "tracing", + "twox-hash", "unicode-width 0.2.0", "uzers", "yazi-adapter", diff --git a/Cargo.toml b/Cargo.toml index bccf0b32..d412fd2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,8 @@ shell-words = "1.1.0" tokio = { version = "1.42.0", features = [ "full" ] } tokio-stream = "0.1.17" tokio-util = "0.7.13" +toml = { version = "0.8.19" } tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] } +twox-hash = { version = "2.1.0", default-features = false, features = [ "std", "random", "xxhash3_128" ] } unicode-width = "0.2.0" uzers = "0.12.1" diff --git a/cspell.json b/cspell.json index f1cbd0af..31dfeb26 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","Sysinfo","ffprobe","vframes","luma","obase","outln","errln","tmtheme"],"version":"0.2","language":"en"} \ No newline at end of file +{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","Sysinfo","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox"],"version":"0.2","language":"en","flagWords":[]} \ No newline at end of file diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index ba3bfaa0..725a6aa9 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -19,11 +19,11 @@ yazi-shared = { path = "../yazi-shared", version = "0.4.3" } anyhow = { workspace = true } clap = { workspace = true } crossterm = { workspace = true } -md-5 = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } -toml = { version = "0.8.19" } +toml = { workspace = true } +twox-hash = { workspace = true } [build-dependencies] yazi-shared = { path = "../yazi-shared", version = "0.4.3" } diff --git a/yazi-cli/src/args.rs b/yazi-cli/src/args.rs index e22f8e9a..b74082ee 100644 --- a/yazi-cli/src/args.rs +++ b/yazi-cli/src/args.rs @@ -66,6 +66,9 @@ pub(super) struct CommandPack { /// Upgrade all packages. #[arg(short = 'u', long)] pub(super) upgrade: bool, + /// Migrate all packages. + #[arg(short = 'm', long)] + pub(super) migrate: bool, // TODO: remove this } #[derive(clap::Args)] diff --git a/yazi-cli/src/main.rs b/yazi-cli/src/main.rs index f1d90081..168f7f7c 100644 --- a/yazi-cli/src/main.rs +++ b/yazi-cli/src/main.rs @@ -62,7 +62,10 @@ async fn run() -> anyhow::Result<()> { Command::Pack(cmd) => { package::init()?; - if cmd.install { + package::Package::load().await?.sync().await.ok(); + if cmd.migrate { + outln!("Migration successful")?; + } else if cmd.install { package::Package::load().await?.install(false).await?; } else if cmd.list { package::Package::load().await?.print()?; diff --git a/yazi-cli/src/package/dependency.rs b/yazi-cli/src/package/dependency.rs index 8846a3bc..8867f819 100644 --- a/yazi-cli/src/package/dependency.rs +++ b/yazi-cli/src/package/dependency.rs @@ -1,63 +1,36 @@ -use std::{borrow::Cow, io::BufWriter, path::PathBuf}; +use std::{io::BufWriter, path::PathBuf, str::FromStr}; -use anyhow::Result; -use md5::{Digest, Md5}; +use anyhow::{Result, bail}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use twox_hash::XxHash3_128; use yazi_fs::Xdg; +#[derive(Default)] pub(crate) struct Dependency { - pub(crate) repo: String, - pub(crate) child: String, - pub(crate) rev: String, + 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) rev: String, + pub(crate) hash: String, + pub(super) is_flavor: bool, } impl Dependency { - pub(super) fn new(url: &str, rev: 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, rev: rev.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())) + .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.repo) + format!("https://github.com/{}.git", self.parent) } pub(super) fn header(&self, s: &str) -> Result<()> { @@ -69,7 +42,7 @@ impl Dependency { SetAttributes(Attribute::Reverse.into()), SetAttributes(Attribute::Bold.into()), Print(" "), - Print(s.replacen("{name}", self.name().unwrap_or_default(), 1)), + Print(s.replacen("{name}", &self.name, 1)), Print(" "), SetAttributes(Attribute::Reset.into()), Print("\n\n"), @@ -78,6 +51,34 @@ impl Dependency { } } +impl FromStr for Dependency { + type Err = anyhow::Error; + + fn from_str(s: &str) -> std::result::Result { + let mut parts = s.splitn(2, ':'); + + let Some(parent) = parts.next() else { bail!("Package url cannot be empty") }; + let child = parts.next().unwrap_or_default(); + + let Some((_, repo)) = parent.split_once('/') else { + bail!("Package url `{parent}` must be in the format `owner/repo`") + }; + + let name = if child.is_empty() { repo } else { child }; + if !name.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) { + bail!("Package name `{name}` must be in kebab-case") + } + + Ok(Self { + use_: s.to_owned(), + name: format!("{name}.yazi"), + parent: format!("{parent}{}", if child.is_empty() { ".yazi" } else { "" }), + child: child.to_owned(), + ..Default::default() + }) + } +} + impl<'de> Deserialize<'de> for Dependency { fn deserialize(deserializer: D) -> Result where @@ -87,11 +88,18 @@ impl<'de> Deserialize<'de> for Dependency { struct Shadow { #[serde(rename = "use")] use_: String, - rev: Option, + #[serde(default)] + rev: String, + #[serde(default)] + hash: String, } let outer = Shadow::deserialize(deserializer)?; - Ok(Self::new(&outer.use_, outer.rev.as_deref())) + Ok(Self { + rev: outer.rev, + hash: outer.hash, + ..Self::from_str(&outer.use_).map_err(serde::de::Error::custom)? + }) } } @@ -103,11 +111,11 @@ impl Serialize for Dependency { #[derive(Serialize)] struct Shadow<'a> { #[serde(rename = "use")] - use_: Cow<'a, str>, - rev: Option<&'a String>, + use_: &'a str, + rev: &'a str, + hash: &'a str, } - Shadow { use_: self.use_(), rev: Some(&self.rev).filter(|&s| !s.is_empty()) } - .serialize(serializer) + Shadow { use_: &self.use_, rev: &self.rev, hash: &self.hash }.serialize(serializer) } } diff --git a/yazi-cli/src/package/deploy.rs b/yazi-cli/src/package/deploy.rs index 7c205aff..1f4469e2 100644 --- a/yazi-cli/src/package/deploy.rs +++ b/yazi-cli/src/package/deploy.rs @@ -2,48 +2,51 @@ use std::path::PathBuf; use anyhow::{Context, Result, bail}; use tokio::fs; -use yazi_fs::{Xdg, maybe_exists, must_exists, remove_dir_clean}; +use yazi_fs::{Xdg, copy_and_seal, maybe_exists, remove_dir_clean}; use yazi_macro::outln; use super::Dependency; -const TRACKER: &str = "DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY"; - impl Dependency { 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.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/{name}")) + Xdg::config_dir().join(format!("flavors/{}", self.name)) } else { - Xdg::config_dir().join(format!("plugins/{name}")) + Xdg::config_dir().join(format!("plugins/{}", self.name)) }; - let tracker = to.join(TRACKER); - if maybe_exists(&to).await && !must_exists(&tracker).await { + if maybe_exists(&to).await && self.hash != self.hash().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." + "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 ); } 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 { - // TODO: init.lua - &["init.lua", "README.md", "LICENSE"][..] + &["main.lua", "README.md", "LICENSE"][..] }; for file in files { - let (from, to) = (from.join(file), to.join(file)); + // TODO: remove this + let (from, to) = if *file == "main.lua" { + if maybe_exists(from.join(file)).await { + (from.join(file), to.join(file)) + } else { + (from.join("init.lua"), to.join("main.lua")) + } + } else { + (from.join(file), to.join(file)) + }; - fs::copy(&from, &to) + copy_and_seal(&from, &to) .await .with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()))?; } @@ -75,7 +78,7 @@ For safety, please manually delete it from your plugin/flavor directory and re-r fs::create_dir_all(&to).await?; while let Some(entry) = it.next_entry().await? { let (src, dist) = (entry.path(), to.join(entry.file_name())); - fs::copy(&src, &dist).await.with_context(|| { + copy_and_seal(&src, &dist).await.with_context(|| { format!("failed to copy `{}` to `{}`", src.display(), dist.display()) })?; } diff --git a/yazi-cli/src/package/hash.rs b/yazi-cli/src/package/hash.rs new file mode 100644 index 00000000..2cf97c60 --- /dev/null +++ b/yazi-cli/src/package/hash.rs @@ -0,0 +1,57 @@ +use anyhow::{Context, Result}; +use tokio::fs; +use twox_hash::XxHash3_128; +use yazi_fs::{Xdg, 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 files = if self.is_flavor { + &[ + "LICENSE", + "LICENSE-tmtheme", + "README.md", + "filestyle.toml", + "flavor.toml", + "preview.png", + "tmtheme.xml", + ][..] + } else { + &["LICENSE", "README.md", "main.lua"][..] + }; + + let mut hasher = XxHash3_128::new(); + for file in files { + hasher.write(file.as_bytes()); + hasher.write(b"VpvFw9Atb7cWGOdqhZCra634CcJJRlsRl72RbZeV0vpG1\0"); + hasher.write(&ok_or_not_found(fs::read(dir.join(file)).await)?); + } + + let mut assets = vec![]; + match fs::read_dir(dir.join("assets")).await { + Ok(mut it) => { + while let Some(entry) = it.next_entry().await? { + assets.push((entry.file_name(), fs::read(entry.path()).await?)); + } + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => Err(e).context(format!("failed to read `{}`", dir.join("assets").display()))?, + } + + assets.sort_unstable_by(|(a, _), (b, _)| a.cmp(b)); + for (name, data) in assets { + hasher.write(name.as_encoded_bytes()); + hasher.write(b"pQU2in0xcsu97Y77Nuq2LnT8mczMlFj22idcYRmMrglqU\0"); + hasher.write(&data); + } + + Ok(format!("{:x}", hasher.finish_128())) + } +} diff --git a/yazi-cli/src/package/mod.rs b/yazi-cli/src/package/mod.rs index 7dcd9d0d..3328a374 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 install package upgrade); +yazi_macro::mod_flat!(add 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 3ce6dd99..f6d503de 100644 --- a/yazi-cli/src/package/package.rs +++ b/yazi-cli/src/package/package.rs @@ -1,22 +1,22 @@ -use std::path::PathBuf; +use std::{path::{Path, PathBuf}, str::FromStr}; use anyhow::{Context, Result, bail}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tokio::fs; -use yazi_fs::Xdg; +use yazi_fs::{Xdg, create_and_seal, ok_or_not_found, unique_name}; use yazi_macro::outln; use super::Dependency; #[derive(Default)] pub(crate) struct Package { - plugins: Vec, - flavors: Vec, + pub(crate) plugins: Vec, + pub(crate) flavors: Vec, } impl Package { pub(crate) async fn load() -> Result { - Ok(match fs::read_to_string(Self::path()).await { + Ok(match fs::read_to_string(Self::toml()).await { Ok(s) => toml::from_str(&s)?, Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::default(), Err(e) => Err(e)?, @@ -24,14 +24,12 @@ impl Package { } pub(crate) async fn add(&mut self, use_: &str) -> Result<()> { - let mut dep = Dependency::new(use_, None); - let Some(name) = dep.name() else { bail!("Invalid package `use`") }; - - if self.plugins.iter().any(|d| d.repo == dep.repo && d.child == dep.child) { - bail!("Plugin `{name}` already exists in package.toml"); + 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.repo == dep.repo && d.child == dep.child) { - bail!("Flavor `{name}` already exists in package.toml"); + if self.flavors.iter().any(|d| d.parent == dep.parent && d.child == dep.child) { + bail!("Flavor `{}` already exists in package.toml", dep.name); } dep.add().await?; @@ -41,7 +39,8 @@ impl Package { self.plugins.push(dep); } - Ok(fs::write(Self::path(), toml::to_string_pretty(self)?).await?) + 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<()> { @@ -61,33 +60,113 @@ impl Package { } let s = toml::to_string_pretty(self)?; - fs::write(Self::path(), s).await.context("Failed to write package.toml") + create_and_seal(&Self::toml(), s.as_bytes()).await.context("Failed to write package.toml") } pub(crate) fn print(&self) -> Result<()> { outln!("Plugins:")?; for d in &self.plugins { if d.rev.is_empty() { - outln!("\t{}", d.use_())?; + outln!("\t{}", d.use_)?; } else { - outln!("\t{} ({})", d.use_(), d.rev)?; + outln!("\t{} ({})", d.use_, d.rev)?; } } outln!("Flavors:")?; for d in &self.flavors { if d.rev.is_empty() { - outln!("\t{}", d.use_())?; + outln!("\t{}", d.use_)?; } else { - outln!("\t{} ({})", d.use_(), d.rev)?; + outln!("\t{} ({})", d.use_, d.rev)?; } } Ok(()) } + // TODO: remove this + pub(crate) async fn sync(&mut self) -> Result<()> { + async fn make_readonly(p: &Path) -> Result<()> { + let mut perms = fs::metadata(p).await?.permissions(); + perms.set_readonly(true); + fs::set_permissions(p, perms).await?; + Ok(()) + } + + match fs::read_dir(Xdg::config_dir().join("plugins")).await { + Ok(mut it) => { + while let Some(entry) = it.next_entry().await? { + let dir = entry.path(); + if !dir.is_dir() || dir.extension().is_none_or(|s| s != "yazi") { + continue; + } + + match fs::symlink_metadata(dir.join("init.lua")).await { + Ok(_) => {} + Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue, + Err(e) => Err(e)?, + } + + ok_or_not_found( + fs::rename( + dir.join("main.lua"), + unique_name(dir.join("main.lua.bak").into(), async { false }).await?, + ) + .await, + )?; + + ok_or_not_found(fs::rename(dir.join("init.lua"), dir.join("main.lua")).await)?; + } + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => Err(e) + .context(format!("failed to read `{}`", Xdg::config_dir().join("plugins").display()))?, + } + + for d in &mut self.plugins { + let dir = Xdg::config_dir().join(format!("plugins/{}", d.name)); + for f in ["LICENSE", "README.md", "main.lua"] { + make_readonly(&dir.join(f)).await.ok(); + } + + let tracker = dir.join("DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY"); + if fs::read(&tracker).await.is_ok_and(|v| v.is_empty()) { + if d.hash.is_empty() { + d.hash = d.hash().await?; + } + fs::remove_file(&tracker).await.ok(); + } + } + for d in &mut self.flavors { + let dir = Xdg::config_dir().join(format!("flavors/{}", d.name)); + for f in [ + "LICENSE", + "LICENSE-tmtheme", + "README.md", + "filestyle.toml", + "flavor.toml", + "preview.png", + "tmtheme.xml", + ] { + make_readonly(&dir.join(f)).await.ok(); + } + + let tracker = dir.join("DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY"); + if fs::read(&tracker).await.is_ok_and(|v| v.is_empty()) { + if d.hash.is_empty() { + d.hash = d.hash().await?; + } + fs::remove_file(&tracker).await.ok(); + } + } + + let s = toml::to_string_pretty(self)?; + create_and_seal(&Self::toml(), s.as_bytes()).await.context("Failed to write package.toml") + } + #[inline] - fn path() -> PathBuf { Xdg::config_dir().join("package.toml") } + fn toml() -> PathBuf { Xdg::config_dir().join("package.toml") } } impl<'de> Deserialize<'de> for Package { @@ -107,7 +186,9 @@ impl<'de> Deserialize<'de> for Package { deps: Vec, } - let outer = Outer::deserialize(deserializer)?; + let mut outer = Outer::deserialize(deserializer)?; + outer.flavor.deps.iter_mut().for_each(|d| d.is_flavor = true); + Ok(Self { plugins: outer.plugin.deps, flavors: outer.flavor.deps }) } } diff --git a/yazi-config/Cargo.toml b/yazi-config/Cargo.toml index b465f79b..64c35df4 100644 --- a/yazi-config/Cargo.toml +++ b/yazi-config/Cargo.toml @@ -22,7 +22,7 @@ indexmap = { version = "2.7.0", features = [ "serde" ] } ratatui = { workspace = true } regex = { workspace = true } serde = { workspace = true } -toml = { version = "0.8.19" } +toml = { workspace = true } tracing = { workspace = true } validator = { version = "0.19.0", features = [ "derive" ] } diff --git a/yazi-fs/src/fns.rs b/yazi-fs/src/fns.rs index 70872ca2..21026eb7 100644 --- a/yazi-fs/src/fns.rs +++ b/yazi-fs/src/fns.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, collections::{HashMap, HashSet, VecDeque}, ffi::{OsStr, OsString}, path::{Path, PathBuf}}; use anyhow::{Result, bail}; -use tokio::{fs, io, select, sync::{mpsc, oneshot}, time}; +use tokio::{fs, io::{self, AsyncWriteExt}, select, sync::{mpsc, oneshot}, time}; use super::Cha; @@ -22,10 +22,10 @@ pub async fn must_be_dir(p: impl AsRef) -> bool { } #[inline] -pub fn ok_or_not_found(result: io::Result<()>) -> io::Result<()> { +pub fn ok_or_not_found(result: io::Result) -> io::Result { match result { - Ok(()) => Ok(()), - Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()), + Ok(t) => Ok(t), + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(T::default()), Err(_) => result, } } @@ -83,6 +83,24 @@ async fn _paths_to_same_file(a: &Path, b: &Path) -> std::io::Result { Ok(final_name(a).await? == final_name(b).await?) } +#[inline] +pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> { + create_and_seal(to, &fs::read(from).await?).await +} + +pub async fn create_and_seal(p: &Path, b: &[u8]) -> io::Result<()> { + ok_or_not_found(fs::remove_file(p).await)?; + + let mut file = fs::OpenOptions::new().create_new(true).write(true).truncate(true).open(p).await?; + file.write_all(b).await?; + + let mut perm = file.metadata().await?.permissions(); + perm.set_readonly(true); + file.set_permissions(perm).await?; + + Ok(()) +} + pub async fn realname(p: &Path) -> Option { let name = p.file_name()?; if p == fs::canonicalize(p).await.ok()? { diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index 53d022ca..7a14914b 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -39,6 +39,7 @@ tokio = { workspace = true } tokio-stream = { workspace = true } tokio-util = { workspace = true } tracing = { workspace = true } +twox-hash = { workspace = true } unicode-width = { workspace = true } yazi-prebuild = "0.1.2" diff --git a/yazi-plugin/src/loader/loader.rs b/yazi-plugin/src/loader/loader.rs index f822b471..6d4803b6 100644 --- a/yazi-plugin/src/loader/loader.rs +++ b/yazi-plugin/src/loader/loader.rs @@ -55,12 +55,32 @@ impl Loader { return Ok(()); } - // TODO: init.lua - let p = BOOT.plugin_dir.join(format!("{name}.yazi/init.lua")); - let chunk = - fs::read(&p).await.with_context(|| format!("Failed to load plugin from {p:?}"))?.into(); + // TODO: remove this + let p = BOOT.plugin_dir.join(format!("{name}.yazi/main.lua")); + let chunk = match fs::read(&p).await { + Ok(b) => b, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) { + yazi_proxy::AppProxy::notify(yazi_proxy::options::NotifyOpt { + title: "Deprecated entry file".to_owned(), + content: format!( + "The plugin entry file `init.lua` has been deprecated in v0.4.3 in favor of the new `main.lua`, and it will be fully removed in the next major version 0.5. - self.cache.write().insert(name.to_owned(), chunk); +Please run `ya pack -m` to automatically migrate all plugins, or manually rename your `{name}.yazi/init.lua` to `{name}.yazi/main.lua`." + ), + level: yazi_proxy::options::NotifyLevel::Warn, + timeout: std::time::Duration::from_secs(25), + }); + } + + let p = BOOT.plugin_dir.join(format!("{name}.yazi/init.lua")); + fs::read(&p).await.with_context(|| format!("Failed to load plugin from {p:?}"))? + } + Err(e) => Err(e).with_context(|| format!("Failed to load plugin from {p:?}"))?, + }; + + self.cache.write().insert(name.to_owned(), chunk.into()); Ok(()) } diff --git a/yazi-plugin/src/macros.rs b/yazi-plugin/src/macros.rs index 99a32f77..30fe25b3 100644 --- a/yazi-plugin/src/macros.rs +++ b/yazi-plugin/src/macros.rs @@ -120,3 +120,22 @@ macro_rules! impl_file_methods { }); }; } + +#[macro_export] +macro_rules! deprecate { + ($lua:ident, $tt:tt) => {{ + static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) { + let id = match $lua.named_registry_value::<$crate::RtRef>("rt")?.current() { + Some(id) => &format!("`{id}.yazi` plugin"), + None => "`init.lua` config", + }; + yazi_proxy::AppProxy::notify(yazi_proxy::options::NotifyOpt { + title: "Deprecated API".to_owned(), + content: format!($tt, id), + level: yazi_proxy::options::NotifyLevel::Warn, + timeout: std::time::Duration::from_secs(20), + }); + } + }}; +} diff --git a/yazi-plugin/src/utils/cache.rs b/yazi-plugin/src/utils/cache.rs index 8d215576..0de448b0 100644 --- a/yazi-plugin/src/utils/cache.rs +++ b/yazi-plugin/src/utils/cache.rs @@ -1,5 +1,5 @@ -use md5::{Digest, Md5}; use mlua::{Function, Lua, Table}; +use twox_hash::XxHash3_128; use yazi_config::PREVIEW; use super::Utils; @@ -14,9 +14,10 @@ impl Utils { } let hex = { - let mut digest = Md5::new_with_prefix(file.url.as_os_str().as_encoded_bytes()); - digest.update(format!("//{:?}//{}", file.cha.mtime, t.raw_get("skip").unwrap_or(0))); - format!("{:x}", digest.finalize()) + let mut h = XxHash3_128::new(); + h.write(file.url.as_os_str().as_encoded_bytes()); + h.write(format!("//{:?}//{}", file.cha.mtime, t.raw_get("skip").unwrap_or(0)).as_bytes()); + format!("{:x}", h.finish_128()) }; Some(Url::cast(lua, PREVIEW.cache_dir.join(hex))).transpose() diff --git a/yazi-plugin/src/utils/text.rs b/yazi-plugin/src/utils/text.rs index f48019af..809efcb2 100644 --- a/yazi-plugin/src/utils/text.rs +++ b/yazi-plugin/src/utils/text.rs @@ -2,15 +2,24 @@ use std::ops::ControlFlow; use md5::{Digest, Md5}; use mlua::{Function, Lua, Table}; +use twox_hash::XxHash3_128; use unicode_width::UnicodeWidthChar; use super::Utils; use crate::CLIPBOARD; impl Utils { - pub(super) fn hash(lua: &Lua) -> mlua::Result { - lua.create_async_function(|_, s: mlua::String| async move { - Ok(format!("{:x}", Md5::new_with_prefix(s.as_bytes()).finalize())) + pub(super) fn hash(lua: &Lua, deprecated: bool) -> mlua::Result { + lua.create_async_function(move |lua, s: mlua::String| async move { + if deprecated { + crate::deprecate!( + lua, + "The `ya.md5()` function is deprecated, please use `ya.hash()` instead, in your {}" + ); + Ok(format!("{:x}", Md5::new_with_prefix(s.as_bytes()).finalize())) + } else { + Ok(format!("{:x}", XxHash3_128::oneshot(s.as_bytes().as_ref()))) + } }) } diff --git a/yazi-plugin/src/utils/utils.rs b/yazi-plugin/src/utils/utils.rs index 914b905d..2033acb3 100644 --- a/yazi-plugin/src/utils/utils.rs +++ b/yazi-plugin/src/utils/utils.rs @@ -53,8 +53,8 @@ pub fn compose(lua: &Lua, isolate: bool) -> mlua::Result { b"target_family" => Utils::target_family(lua)?, // Text - b"md5" => Utils::hash(lua)?, // TODO: deprecate this in the future - b"hash" => Utils::hash(lua)?, + b"md5" => Utils::hash(lua, true)?, // TODO: deprecate this in the future + b"hash" => Utils::hash(lua, false)?, b"quote" => Utils::quote(lua)?, b"truncate" => Utils::truncate(lua)?, b"clipboard" => Utils::clipboard(lua)?,