feat: dynamic previewer Lua API

This commit is contained in:
sxyazi 2026-04-15 21:58:55 +08:00
parent 80a54c3dd5
commit 8ceb6b87db
No known key found for this signature in database
142 changed files with 2174 additions and 1354 deletions

26
Cargo.lock generated
View file

@ -211,6 +211,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
dependencies = [
"rustversion",
"serde",
]
[[package]]
@ -2130,7 +2131,7 @@ checksum = "ae8e22120c32fb4d19ec55fba35015f57095cd95a2e3b732e44457f5915b2ee8"
dependencies = [
"num-integer",
"num-traits",
"rand 0.10.0",
"rand 0.10.1",
"rand_core 0.10.0",
]
@ -3431,9 +3432,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
@ -3441,9 +3442,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20 0.10.0",
"getrandom 0.4.2",
@ -3623,7 +3624,7 @@ dependencies = [
"num-traits",
"paste",
"profiling",
"rand 0.9.2",
"rand 0.9.3",
"rand_chacha 0.9.0",
"simd_helpers",
"thiserror 2.0.18",
@ -3828,7 +3829,7 @@ dependencies = [
"pkcs5",
"pkcs8",
"polyval 0.7.1",
"rand 0.10.0",
"rand 0.10.1",
"rand_core 0.10.0",
"ring",
"rsa",
@ -4864,7 +4865,7 @@ version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
dependencies = [
"rand 0.9.2",
"rand 0.9.3",
]
[[package]]
@ -5908,6 +5909,7 @@ name = "yazi-config"
version = "26.2.2"
dependencies = [
"anyhow",
"arc-swap",
"bitflags 2.11.0",
"crossterm 0.29.0",
"globset",
@ -5916,6 +5918,7 @@ dependencies = [
"ratatui",
"regex",
"serde",
"serde_with",
"strum 0.28.0",
"tokio",
"toml",
@ -5924,6 +5927,7 @@ dependencies = [
"yazi-fs",
"yazi-macro",
"yazi-shared",
"yazi-shim",
"yazi-tty",
]
@ -6076,7 +6080,7 @@ dependencies = [
"objc2",
"parking_lot",
"percent-encoding",
"rand 0.10.0",
"rand 0.10.1",
"regex",
"scopeguard",
"serde",
@ -6278,8 +6282,12 @@ dependencies = [
name = "yazi-shim"
version = "26.2.2"
dependencies = [
"arc-swap",
"crossterm 0.29.0",
"ratatui",
"serde",
"thiserror 2.0.18",
"toml",
"twox-hash",
"unicode-segmentation",
"unicode-width",

View file

@ -37,7 +37,7 @@ debug = false
[workspace.dependencies]
ansi-to-tui = "8.0.1"
anyhow = "1.0.102"
arc-swap = "1.9.1"
arc-swap = { version = "1.9.1", features = [ "serde" ] }
base64 = "0.22.1"
bitflags = { version = "2.11.0", features = [ "serde" ] }
chrono = "0.4.44"
@ -60,7 +60,7 @@ ordered-float = { version = "5.3.0", features = [ "serde" ] }
parking_lot = "0.12.5"
paste = "1.0.15"
percent-encoding = "2.3.2"
rand = { version = "0.10.0", default-features = false, features = [ "std", "sys_rng" ] }
rand = { version = "0.10.1", default-features = false, features = [ "std", "sys_rng" ] }
ratatui = { version = "0.30.0", features = [ "serde", "unstable-rendered-line-info", "unstable-widget-ref" ] }
regex = "1.12.3"
russh = { version = "0.60.0", default-features = false, features = [ "ring", "rsa" ] }

View file

@ -1,8 +1,8 @@
<div align="center">
<sup>Special thanks to:</sup><br>
|<a href="https://go.warp.dev/yazi" target="_blank"><img alt="Warp sponsorship" width=350 src="https://github.com/warpdotdev/brand-assets/blob/main/Github/Sponsor/Warp-Github-LG-02.png"><br><b>Warp, built for coding with multiple AI agents</b><br><sup>Available for macOS, Linux and Windows</sup></a>|
|-|
| <a href="https://go.warp.dev/yazi" target="_blank"><img alt="Warp sponsorship" width=350 src="https://github.com/warpdotdev/brand-assets/blob/main/Github/Sponsor/Warp-Github-LG-02.png"><br><b>Warp, built for coding with multiple AI agents</b><br><sup>Available for macOS, Linux and Windows</sup></a> |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
</div>
@ -47,24 +47,24 @@ Yazi is currently in heavy development, expect breaking changes.
## Image Preview
| Platform | Protocol | Support |
| ---------------------------------------------------------------------------- | -------------------------------------- | ---------------------------------------- |
| [kitty](https://github.com/kovidgoyal/kitty) (>= 0.28.0) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
| [iTerm2](https://iterm2.com) | [Inline images protocol][iip] | ✅ Built-in |
| [WezTerm](https://github.com/wez/wezterm) | [Inline images protocol][iip] | ✅ Built-in |
| [Konsole](https://invent.kde.org/utilities/konsole) | [Kitty old protocol][kgp-old] | ✅ Built-in |
| [foot](https://codeberg.org/dnkl/foot) | [Sixel graphics format][sixel] | ✅ Built-in |
| [Ghostty](https://github.com/ghostty-org/ghostty) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
| [Windows Terminal](https://github.com/microsoft/terminal) (>= v1.22.10352.0) | [Sixel graphics format][sixel] | ✅ Built-in |
| [st with Sixel patch](https://github.com/bakkeby/st-flexipatch) | [Sixel graphics format][sixel] | ✅ Built-in |
| [Warp](https://www.warp.dev) (macOS/Linux only) | [Inline images protocol][iip] | ✅ Built-in |
| [Tabby](https://github.com/Eugeny/tabby) | [Inline images protocol][iip] | ✅ Built-in |
| [VSCode](https://github.com/microsoft/vscode) | [Inline images protocol][iip] | ✅ Built-in |
| [Rio](https://github.com/raphamorim/rio) | [Inline images protocol][iip] | ❌ Rio renders images at incorrect sizes |
| [Black Box](https://gitlab.gnome.org/raggesilver/blackbox) | [Sixel graphics format][sixel] | ✅ Built-in |
| [Bobcat](https://github.com/ismail-yilmaz/Bobcat) | [Inline images protocol][iip] | ✅ Built-in |
| X11 / Wayland | Window system protocol | ☑️ [Überzug++][ueberzug] required |
| Fallback | [ASCII art (Unicode block)][ascii-art] | ☑️ [Chafa][chafa] required (>= 1.16.0) |
| Platform | Protocol | Support |
| ---------------------------------------------------------------------------- | -------------------------------------- | -------------------------------------- |
| [kitty](https://github.com/kovidgoyal/kitty) (>= 0.28.0) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
| [iTerm2](https://iterm2.com) | [Inline images protocol][iip] | ✅ Built-in |
| [WezTerm](https://github.com/wez/wezterm) | [Inline images protocol][iip] | ✅ Built-in |
| [Konsole](https://invent.kde.org/utilities/konsole) | [Kitty old protocol][kgp-old] | ✅ Built-in |
| [foot](https://codeberg.org/dnkl/foot) | [Sixel graphics format][sixel] | ✅ Built-in |
| [Ghostty](https://github.com/ghostty-org/ghostty) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
| [Windows Terminal](https://github.com/microsoft/terminal) (>= v1.22.10352.0) | [Sixel graphics format][sixel] | ✅ Built-in |
| [st with Sixel patch](https://github.com/bakkeby/st-flexipatch) | [Sixel graphics format][sixel] | ✅ Built-in |
| [Warp](https://www.warp.dev) (macOS/Linux only) | [Inline images protocol][iip] | ✅ Built-in |
| [Tabby](https://github.com/Eugeny/tabby) | [Inline images protocol][iip] | ✅ Built-in |
| [VSCode](https://github.com/microsoft/vscode) | [Inline images protocol][iip] | ✅ Built-in |
| [Rio](https://github.com/raphamorim/rio) (>= 0.3.9) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
| [Black Box](https://gitlab.gnome.org/raggesilver/blackbox) | [Sixel graphics format][sixel] | ✅ Built-in |
| [Bobcat](https://github.com/ismail-yilmaz/Bobcat) | [Inline images protocol][iip] | ✅ Built-in |
| X11 / Wayland | Window system protocol | ☑️ [Überzug++][ueberzug] required |
| Fallback | [ASCII art (Unicode block)][ascii-art] | ☑️ [Chafa][chafa] required (>= 1.16.0) |
See https://yazi-rs.github.io/docs/image-preview for details.

View file

@ -1 +1 @@
{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","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","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","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","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike","sstr"],"flagWords":[],"language":"en","version":"0.2"}
{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","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","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","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","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike","sstr","pointee"],"version":"0.2","language":"en","flagWords":[]}

View file

@ -25,12 +25,12 @@ impl Actor for Seek {
succ!(cx.tab_mut().preview.reset());
};
let Some(previewer) = YAZI.plugin.previewer(hovered, mime) else {
let Some(previewer) = YAZI.plugin.previewers.matches(hovered, mime) else {
succ!(cx.tab_mut().preview.reset());
};
let job = SeekJob { file: hovered.clone(), units: form.units };
let opt = PluginOpt::new_callback(&*previewer.run.name, move |_, plugin| {
let opt = PluginOpt::new_callback(previewer.name.clone(), move |_, plugin| {
plugin.call_method("seek", job)
});
act!(app:plugin, cx, opt)

View file

@ -33,7 +33,7 @@ impl From<yazi_emulator::Brand> for Adapters {
B::Ghostty => vec![A::Kgp],
B::Microsoft => vec![A::Sixel],
B::Warp => vec![A::Iip, A::KgpOld],
B::Rio => vec![A::Iip, A::Sixel],
B::Rio => vec![A::Kgp],
B::BlackBox => vec![A::Sixel],
B::VSCode => vec![A::Iip, A::Sixel],
B::Tabby => vec![A::Iip, A::Sixel],

View file

@ -8,15 +8,15 @@ pub struct Access(yazi_vfs::provider::Gate);
impl UserData for Access {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_function_mut("append", |_, (ud, append): (AnyUserData, bool)| {
methods.add_function("append", |_, (ud, append): (AnyUserData, bool)| {
ud.borrow_mut::<Self>()?.0.append(append);
Ok(ud)
});
methods.add_function_mut("create", |_, (ud, create): (AnyUserData, bool)| {
methods.add_function("create", |_, (ud, create): (AnyUserData, bool)| {
ud.borrow_mut::<Self>()?.0.create(create);
Ok(ud)
});
methods.add_function_mut("create_new", |_, (ud, create_new): (AnyUserData, bool)| {
methods.add_function("create_new", |_, (ud, create_new): (AnyUserData, bool)| {
ud.borrow_mut::<Self>()?.0.create_new(create_new);
Ok(ud)
});
@ -26,15 +26,15 @@ impl UserData for Access {
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}
});
methods.add_function_mut("read", |_, (ud, read): (AnyUserData, bool)| {
methods.add_function("read", |_, (ud, read): (AnyUserData, bool)| {
ud.borrow_mut::<Self>()?.0.read(read);
Ok(ud)
});
methods.add_function_mut("truncate", |_, (ud, truncate): (AnyUserData, bool)| {
methods.add_function("truncate", |_, (ud, truncate): (AnyUserData, bool)| {
ud.borrow_mut::<Self>()?.0.truncate(truncate);
Ok(ud)
});
methods.add_function_mut("write", |_, (ud, write): (AnyUserData, bool)| {
methods.add_function("write", |_, (ud, write): (AnyUserData, bool)| {
ud.borrow_mut::<Self>()?.0.write(write);
Ok(ud)
});

View file

@ -0,0 +1,32 @@
use std::{ops::Deref, sync::Arc};
use mlua::{UserData, UserDataFields, Value};
use crate::{Id, cached_field};
#[derive(Clone)]
pub struct Fetcher {
inner: Arc<yazi_config::plugin::Fetcher>,
v_name: Option<Value>,
}
impl Deref for Fetcher {
type Target = yazi_config::plugin::Fetcher;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl Fetcher {
pub fn new(inner: impl Into<Arc<yazi_config::plugin::Fetcher>>) -> Self {
Self { inner: inner.into(), v_name: None }
}
}
impl UserData for Fetcher {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, me| Ok(Id(me.id)));
cached_field!(fields, name, |lua, me| lua.create_string(&*me.name));
}
}

View file

@ -0,0 +1,51 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use yazi_config::YAZI;
use crate::{FileRef, Id, Iter, config::Fetcher};
pub struct Fetchers;
impl UserData for Fetchers {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("match", |_, _, matcher: FetcherMatcher| Ok(matcher));
methods.add_meta_method(MetaMethod::Len, |_, _, ()| Ok(YAZI.plugin.fetchers.load().len()));
}
}
// --- Matcher
pub struct FetcherMatcher(yazi_config::plugin::FetcherMatcher<'static>);
impl TryFrom<Table> for FetcherMatcher {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> {
let id: Id = value.raw_get("id").unwrap_or_default();
let file: Option<FileRef> = value.raw_get("file")?;
let mime: Option<String> = value.raw_get("mime")?;
Ok(Self(yazi_config::plugin::FetcherMatcher {
fetchers: YAZI.plugin.fetchers.load_full(),
id: id.0,
file: file.map(|f| f.inner.clone().into()),
mime: mime.map(Into::into),
offset: 0,
seen: Default::default(),
}))
}
}
impl FromLua for FetcherMatcher {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(t) => t.try_into(),
_ => return Err("expected a table of FetcherMatcher".into_lua_err()),
}
}
}
impl IntoLua for FetcherMatcher {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
Iter::new(self.0.into_iter().map(Fetcher::new), None).into_lua(lua)
}
}

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(fetcher fetchers preloader preloaders previewer previewers spotter spotters);

View file

@ -0,0 +1,32 @@
use std::{ops::Deref, sync::Arc};
use mlua::{UserData, UserDataFields, Value};
use crate::{Id, cached_field};
#[derive(Clone)]
pub struct Preloader {
inner: Arc<yazi_config::plugin::Preloader>,
v_name: Option<Value>,
}
impl Deref for Preloader {
type Target = yazi_config::plugin::Preloader;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl Preloader {
pub fn new(inner: impl Into<Arc<yazi_config::plugin::Preloader>>) -> Self {
Self { inner: inner.into(), v_name: None }
}
}
impl UserData for Preloader {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, me| Ok(Id(me.id)));
cached_field!(fields, name, |lua, me| lua.create_string(&*me.name));
}
}

View file

@ -0,0 +1,51 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use yazi_config::YAZI;
use crate::{FileRef, Id, Iter, config::Preloader};
pub struct Preloaders;
impl UserData for Preloaders {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("match", |_, _, matcher: PreloaderMatcher| Ok(matcher));
methods.add_meta_method(MetaMethod::Len, |_, _, ()| Ok(YAZI.plugin.preloaders.load().len()));
}
}
// --- Matcher
pub struct PreloaderMatcher(yazi_config::plugin::PreloaderMatcher<'static>);
impl TryFrom<Table> for PreloaderMatcher {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> {
let id: Id = value.raw_get("id").unwrap_or_default();
let file: Option<FileRef> = value.raw_get("file")?;
let mime: Option<String> = value.raw_get("mime")?;
Ok(Self(yazi_config::plugin::PreloaderMatcher {
preloaders: YAZI.plugin.preloaders.load_full(),
id: id.0,
file: file.map(|f| f.inner.clone().into()),
mime: mime.map(Into::into),
offset: 0,
next: true,
}))
}
}
impl FromLua for PreloaderMatcher {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(t) => t.try_into(),
_ => return Err("expected a table of PreloaderMatcher".into_lua_err()),
}
}
}
impl IntoLua for PreloaderMatcher {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
Iter::new(self.0.into_iter().map(Preloader::new), None).into_lua(lua)
}
}

View file

@ -0,0 +1,70 @@
use std::{ops::Deref, sync::Arc};
use mlua::{AnyUserData, ExternalError, FromLua, Lua, UserData, UserDataFields, Value};
use yazi_config::plugin::previewer_id;
use crate::{Id, cached_field};
const EXPECTED: &str = "expected a table or Previewer";
#[derive(Clone)]
pub struct Previewer {
inner: Arc<yazi_config::plugin::Previewer>,
v_name: Option<Value>,
}
impl Deref for Previewer {
type Target = yazi_config::plugin::Previewer;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl From<Previewer> for Arc<yazi_config::plugin::Previewer> {
fn from(value: Previewer) -> Self { value.inner }
}
impl Previewer {
pub fn new(inner: impl Into<Arc<yazi_config::plugin::Previewer>>) -> Self {
Self { inner: inner.into(), v_name: None }
}
}
impl TryFrom<mlua::Table> for Previewer {
type Error = mlua::Error;
fn try_from(t: mlua::Table) -> Result<Self, Self::Error> {
Ok(Self::new(yazi_config::plugin::Previewer {
id: previewer_id(),
url: t.raw_get::<Option<mlua::String>>("url")?.map(|s| s.to_str()?.parse()).transpose()?,
mime: t.raw_get::<Option<mlua::String>>("mime")?.map(|s| s.to_str()?.parse()).transpose()?,
run: t.raw_get::<mlua::String>("run")?.to_str()?.parse()?,
}))
}
}
impl TryFrom<AnyUserData> for Previewer {
type Error = mlua::Error;
fn try_from(value: AnyUserData) -> Result<Self, Self::Error> {
Ok(Self::new(value.borrow::<Self>()?.clone()))
}
}
impl FromLua for Previewer {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(tbl) => Self::try_from(tbl),
Value::UserData(ud) => Self::try_from(ud),
_ => Err(EXPECTED.into_lua_err())?,
}
}
}
impl UserData for Previewer {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, me| Ok(Id(me.id)));
cached_field!(fields, name, |lua, me| lua.create_string(&*me.name));
}
}

View file

@ -0,0 +1,66 @@
use mlua::{ExternalError, ExternalResult, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use yazi_config::YAZI;
use crate::{FileRef, Id, Iter, config::Previewer};
pub struct Previewers;
impl UserData for Previewers {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("match", |_, _, matcher: PreviewerMatcher| Ok(matcher));
methods.add_method("insert", |_, _, (index, previewer): (isize, Previewer)| {
let index = match index {
1.. => index - 1,
0 => return Err("index must be 1-based or negative".into_lua_err()),
_ => index,
};
YAZI.plugin.previewers.insert(index, previewer.clone().into()).into_lua_err()?;
Ok(previewer)
});
methods.add_method("remove", |_, _, matcher: PreviewerMatcher| {
YAZI.plugin.previewers.remove(matcher.0);
Ok(())
});
methods.add_meta_method(MetaMethod::Len, |_, _, ()| Ok(YAZI.plugin.previewers.load().len()));
}
}
// --- Matcher
pub struct PreviewerMatcher(yazi_config::plugin::PreviewerMatcher<'static>);
impl TryFrom<Table> for PreviewerMatcher {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> {
let id: Id = value.raw_get("id").unwrap_or_default();
let file: Option<FileRef> = value.raw_get("file")?;
let mime: Option<String> = value.raw_get("mime")?;
Ok(Self(yazi_config::plugin::PreviewerMatcher {
previewers: YAZI.plugin.previewers.load_full(),
id: id.0,
file: file.map(|f| f.inner.clone().into()),
mime: mime.map(Into::into),
offset: 0,
}))
}
}
impl FromLua for PreviewerMatcher {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(t) => t.try_into(),
_ => return Err("expected a table of PreviewerMatcher".into_lua_err()),
}
}
}
impl IntoLua for PreviewerMatcher {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
Iter::new(self.0.into_iter().map(Previewer::new), None).into_lua(lua)
}
}

View file

@ -0,0 +1,32 @@
use std::{ops::Deref, sync::Arc};
use mlua::{UserData, UserDataFields, Value};
use crate::{Id, cached_field};
#[derive(Clone)]
pub struct Spotter {
inner: Arc<yazi_config::plugin::Spotter>,
v_name: Option<Value>,
}
impl Deref for Spotter {
type Target = yazi_config::plugin::Spotter;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl Spotter {
pub fn new(inner: impl Into<Arc<yazi_config::plugin::Spotter>>) -> Self {
Self { inner: inner.into(), v_name: None }
}
}
impl UserData for Spotter {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, me| Ok(Id(me.id)));
cached_field!(fields, name, |lua, me| lua.create_string(&*me.name));
}
}

View file

@ -0,0 +1,50 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, MetaMethod, Table, UserData, UserDataMethods, Value};
use yazi_config::YAZI;
use crate::{FileRef, Id, Iter, config::Spotter};
pub struct Spotters;
impl UserData for Spotters {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("match", |_, _, matcher: SpotterMatcher| Ok(matcher));
methods.add_meta_method(MetaMethod::Len, |_, _, ()| Ok(YAZI.plugin.spotters.load().len()));
}
}
// --- Matcher
pub struct SpotterMatcher(yazi_config::plugin::SpotterMatcher<'static>);
impl TryFrom<Table> for SpotterMatcher {
type Error = mlua::Error;
fn try_from(value: Table) -> Result<Self, Self::Error> {
let id: Id = value.raw_get("id").unwrap_or_default();
let file: Option<FileRef> = value.raw_get("file")?;
let mime: Option<String> = value.raw_get("mime")?;
Ok(Self(yazi_config::plugin::SpotterMatcher {
spotters: YAZI.plugin.spotters.load_full(),
id: id.0,
file: file.map(|f| f.inner.clone().into()),
mime: mime.map(Into::into),
offset: 0,
}))
}
}
impl FromLua for SpotterMatcher {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(t) => t.try_into(),
_ => return Err("expected a table of SpotterMatcher".into_lua_err()),
}
}
}
impl IntoLua for SpotterMatcher {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
Iter::new(self.0.into_iter().map(Spotter::new), None).into_lua(lua)
}
}

View file

@ -81,11 +81,11 @@ impl UserData for Bar {
crate::impl_area_method!(methods);
crate::impl_style_method!(methods, style);
methods.add_function_mut("edge", |_, (ud, edge): (AnyUserData, Edge)| {
methods.add_function("edge", |_, (ud, edge): (AnyUserData, Edge)| {
ud.borrow_mut::<Self>()?.edge = edge;
Ok(ud)
});
methods.add_function_mut("symbol", |_, (ud, symbol): (AnyUserData, String)| {
methods.add_function("symbol", |_, (ud, symbol): (AnyUserData, String)| {
ud.borrow_mut::<Self>()?.symbol = symbol;
Ok(ud)
});

View file

@ -79,7 +79,7 @@ impl UserData for Border {
crate::impl_area_method!(methods);
crate::impl_style_method!(methods, style);
methods.add_function_mut("type", |_, (ud, value): (AnyUserData, u8)| {
methods.add_function("type", |_, (ud, value): (AnyUserData, u8)| {
ud.borrow_mut::<Self>()?.r#type = match value {
ROUNDED => ratatui::widgets::BorderType::Rounded,
DOUBLE => ratatui::widgets::BorderType::Double,
@ -90,20 +90,17 @@ impl UserData for Border {
};
Ok(ud)
});
methods.add_function_mut(
"title",
|_, (ud, line, position): (AnyUserData, Line, Option<u8>)| {
let position = if position == Some(Borders::BOTTOM.bits()) {
ratatui::widgets::TitlePosition::Bottom
} else {
ratatui::widgets::TitlePosition::Top
};
methods.add_function("title", |_, (ud, line, position): (AnyUserData, Line, Option<u8>)| {
let position = if position == Some(Borders::BOTTOM.bits()) {
ratatui::widgets::TitlePosition::Bottom
} else {
ratatui::widgets::TitlePosition::Top
};
ud.borrow_mut::<Self>()?.titles.push((position, line.inner));
Ok(ud)
},
);
methods.add_function_mut("edge", |_, (ud, edge): (AnyUserData, Edge)| {
ud.borrow_mut::<Self>()?.titles.push((position, line.inner));
Ok(ud)
});
methods.add_function("edge", |_, (ud, edge): (AnyUserData, Edge)| {
ud.borrow_mut::<Self>()?.edge = edge;
Ok(ud)
});

View file

@ -57,7 +57,7 @@ impl UserData for Gauge {
crate::impl_area_method!(methods);
crate::impl_style_method!(methods, style);
methods.add_function_mut("percent", |_, (ud, percent): (AnyUserData, u8)| {
methods.add_function("percent", |_, (ud, percent): (AnyUserData, u8)| {
if percent > 100 {
return Err("percent must be between 0 and 100".into_lua_err());
}
@ -66,7 +66,7 @@ impl UserData for Gauge {
Ok(ud)
});
methods.add_function_mut("ratio", |_, (ud, ratio): (AnyUserData, f64)| {
methods.add_function("ratio", |_, (ud, ratio): (AnyUserData, f64)| {
if !(0.0..1.0).contains(&ratio) {
return Err("ratio must be between 0 and 1".into_lua_err());
}
@ -75,12 +75,12 @@ impl UserData for Gauge {
Ok(ud)
});
methods.add_function_mut("label", |_, (ud, label): (AnyUserData, Span)| {
methods.add_function("label", |_, (ud, label): (AnyUserData, Span)| {
ud.borrow_mut::<Self>()?.label = Some(label.0);
Ok(ud)
});
methods.add_function_mut("gauge_style", |_, (ud, style): (AnyUserData, Style)| {
methods.add_function("gauge_style", |_, (ud, style): (AnyUserData, Style)| {
ud.borrow_mut::<Self>()?.gauge_style = style.0;
Ok(ud)
});

View file

@ -25,15 +25,15 @@ impl Layout {
impl UserData for Layout {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_function_mut("direction", |_, (ud, value): (AnyUserData, bool)| {
methods.add_function("direction", |_, (ud, value): (AnyUserData, bool)| {
ud.borrow_mut::<Self>()?.direction = value;
Ok(ud)
});
methods.add_function_mut("margin", |_, (ud, value): (AnyUserData, u16)| {
methods.add_function("margin", |_, (ud, value): (AnyUserData, u16)| {
ud.borrow_mut::<Self>()?.margin = Some(ratatui::layout::Margin::new(value, value));
Ok(ud)
});
methods.add_function_mut("margin_h", |_, (ud, value): (AnyUserData, u16)| {
methods.add_function("margin_h", |_, (ud, value): (AnyUserData, u16)| {
{
let mut me = ud.borrow_mut::<Self>()?;
if let Some(margin) = &mut me.margin {
@ -44,7 +44,7 @@ impl UserData for Layout {
}
Ok(ud)
});
methods.add_function_mut("margin_v", |_, (ud, value): (AnyUserData, u16)| {
methods.add_function("margin_v", |_, (ud, value): (AnyUserData, u16)| {
{
let mut me = ud.borrow_mut::<Self>()?;
if let Some(margin) = &mut me.margin {
@ -55,7 +55,7 @@ impl UserData for Layout {
}
Ok(ud)
});
methods.add_function_mut("constraints", |_, (ud, value): (AnyUserData, Vec<Constraint>)| {
methods.add_function("constraints", |_, (ud, value): (AnyUserData, Vec<Constraint>)| {
ud.borrow_mut::<Self>()?.constraints = value.into_iter().map(Into::into).collect();
Ok(ud)
});

View file

@ -133,14 +133,14 @@ impl UserData for Line {
crate::impl_style_shorthands!(methods, style);
methods.add_method("width", |_, me, ()| Ok(me.width()));
methods.add_function_mut("align", |_, (ud, align): (AnyUserData, Align)| {
methods.add_function("align", |_, (ud, align): (AnyUserData, Align)| {
ud.borrow_mut::<Self>()?.alignment = Some(align.0);
Ok(ud)
});
methods.add_method("visible", |_, me, ()| {
Ok(me.iter().flat_map(|s| s.content.chars()).any(|c| c.width().unwrap_or(0) > 0))
});
methods.add_function_mut("truncate", |lua, (ud, t): (AnyUserData, Table)| {
methods.add_function("truncate", |lua, (ud, t): (AnyUserData, Table)| {
let mut me = ud.borrow_mut::<Self>()?;
let max = t.raw_get("max")?;

View file

@ -91,7 +91,7 @@ impl UserData for Pos {
}
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_function_mut("pad", |_, (ud, pad): (AnyUserData, Pad)| {
methods.add_function("pad", |_, (ud, pad): (AnyUserData, Pad)| {
ud.borrow_mut::<Self>()?.pad = pad;
Ok(ud)
});

View file

@ -55,15 +55,15 @@ impl UserData for Row {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
crate::impl_style_method!(methods, style);
methods.add_function_mut("height", |_, (ud, value): (AnyUserData, u16)| {
methods.add_function("height", |_, (ud, value): (AnyUserData, u16)| {
ud.borrow_mut::<Self>()?.height = value;
Ok(ud)
});
methods.add_function_mut("margin_t", |_, (ud, value): (AnyUserData, u16)| {
methods.add_function("margin_t", |_, (ud, value): (AnyUserData, u16)| {
ud.borrow_mut::<Self>()?.top_margin = value;
Ok(ud)
});
methods.add_function_mut("margin_b", |_, (ud, value): (AnyUserData, u16)| {
methods.add_function("margin_b", |_, (ud, value): (AnyUserData, u16)| {
ud.borrow_mut::<Self>()?.bottom_margin = value;
Ok(ud)
});

View file

@ -82,7 +82,7 @@ impl UserData for Span {
methods.add_method("visible", |_, Self(me), ()| {
Ok(me.content.chars().any(|c| c.width().unwrap_or(0) > 0))
});
methods.add_function_mut("truncate", |_, (ud, t): (AnyUserData, Table)| {
methods.add_function("truncate", |_, (ud, t): (AnyUserData, Table)| {
ud.borrow_mut::<Self>()?.truncate(t.raw_get("max")?);
Ok(ud)
});

View file

@ -108,45 +108,45 @@ impl UserData for Table {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
crate::impl_area_method!(methods);
methods.add_function_mut("header", |_, (ud, header): (AnyUserData, Row)| {
methods.add_function("header", |_, (ud, header): (AnyUserData, Row)| {
ud.borrow_mut::<Self>()?.header = Some(header.into());
Ok(ud)
});
methods.add_function_mut("footer", |_, (ud, footer): (AnyUserData, Row)| {
methods.add_function("footer", |_, (ud, footer): (AnyUserData, Row)| {
ud.borrow_mut::<Self>()?.footer = Some(footer.into());
Ok(ud)
});
methods.add_function_mut("widths", |_, (ud, widths): (AnyUserData, Vec<Constraint>)| {
methods.add_function("widths", |_, (ud, widths): (AnyUserData, Vec<Constraint>)| {
ud.borrow_mut::<Self>()?.widths = widths.into_iter().map(Into::into).collect();
Ok(ud)
});
methods.add_function_mut("spacing", |_, (ud, spacing): (AnyUserData, u16)| {
methods.add_function("spacing", |_, (ud, spacing): (AnyUserData, u16)| {
ud.borrow_mut::<Self>()?.column_spacing = spacing;
Ok(ud)
});
methods.add_function_mut("row", |_, (ud, idx): (AnyUserData, Option<usize>)| {
methods.add_function("row", |_, (ud, idx): (AnyUserData, Option<usize>)| {
ud.borrow_mut::<Self>()?.state.select(idx);
Ok(ud)
});
methods.add_function_mut("col", |_, (ud, idx): (AnyUserData, Option<usize>)| {
methods.add_function("col", |_, (ud, idx): (AnyUserData, Option<usize>)| {
ud.borrow_mut::<Self>()?.state.select_column(idx);
Ok(ud)
});
methods.add_function_mut("style", |_, (ud, style): (AnyUserData, Style)| {
methods.add_function("style", |_, (ud, style): (AnyUserData, Style)| {
ud.borrow_mut::<Self>()?.style = style.0;
Ok(ud)
});
methods.add_function_mut("row_style", |_, (ud, style): (AnyUserData, Style)| {
methods.add_function("row_style", |_, (ud, style): (AnyUserData, Style)| {
ud.borrow_mut::<Self>()?.row_highlight_style = style.0;
Ok(ud)
});
methods.add_function_mut("col_style", |_, (ud, style): (AnyUserData, Style)| {
methods.add_function("col_style", |_, (ud, style): (AnyUserData, Style)| {
ud.borrow_mut::<Self>()?.column_highlight_style = style.0;
Ok(ud)
});
methods.add_function_mut("cell_style", |_, (ud, style): (AnyUserData, Style)| {
methods.add_function("cell_style", |_, (ud, style): (AnyUserData, Style)| {
ud.borrow_mut::<Self>()?.cell_highlight_style = style.0;
Ok(ud)
});

View file

@ -124,15 +124,15 @@ impl UserData for Text {
crate::impl_style_method!(methods, inner.style);
crate::impl_style_shorthands!(methods, inner.style);
methods.add_function_mut("align", |_, (ud, align): (AnyUserData, Align)| {
methods.add_function("align", |_, (ud, align): (AnyUserData, Align)| {
ud.borrow_mut::<Self>()?.inner.alignment = Some(align.0);
Ok(ud)
});
methods.add_function_mut("wrap", |_, (ud, wrap): (AnyUserData, Wrap)| {
methods.add_function("wrap", |_, (ud, wrap): (AnyUserData, Wrap)| {
ud.borrow_mut::<Self>()?.wrap = wrap;
Ok(ud)
});
methods.add_function_mut("scroll", |_, (ud, x, y): (AnyUserData, u16, u16)| {
methods.add_function("scroll", |_, (ud, x, y): (AnyUserData, u16, u16)| {
ud.borrow_mut::<Self>()?.scroll = ratatui::layout::Position { x, y };
Ok(ud)
});

View file

@ -10,7 +10,7 @@ const EXPECTED: &str = "expected a table, File, or fs::File";
#[derive(Clone)]
pub struct File {
inner: yazi_fs::File,
pub(crate) inner: yazi_fs::File,
v_cha: Option<Value>,
v_url: Option<Value>,

View file

@ -2,7 +2,7 @@ use std::ops::Deref;
use mlua::{ExternalError, ExternalResult, FromLua, Lua, UserData, Value};
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Default)]
pub struct Id(pub yazi_shared::Id);
impl Deref for Id {

View file

@ -1,5 +1,5 @@
mod macros;
yazi_macro::mod_pub!(elements process);
yazi_macro::mod_pub!(config elements process);
yazi_macro::mod_flat!(access calculator cha chan chord_cow composer error fd file handle icon id image input iter layer mouse path permit range runtime scheme stage style url utils);

View file

@ -88,7 +88,7 @@ macro_rules! cached_field_mut {
#[macro_export]
macro_rules! impl_area_method {
($methods:ident) => {
$methods.add_function_mut(
$methods.add_function(
"area",
|lua, (ud, area): (mlua::AnyUserData, Option<mlua::AnyUserData>)| {
use mlua::IntoLua;
@ -106,7 +106,7 @@ macro_rules! impl_area_method {
#[macro_export]
macro_rules! impl_style_method {
($methods:ident, $($field:tt).+) => {
$methods.add_function_mut("style", |_, (ud, style): (mlua::AnyUserData, $crate::Style)| {
$methods.add_function("style", |_, (ud, style): (mlua::AnyUserData, $crate::Style)| {
ud.borrow_mut::<Self>()?.$($field).+ = style.0;
Ok(ud)
});
@ -116,7 +116,7 @@ macro_rules! impl_style_method {
#[macro_export]
macro_rules! impl_style_shorthands {
($methods:ident, $($field:tt).+) => {
$methods.add_function_mut("fg", |lua, (ud, value): (mlua::AnyUserData, mlua::Value)| {
$methods.add_function("fg", |lua, (ud, value): (mlua::AnyUserData, mlua::Value)| {
use $crate::elements::Color;
use mlua::FromLua;
use ratatui::style::Modifier;
@ -135,7 +135,7 @@ macro_rules! impl_style_shorthands {
}
}
});
$methods.add_function_mut("bg", |lua, (ud, value): (mlua::AnyUserData, mlua::Value)| {
$methods.add_function("bg", |lua, (ud, value): (mlua::AnyUserData, mlua::Value)| {
use $crate::elements::Color;
use mlua::FromLua;
use ratatui::style::Modifier;
@ -154,7 +154,7 @@ macro_rules! impl_style_shorthands {
}
}
});
$methods.add_function_mut("bold", |_, (ud, remove): (mlua::AnyUserData, bool)| {
$methods.add_function("bold", |_, (ud, remove): (mlua::AnyUserData, bool)| {
let me = &mut ud.borrow_mut::<Self>()?.$($field).+;
if remove {
*me = me.remove_modifier(ratatui::style::Modifier::BOLD);
@ -163,7 +163,7 @@ macro_rules! impl_style_shorthands {
}
Ok(ud)
});
$methods.add_function_mut("dim", |_, (ud, remove): (mlua::AnyUserData, bool)| {
$methods.add_function("dim", |_, (ud, remove): (mlua::AnyUserData, bool)| {
let me = &mut ud.borrow_mut::<Self>()?.$($field).+;
if remove {
*me = me.remove_modifier(ratatui::style::Modifier::DIM);
@ -172,7 +172,7 @@ macro_rules! impl_style_shorthands {
}
Ok(ud)
});
$methods.add_function_mut("italic", |_, (ud, remove): (mlua::AnyUserData, bool)| {
$methods.add_function("italic", |_, (ud, remove): (mlua::AnyUserData, bool)| {
let me = &mut ud.borrow_mut::<Self>()?.$($field).+;
if remove {
*me = me.remove_modifier(ratatui::style::Modifier::ITALIC);
@ -181,7 +181,7 @@ macro_rules! impl_style_shorthands {
}
Ok(ud)
});
$methods.add_function_mut("underline", |_, (ud, remove): (mlua::AnyUserData, bool)| {
$methods.add_function("underline", |_, (ud, remove): (mlua::AnyUserData, bool)| {
let me = &mut ud.borrow_mut::<Self>()?.$($field).+;
if remove {
*me = me.remove_modifier(ratatui::style::Modifier::UNDERLINED);
@ -190,7 +190,7 @@ macro_rules! impl_style_shorthands {
}
Ok(ud)
});
$methods.add_function_mut("blink", |_, (ud, remove): (mlua::AnyUserData, bool)| {
$methods.add_function("blink", |_, (ud, remove): (mlua::AnyUserData, bool)| {
let me = &mut ud.borrow_mut::<Self>()?.$($field).+;
if remove {
*me = me.remove_modifier(ratatui::style::Modifier::SLOW_BLINK);
@ -199,7 +199,7 @@ macro_rules! impl_style_shorthands {
}
Ok(ud)
});
$methods.add_function_mut("blink_rapid", |_, (ud, remove): (mlua::AnyUserData, bool)| {
$methods.add_function("blink_rapid", |_, (ud, remove): (mlua::AnyUserData, bool)| {
let me = &mut ud.borrow_mut::<Self>()?.$($field).+;
if remove {
*me = me.remove_modifier(ratatui::style::Modifier::RAPID_BLINK);
@ -208,7 +208,7 @@ macro_rules! impl_style_shorthands {
}
Ok(ud)
});
$methods.add_function_mut("reverse", |_, (ud, remove): (mlua::AnyUserData, bool)| {
$methods.add_function("reverse", |_, (ud, remove): (mlua::AnyUserData, bool)| {
let me = &mut ud.borrow_mut::<Self>()?.$($field).+;
if remove {
*me = me.remove_modifier(ratatui::style::Modifier::REVERSED);
@ -217,7 +217,7 @@ macro_rules! impl_style_shorthands {
}
Ok(ud)
});
$methods.add_function_mut("hidden", |_, (ud, remove): (mlua::AnyUserData, bool)| {
$methods.add_function("hidden", |_, (ud, remove): (mlua::AnyUserData, bool)| {
let me = &mut ud.borrow_mut::<Self>()?.$($field).+;
if remove {
*me = me.remove_modifier(ratatui::style::Modifier::HIDDEN);
@ -226,7 +226,7 @@ macro_rules! impl_style_shorthands {
}
Ok(ud)
});
$methods.add_function_mut("crossed", |_, (ud, remove): (mlua::AnyUserData, bool)| {
$methods.add_function("crossed", |_, (ud, remove): (mlua::AnyUserData, bool)| {
let me = &mut ud.borrow_mut::<Self>()?.$($field).+;
if remove {
*me = me.remove_modifier(ratatui::style::Modifier::CROSSED_OUT);
@ -235,7 +235,7 @@ macro_rules! impl_style_shorthands {
}
Ok(ud)
});
$methods.add_function_mut("reset", |_, ud: mlua::AnyUserData| {
$methods.add_function("reset", |_, ud: mlua::AnyUserData| {
ud.borrow_mut::<Self>()?.$($field).+ = ratatui::style::Style::reset();
Ok(ud)
});

View file

@ -3,10 +3,9 @@ use std::{ops::DerefMut, process::ExitStatus, time::Duration};
use futures::future::try_join3;
use mlua::{AnyUserData, ExternalError, IntoLua, IntoLuaMulti, Table, UserData, UserDataMethods, Value};
use tokio::{io::{self, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}, process::{ChildStderr, ChildStdin, ChildStdout}, select};
use crate::Error;
use super::Status;
use crate::process::Output;
use crate::{Error, process::Output};
pub struct Child {
inner: tokio::process::Child,

View file

@ -138,7 +138,7 @@ impl UserData for Command {
)
}
methods.add_function_mut("arg", |lua, (ud, arg): (AnyUserData, Value)| {
methods.add_function("arg", |lua, (ud, arg): (AnyUserData, Value)| {
let mut me = ud.borrow_mut::<Self>()?;
match arg {
Value::Nil => return lua.create_sequence_from(me.inner.as_std().get_args())?.into_lua(lua),
@ -154,11 +154,11 @@ impl UserData for Command {
}
ud.into_lua(lua)
});
methods.add_function_mut("cwd", |_, (ud, dir): (AnyUserData, mlua::String)| {
methods.add_function("cwd", |_, (ud, dir): (AnyUserData, mlua::String)| {
ud.borrow_mut::<Self>()?.inner.current_dir(dir.to_str()?.as_ref());
Ok(ud)
});
methods.add_function_mut(
methods.add_function(
"env",
|_, (ud, key, value): (AnyUserData, mlua::String, mlua::String)| {
ud.borrow_mut::<Self>()?
@ -167,19 +167,19 @@ impl UserData for Command {
Ok(ud)
},
);
methods.add_function_mut("stdin", |_, (ud, stdio): (AnyUserData, Value)| {
methods.add_function("stdin", |_, (ud, stdio): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.inner.stdin(make_stdio(stdio)?);
Ok(ud)
});
methods.add_function_mut("stdout", |_, (ud, stdio): (AnyUserData, Value)| {
methods.add_function("stdout", |_, (ud, stdio): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.inner.stdout(make_stdio(stdio)?);
Ok(ud)
});
methods.add_function_mut("stderr", |_, (ud, stdio): (AnyUserData, Value)| {
methods.add_function("stderr", |_, (ud, stdio): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.inner.stderr(make_stdio(stdio)?);
Ok(ud)
});
methods.add_function_mut("memory", |_, (ud, max): (AnyUserData, usize)| {
methods.add_function("memory", |_, (ud, max): (AnyUserData, usize)| {
ud.borrow_mut::<Self>()?.memory = Some(max);
Ok(ud)
});

View file

@ -1,9 +1,9 @@
use std::mem;
use mlua::{UserData, Value};
use crate::{cached_field, cached_field_mut};
use super::Status;
use crate::{cached_field, cached_field_mut};
pub struct Output {
inner: std::process::Output,

View file

@ -2,6 +2,7 @@ use std::ops::Deref;
use mlua::{ExternalError, FromLua, IntoLua, Lua, LuaSerdeExt, MetaMethod, Table, UserData, UserDataMethods, Value};
use ratatui::style::Modifier;
use yazi_shared::SyncCell;
use crate::{SER_OPT, elements::Color};
@ -22,6 +23,10 @@ impl From<Style> for ratatui::style::Style {
fn from(value: Style) -> ratatui::style::Style { value.0 }
}
impl From<&SyncCell<yazi_config::Style>> for Style {
fn from(value: &SyncCell<yazi_config::Style>) -> Self { value.get().into() }
}
impl Style {
pub fn compose(lua: &Lua) -> mlua::Result<Value> {
let new = lua.create_function(|_, (_, style): (Table, Self)| Ok(style))?;

View file

@ -203,7 +203,7 @@ impl UserData for Url {
methods.add_method("starts_with", |_, me, base: Value| me.starts_with(base));
methods.add_method("strip_prefix", |_, me, base: Value| me.strip_prefix(base));
methods.add_function_mut("into_search", |_, (ud, domain): (AnyUserData, mlua::String)| {
methods.add_function("into_search", |_, (ud, domain): (AnyUserData, mlua::String)| {
let url = ud.take::<Self>()?.inner.into_search(domain.to_str()?).into_lua_err()?;
Ok(Self::new(url))
});

View file

@ -1,24 +1,22 @@
use proc_macro::TokenStream;
use quote::quote;
use quote::{format_ident, quote};
use syn::{Data, DeriveInput, Fields, parse_macro_input};
#[proc_macro_derive(DeserializeOver)]
pub fn deserialize_over(input: TokenStream) -> TokenStream {
let DeriveInput { ident, .. } = parse_macro_input!(input as DeriveInput);
let DeriveInput { ident, generics, .. } = parse_macro_input!(input as DeriveInput);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! {
impl #ident {
pub(crate) fn deserialize_over(self, input: &str) -> Result<Self, toml::de::Error> {
crate::error_with_input(self.deserialize_over_with(toml::de::DeTable::parse(input)?), input)
}
}
impl #impl_generics yazi_shim::toml::DeserializeOverHook for #ident #ty_generics #where_clause {}
}
.into()
}
#[proc_macro_derive(DeserializeOver1)]
pub fn deserialize_over1(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);
let DeriveInput { ident, generics, data, .. } = parse_macro_input!(input as DeriveInput);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let assignments = match data {
Data::Struct(struct_) => match struct_.fields {
@ -32,7 +30,7 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream {
assignments.push(quote! {
if let Some(value) = table.remove(#field_name) {
if !matches!(value.get_ref(), toml::de::DeValue::Table(_)) {
_ = toml::Table::deserialize(value.into_deserializer())?;
_ = deserialize_spanned::<toml::Table>(value)?;
return Err(serde::de::Error::custom(format!("expected top-level `{}` to be a TOML table", #field_name)));
}
@ -41,6 +39,7 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream {
self.#field_ident = self.#field_ident.deserialize_over_with(toml::Spanned::new(span, table))?;
}
}
self.#field_ident = self.#field_ident.deserialize_over_hook()?;
});
}
@ -52,14 +51,14 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream {
};
quote! {
impl #ident {
pub(crate) fn deserialize_over_with<'de>(mut self, table: toml::Spanned<toml::de::DeTable<'de>>) -> Result<Self, toml::de::Error> {
use serde::{Deserialize, de::IntoDeserializer};
impl #impl_generics yazi_shim::toml::DeserializeOverWith for #ident #ty_generics #where_clause {
fn deserialize_over_with<'de>(mut self, table: toml::Spanned<toml::de::DeTable<'de>>) -> Result<Self, toml::de::Error> {
use yazi_shim::toml::{DeserializeOverHook, DeserializeOverWith, deserialize_spanned};
let mut table = table.into_inner();
#(#assignments)*
Ok(self)
self.deserialize_over_hook()
}
}
}
@ -68,20 +67,30 @@ pub fn deserialize_over1(input: TokenStream) -> TokenStream {
#[proc_macro_derive(DeserializeOver2)]
pub fn deserialize_over2(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);
let DeriveInput { ident, generics, data, .. } = parse_macro_input!(input as DeriveInput);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let assignments = match data {
Data::Struct(struct_) => match struct_.fields {
Fields::Named(fields) => {
let mut assignments = Vec::with_capacity(fields.named.len());
for field in fields.named {
for (index, field) in fields.named.into_iter().enumerate() {
let field_ident = field.ident;
let field_name = field_ident.as_ref().unwrap().to_string();
let field_ty = field.ty;
let field_attrs = field.attrs;
let field_helper = format_ident!("__DeserializeOver2Field{index}");
assignments.push(quote! {
#[derive(serde::Deserialize)]
struct #field_helper #impl_generics (
#(#field_attrs)*
#field_ty
) #where_clause;
if let Some(value) = table.remove(#field_name) {
self.#field_ident = <_>::deserialize(value.into_deserializer())?;
self.#field_ident = deserialize_spanned::<#field_helper #ty_generics>(value)?.0;
}
});
}
@ -94,9 +103,9 @@ pub fn deserialize_over2(input: TokenStream) -> TokenStream {
};
quote! {
impl #ident {
pub(crate) fn deserialize_over_with<'de>(mut self, table: toml::Spanned<toml::de::DeTable<'de>>) -> Result<Self, toml::de::Error> {
use serde::{Deserialize, de::IntoDeserializer};
impl #impl_generics yazi_shim::toml::DeserializeOverWith for #ident #ty_generics #where_clause {
fn deserialize_over_with<'de>(mut self, table: toml::Spanned<toml::de::DeTable<'de>>) -> Result<Self, toml::de::Error> {
use yazi_shim::toml::deserialize_spanned;
let mut table = table.into_inner();
#(#assignments)*

View file

@ -17,22 +17,25 @@ yazi-codegen = { path = "../yazi-codegen", version = "26.2.2" }
yazi-fs = { path = "../yazi-fs", version = "26.2.2" }
yazi-macro = { path = "../yazi-macro", version = "26.2.2" }
yazi-shared = { path = "../yazi-shared", version = "26.2.2" }
yazi-shim = { path = "../yazi-shim", version = "26.2.2" }
yazi-tty = { path = "../yazi-tty", version = "26.2.2" }
# External dependencies
anyhow = { workspace = true }
bitflags = { workspace = true }
crossterm = { workspace = true }
globset = { workspace = true }
hashbrown = { workspace = true }
indexmap = { workspace = true }
ratatui = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
strum = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
anyhow = { workspace = true }
arc-swap = { workspace = true }
bitflags = { workspace = true }
crossterm = { workspace = true }
globset = { workspace = true }
hashbrown = { workspace = true }
indexmap = { workspace = true }
ratatui = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
serde_with = { workspace = true }
strum = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -98,10 +98,10 @@ suppress_preload = false
[plugin]
fetchers = [
# Mimetype
{ id = "mime", url = "*/", run = "mime.dir", prio = "high" },
{ id = "mime", url = "local://*", run = "mime.local", prio = "high" },
{ id = "mime", url = "remote://*", run = "mime.remote", prio = "high" },
# MIME-type
{ url = "*/", run = "mime.dir", prio = "high", group = "mime" },
{ url = "local://*", run = "mime.local", prio = "high", group = "mime" },
{ url = "remote://*", run = "mime.remote", prio = "high", group = "mime" },
]
spotters = [
# Multi-file

View file

@ -1,7 +1,10 @@
use serde::Deserialize;
use crate::Style;
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Deserialize)]
pub struct Icon {
pub text: String,
#[serde(flatten)]
pub style: Style,
}

View file

@ -1,37 +1,38 @@
use std::{borrow::Cow, hash::{Hash, Hasher}, sync::OnceLock};
use anyhow::Result;
use regex::Regex;
use serde::Deserialize;
use serde::{Deserialize, Deserializer, de};
use serde_with::{DeserializeAs, DisplayFromStr, OneOrMany};
use yazi_shared::{Layer, Source, event::Action};
use super::Key;
use crate::Platform;
use crate::{Mixable, Platform};
static RE: OnceLock<Regex> = OnceLock::new();
#[derive(Clone, Debug, Default, Deserialize)]
pub struct Chord {
#[serde(deserialize_with = "super::deserialize_on")]
pub struct Chord<const L: u8 = { Layer::App as u8 }> {
#[serde(deserialize_with = "deserialize_on")]
pub on: Vec<Key>,
#[serde(deserialize_with = "super::deserialize_run")]
#[serde(deserialize_with = "deserialize_run::<L, _>")]
pub run: Vec<Action>,
pub desc: Option<String>,
#[serde(default)]
pub desc: String,
#[serde(default)]
pub r#for: Platform,
}
impl PartialEq for Chord {
impl<const L: u8> PartialEq for Chord<L> {
fn eq(&self, other: &Self) -> bool { self.on == other.on }
}
impl Eq for Chord {}
impl<const L: u8> Eq for Chord<L> {}
impl Hash for Chord {
impl<const L: u8> Hash for Chord<L> {
fn hash<H: Hasher>(&self, state: &mut H) { self.on.hash(state) }
}
impl Chord {
impl<const L: u8> Chord<L> {
pub fn on(&self) -> String { self.on.iter().map(ToString::to_string).collect() }
pub fn run(&self) -> String {
@ -41,7 +42,9 @@ impl Chord {
}
pub fn desc(&self) -> Option<Cow<'_, str>> {
self.desc.as_ref().map(|s| RE.get_or_init(|| Regex::new(r"\s+").unwrap()).replace_all(s, " "))
Some(&self.desc)
.filter(|s| !s.is_empty())
.map(|s| RE.get_or_init(|| Regex::new(r"\s+").unwrap()).replace_all(s, " "))
}
pub fn desc_or_run(&self) -> Cow<'_, str> { self.desc().unwrap_or_else(|| self.run().into()) }
@ -59,14 +62,40 @@ impl Chord {
}
}
impl Chord {
pub(super) fn reshape(mut self, layer: Layer) -> Result<Self> {
for action in &mut self.run {
action.source = Source::Key;
if action.layer == Default::default() {
action.layer = layer;
}
}
Ok(self)
}
impl<const L: u8> Mixable for Chord<L> {
fn filter(&self) -> bool { self.r#for.matches() && !self.noop() }
}
fn deserialize_on<'de, D>(deserializer: D) -> Result<Vec<Key>, D::Error>
where
D: Deserializer<'de>,
{
let keys: Vec<Key> = OneOrMany::<DisplayFromStr>::deserialize_as(deserializer)?;
if keys.is_empty() {
return Err(de::Error::custom("'on' cannot be empty"));
}
Ok(keys)
}
fn deserialize_run<'de, const L: u8, D>(deserializer: D) -> Result<Vec<Action>, D::Error>
where
D: Deserializer<'de>,
{
let mut actions: Vec<Action> = OneOrMany::<DisplayFromStr>::deserialize_as(deserializer)?;
if actions.is_empty() {
return Err(de::Error::custom("'run' cannot be empty"));
}
let Some(layer) = Layer::from_repr(L) else {
return Err(de::Error::custom(format!("invalid keymap layer const: {L}")));
};
for action in &mut actions {
action.source = Source::Key;
if action.layer == Default::default() {
action.layer = layer;
}
}
Ok(actions)
}

View file

@ -1,26 +1,25 @@
use std::ops::Deref;
use yazi_shared::event::ActionCow;
use yazi_shared::{Layer, event::ActionCow};
use super::Chord;
use crate::Platform;
#[derive(Clone, Debug)]
pub enum ChordCow {
Owned(Chord),
Borrowed(&'static Chord),
pub enum ChordCow<const L: u8 = { Layer::App as u8 }> {
Owned(Chord<L>),
Borrowed(&'static Chord<L>),
}
impl From<Chord> for ChordCow {
fn from(c: Chord) -> Self { Self::Owned(c) }
impl<const L: u8> From<Chord<L>> for ChordCow<L> {
fn from(c: Chord<L>) -> Self { Self::Owned(c) }
}
impl From<&'static Chord> for ChordCow {
fn from(c: &'static Chord) -> Self { Self::Borrowed(c) }
impl<const L: u8> From<&'static Chord<L>> for ChordCow<L> {
fn from(c: &'static Chord<L>) -> Self { Self::Borrowed(c) }
}
impl Deref for ChordCow {
type Target = Chord;
impl<const L: u8> Deref for ChordCow<L> {
type Target = Chord<L>;
fn deref(&self) -> &Self::Target {
match self {
@ -30,14 +29,11 @@ impl Deref for ChordCow {
}
}
impl Default for ChordCow {
fn default() -> Self {
const C: &Chord = &Chord { on: vec![], run: vec![], desc: None, r#for: Platform::All };
Self::Borrowed(C)
}
impl<const L: u8> Default for ChordCow<L> {
fn default() -> Self { Self::Owned(Chord::default()) }
}
impl ChordCow {
impl<const L: u8> ChordCow<L> {
pub fn into_seq(self) -> Vec<ActionCow> {
match self {
Self::Owned(c) => c.run.into_iter().rev().map(Into::into).collect(),

View file

@ -1,83 +0,0 @@
use std::{fmt, str::FromStr};
use anyhow::Result;
use serde::{Deserializer, de::{self, Visitor}};
use yazi_shared::event::Action;
use crate::keymap::Key;
pub(super) fn deserialize_on<'de, D>(deserializer: D) -> Result<Vec<Key>, D::Error>
where
D: Deserializer<'de>,
{
struct OnVisitor;
impl<'de> Visitor<'de> for OnVisitor {
type Value = Vec<Key>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a `on` string or array of strings within keymap.toml")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
let mut keys = Vec::with_capacity(seq.size_hint().unwrap_or(0));
while let Some(value) = &seq.next_element::<String>()? {
keys.push(Key::from_str(value).map_err(de::Error::custom)?);
}
if keys.is_empty() {
return Err(de::Error::custom("`on` within keymap.toml cannot be empty"));
}
Ok(keys)
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(vec![Key::from_str(value).map_err(de::Error::custom)?])
}
}
deserializer.deserialize_any(OnVisitor)
}
pub(super) fn deserialize_run<'de, D>(deserializer: D) -> Result<Vec<Action>, D::Error>
where
D: Deserializer<'de>,
{
struct RunVisitor;
impl<'de> Visitor<'de> for RunVisitor {
type Value = Vec<Action>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a `run` string or array of strings within keymap.toml")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
let mut actions = Vec::with_capacity(seq.size_hint().unwrap_or(0));
while let Some(value) = &seq.next_element::<String>()? {
actions.push(Action::from_str(value).map_err(de::Error::custom)?);
}
if actions.is_empty() {
return Err(de::Error::custom("`run` within keymap.toml cannot be empty"));
}
Ok(actions)
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(vec![Action::from_str(value).map_err(de::Error::custom)?])
}
}
deserializer.deserialize_any(RunVisitor)
}

View file

@ -8,28 +8,28 @@ use super::{Chord, KeymapRules};
#[derive(Deserialize, DeserializeOver, DeserializeOver1)]
pub struct Keymap {
pub mgr: KeymapRules,
pub tasks: KeymapRules,
pub spot: KeymapRules,
pub pick: KeymapRules,
pub input: KeymapRules,
pub confirm: KeymapRules,
pub help: KeymapRules,
pub cmp: KeymapRules,
pub mgr: KeymapRules<{ Layer::Mgr as u8 }>,
pub tasks: KeymapRules<{ Layer::Tasks as u8 }>,
pub spot: KeymapRules<{ Layer::Spot as u8 }>,
pub pick: KeymapRules<{ Layer::Pick as u8 }>,
pub input: KeymapRules<{ Layer::Input as u8 }>,
pub confirm: KeymapRules<{ Layer::Confirm as u8 }>,
pub help: KeymapRules<{ Layer::Help as u8 }>,
pub cmp: KeymapRules<{ Layer::Cmp as u8 }>,
}
impl Keymap {
pub fn get(&self, layer: Layer) -> &[Chord] {
match layer {
Layer::App => &[],
Layer::Mgr => &self.mgr,
Layer::Tasks => &self.tasks,
Layer::Spot => &self.spot,
Layer::Pick => &self.pick,
Layer::Input => &self.input,
Layer::Confirm => &self.confirm,
Layer::Help => &self.help,
Layer::Cmp => &self.cmp,
Layer::Mgr => self.mgr.as_erased_slice(),
Layer::Tasks => self.tasks.as_erased_slice(),
Layer::Spot => self.spot.as_erased_slice(),
Layer::Pick => self.pick.as_erased_slice(),
Layer::Input => self.input.as_erased_slice(),
Layer::Confirm => self.confirm.as_erased_slice(),
Layer::Help => self.help.as_erased_slice(),
Layer::Cmp => self.cmp.as_erased_slice(),
Layer::Which => &[],
Layer::Notify => &[],
}
@ -42,17 +42,4 @@ impl Keymap {
ok_or_not_found(std::fs::read_to_string(&p))
.with_context(|| format!("Failed to read keymap {p:?}"))
}
pub(crate) fn reshape(self) -> Result<Self> {
Ok(Self {
mgr: self.mgr.reshape(Layer::Mgr)?,
tasks: self.tasks.reshape(Layer::Tasks)?,
spot: self.spot.reshape(Layer::Spot)?,
pick: self.pick.reshape(Layer::Pick)?,
input: self.input.reshape(Layer::Input)?,
confirm: self.confirm.reshape(Layer::Confirm)?,
help: self.help.reshape(Layer::Help)?,
cmp: self.cmp.reshape(Layer::Cmp)?,
})
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(chord cow deserializers key keymap rules);
yazi_macro::mod_flat!(chord cow key keymap rules);

View file

@ -1,47 +1,52 @@
use std::ops::Deref;
use anyhow::Result;
use hashbrown::HashSet;
use serde::Deserialize;
use yazi_codegen::DeserializeOver2;
use yazi_shared::Layer;
use yazi_shim::toml::DeserializeOverHook;
use super::Chord;
use crate::{Preset, keymap::Key};
use super::{Chord, Key};
use crate::mix;
#[derive(Default, Deserialize, DeserializeOver2)]
pub struct KeymapRules {
pub keymap: Vec<Chord>,
pub struct KeymapRules<const L: u8> {
keymap: Vec<Chord<L>>,
#[serde(default)]
prepend_keymap: Vec<Chord>,
prepend_keymap: Vec<Chord<L>>,
#[serde(default)]
append_keymap: Vec<Chord>,
append_keymap: Vec<Chord<L>>,
}
impl Deref for KeymapRules {
type Target = Vec<Chord>;
impl<const L: u8> Deref for KeymapRules<L> {
type Target = [Chord];
fn deref(&self) -> &Self::Target { &self.keymap }
fn deref(&self) -> &Self::Target { self.as_erased_slice() }
}
impl KeymapRules {
pub(crate) fn reshape(self, layer: Layer) -> Result<Self> {
impl<const L: u8> KeymapRules<L> {
pub(super) fn as_erased_slice(&self) -> &[Chord] {
// Safety: `Chord<L>` only changes deserialization behavior; the const parameter
// does not participate in layout, so a shared slice can be reinterpreted as
// the default `Chord` view.
unsafe { &*(self.keymap.as_slice() as *const [Chord<L>] as *const [Chord]) }
}
}
impl<const L: u8> DeserializeOverHook for KeymapRules<L> {
fn deserialize_over_hook(self) -> Result<Self, toml::de::Error> {
#[inline]
fn on(Chord { on, .. }: &Chord) -> [Key; 2] {
fn on<const L: u8>(Chord { on, .. }: &Chord<L>) -> [Key; 2] {
[on.first().copied().unwrap_or_default(), on.get(1).copied().unwrap_or_default()]
}
let a_seen: HashSet<_> = self.prepend_keymap.iter().map(on).collect();
let b_seen: HashSet<_> = self.keymap.iter().map(on).collect();
let keymap = Preset::mix(
let keymap = mix(
self.prepend_keymap,
self.keymap.into_iter().filter(|v| !a_seen.contains(&on(v))),
self.append_keymap.into_iter().filter(|v| !b_seen.contains(&on(v))),
)
.filter(|chord| !chord.noop() && chord.r#for.matches())
.map(|chord| chord.reshape(layer))
.collect::<Result<_>>()?;
);
Ok(Self { keymap, ..Default::default() })
}

View file

@ -1,10 +1,11 @@
yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme which vfs);
yazi_macro::mod_flat!(icon layout pattern platform preset priority style utils yazi);
yazi_macro::mod_flat!(icon layout mixing pattern platform preset priority style utils yazi);
use std::io::{Read, Write};
use yazi_shared::{RoCell, SyncCell};
use yazi_shim::toml::{DeserializeOver, DeserializeOverWith};
use yazi_tty::TTY;
pub static YAZI: RoCell<yazi::Yazi> = RoCell::new();
@ -29,8 +30,8 @@ fn try_init(merge: bool) -> anyhow::Result<()> {
keymap = keymap.deserialize_over(&keymap::Keymap::read()?)?;
}
YAZI.init(yazi.reshape()?);
KEYMAP.init(keymap.reshape()?);
YAZI.init(yazi);
KEYMAP.init(keymap);
Ok(())
}

View file

@ -0,0 +1,23 @@
use std::ops::Deref;
use serde::{Deserialize, Deserializer, de};
#[derive(Debug)]
pub struct MgrLinemode(String);
impl Deref for MgrLinemode {
type Target = String;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl<'de> Deserialize<'de> for MgrLinemode {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
if s.is_empty() || s.len() > 20 {
return Err(de::Error::custom("linemode must be between 1 and 20 characters."));
}
Ok(Self(s))
}
}

View file

@ -1,12 +1,13 @@
use anyhow::{Result, bail};
use anyhow::Result;
use serde::Deserialize;
use yazi_codegen::DeserializeOver2;
use yazi_codegen::{DeserializeOver, DeserializeOver2};
use yazi_fs::{SortBy, SortFallback};
use yazi_shared::SyncCell;
use super::{MgrRatio, MouseEvents};
use crate::mgr::MgrLinemode;
#[derive(Debug, Deserialize, DeserializeOver2)]
#[derive(Debug, Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Mgr {
pub ratio: SyncCell<MgrRatio>,
@ -19,19 +20,9 @@ pub struct Mgr {
pub sort_fallback: SyncCell<SortFallback>,
// Display
pub linemode: String,
pub linemode: MgrLinemode,
pub show_hidden: SyncCell<bool>,
pub show_symlink: SyncCell<bool>,
pub scrolloff: SyncCell<u8>,
pub mouse_events: SyncCell<MouseEvents>,
}
impl Mgr {
pub(crate) fn reshape(self) -> Result<Self> {
if self.linemode.is_empty() || self.linemode.len() > 20 {
bail!("[mgr].linemode must be between 1 and 20 characters.");
}
Ok(self)
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(mgr mouse ratio);
yazi_macro::mod_flat!(linemode mgr mouse ratio);

77
yazi-config/src/mixing.rs Normal file
View file

@ -0,0 +1,77 @@
use std::{mem, sync::Arc};
pub(crate) trait Mixable {
fn filter(&self) -> bool { true }
fn any_file(&self) -> bool { false }
fn any_dir(&self) -> bool { false }
}
impl<T: Mixable> Mixable for Arc<T> {
fn filter(&self) -> bool { (**self).filter() }
fn any_file(&self) -> bool { (**self).any_file() }
fn any_dir(&self) -> bool { (**self).any_dir() }
}
pub(crate) fn mix<E, A, B, C>(a: A, b: B, c: C) -> Vec<E>
where
E: Mixable,
A: IntoIterator,
A::Item: Into<E> + Mixable,
B: IntoIterator,
B::Item: Into<E> + Mixable,
C: IntoIterator,
C::Item: Into<E> + Mixable,
{
fn dedup<E, I>(it: I, any_file: &mut bool, any_dir: &mut bool) -> impl Iterator<Item = E>
where
I: Iterator,
I::Item: Into<E> + Mixable,
{
it.filter(move |x| {
if !x.filter() {
false
} else if x.any_file() && mem::replace(any_file, true) {
false
} else if x.any_dir() && mem::replace(any_dir, true) {
false
} else {
true
}
})
.map(Into::into)
}
let (a, b, c) = (a.into_iter(), b.into_iter(), c.into_iter());
let mut mixed = Vec::with_capacity(a.size_hint().0 + b.size_hint().0 + c.size_hint().0);
let (mut a_any_file, mut a_any_dir) = (false, false);
mixed.extend(dedup(a, &mut a_any_file, &mut a_any_dir));
let a_len = mixed.len();
mixed.extend(dedup(b, &mut a_any_file, &mut a_any_dir));
let b_len = mixed.len();
let (mut c_any_file, mut c_any_dir) = (false, false);
mixed.extend(dedup(c, &mut c_any_file, &mut c_any_dir));
if c_any_file || c_any_dir {
let mut i = 0;
mixed.retain(|x| {
let in_b = (a_len..b_len).contains(&i);
i += 1;
if in_b && c_any_file && x.any_file() {
return false;
} else if in_b && c_any_dir && x.any_dir() {
return false;
}
true
})
}
mixed
}

View file

@ -5,8 +5,9 @@ use indexmap::IndexSet;
use serde::Deserialize;
use yazi_codegen::DeserializeOver2;
use yazi_shared::url::AsUrl;
use yazi_shim::toml::DeserializeOverHook;
use crate::{Preset, open::OpenRule};
use crate::{mix, open::OpenRule};
#[derive(Default, Deserialize, DeserializeOver2)]
pub struct Open {
@ -60,17 +61,8 @@ impl Open {
}
}
impl Open {
pub(crate) fn reshape(self) -> Result<Self> {
let any_file = self.append_rules.iter().any(|r| r.any_file());
let any_dir = self.append_rules.iter().any(|r| r.any_dir());
let it =
self.rules.into_iter().filter(|r| !(any_file && r.any_file() || any_dir && r.any_dir()));
Ok(Self {
rules: Preset::mix(self.prepend_rules, it, self.append_rules).collect(),
..Default::default()
})
impl DeserializeOverHook for Open {
fn deserialize_over_hook(self) -> Result<Self, toml::de::Error> {
Ok(Self { rules: mix(self.prepend_rules, self.rules, self.append_rules), ..Default::default() })
}
}

View file

@ -1,65 +1,19 @@
use std::fmt;
use serde::Deserialize;
use serde_with::{OneOrMany, formats::PreferOne, serde_as};
use serde::{Deserialize, Deserializer, de::{self, Visitor}};
use crate::pattern::Pattern;
use crate::{Mixable, pattern::Pattern};
#[serde_as]
#[derive(Debug, Deserialize)]
pub struct OpenRule {
pub url: Option<Pattern>,
pub mime: Option<Pattern>,
#[serde(deserialize_with = "OpenRule::deserialize")]
#[serde_as(as = "OneOrMany<_, PreferOne>")]
pub r#use: Vec<String>,
}
impl OpenRule {
#[inline]
pub fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) }
impl Mixable for OpenRule {
fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) }
#[inline]
pub fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) }
}
impl OpenRule {
fn deserialize<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
struct UseVisitor;
impl<'de> Visitor<'de> for UseVisitor {
type Value = Vec<String>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string, or array of strings")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
let mut uses = Vec::with_capacity(seq.size_hint().unwrap_or(0));
while let Some(use_) = seq.next_element::<String>()? {
uses.push(use_);
}
Ok(uses)
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(vec![value.to_owned()])
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(vec![v])
}
}
deserializer.deserialize_any(UseVisitor)
}
fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) }
}

View file

@ -1,15 +1,13 @@
use std::{mem, ops::Deref};
use std::ops::Deref;
use anyhow::Result;
use hashbrown::HashMap;
use indexmap::IndexSet;
use serde::{Deserialize, de::IntoDeserializer};
use serde::Deserialize;
use toml::{Spanned, de::DeTable};
use yazi_codegen::DeserializeOver;
use yazi_shim::toml::{DeserializeOverHook, DeserializeOverWith, deserialize_spanned};
use super::OpenerRule;
#[derive(Debug, Deserialize, DeserializeOver)]
#[derive(Debug, Deserialize)]
pub struct Opener(HashMap<String, Vec<OpenerRule>>);
impl Deref for Opener {
@ -41,27 +39,24 @@ impl Opener {
}
}
impl Opener {
pub(crate) fn reshape(mut self) -> Result<Self> {
impl DeserializeOverHook for Opener {
fn deserialize_over_hook(mut self) -> Result<Self, toml::de::Error> {
for rules in self.0.values_mut() {
*rules = mem::take(rules)
.into_iter()
.filter(|r| r.r#for.matches())
.map(|r| r.reshape())
.collect::<Result<IndexSet<_>>>()?
.into_iter()
.collect();
}
Ok(self)
}
pub(crate) fn deserialize_over_with<'de>(
mut self,
table: Spanned<DeTable<'de>>,
) -> Result<Self, toml::de::Error> {
for (key, value) in table.into_inner() {
self.0.insert(key.into_inner().into_owned(), <_>::deserialize(value.into_deserializer())?);
rules.retain(|r| r.r#for.matches());
rules.iter_mut().for_each(|r| r.fill());
}
Ok(self)
}
}
impl DeserializeOverWith for Opener {
fn deserialize_over_with<'de>(
mut self,
table: Spanned<DeTable<'de>>,
) -> Result<Self, toml::de::Error> {
for (key, value) in table.into_inner() {
self.0.insert(key.into_inner().into_owned(), deserialize_spanned(value)?);
}
Ok(self)

View file

@ -1,12 +1,12 @@
use anyhow::{Result, bail};
use serde::Deserialize;
use yazi_fs::Splatter;
use yazi_shared::NonEmptyString;
use crate::Platform;
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct OpenerRule {
pub run: String,
pub run: NonEmptyString,
#[serde(default)]
pub block: bool,
#[serde(default)]
@ -29,14 +29,8 @@ impl OpenerRule {
String::new()
}
}
}
impl OpenerRule {
pub(super) fn reshape(mut self) -> Result<Self> {
if self.run.is_empty() {
bail!("[open].rules.*.run cannot be empty.");
}
pub(super) fn fill(&mut self) {
#[cfg(unix)]
{
self.spread =
@ -46,7 +40,5 @@ impl OpenerRule {
{
self.spread = Splatter::<()>::spread(&self.run) || self.run.contains("%*");
}
Ok(self)
}
}

View file

@ -5,6 +5,8 @@ use globset::{Candidate, GlobBuilder};
use serde::Deserialize;
use yazi_shared::{scheme::SchemeKind, url::AsUrl};
use crate::Mixable;
#[derive(Deserialize)]
#[serde(try_from = "String")]
pub struct Pattern {
@ -58,12 +60,6 @@ impl Pattern {
pub fn match_mime(&self, mime: impl AsRef<str>) -> bool {
self.is_star || (!mime.as_ref().is_empty() && self.inner.is_match(mime.as_ref()))
}
#[inline]
pub fn any_file(&self) -> bool { self.is_star && !self.is_dir }
#[inline]
pub fn any_dir(&self) -> bool { self.is_star && self.is_dir }
}
impl FromStr for Pattern {
@ -102,12 +98,19 @@ impl FromStr for Pattern {
}
}
// FIXME: remove
impl TryFrom<String> for Pattern {
type Error = anyhow::Error;
fn try_from(s: String) -> Result<Self, Self::Error> { Self::from_str(s.as_str()) }
}
impl Mixable for Pattern {
fn any_file(&self) -> bool { self.is_star && !self.is_dir }
fn any_dir(&self) -> bool { self.is_star && self.is_dir }
}
// --- Scheme
#[derive(Clone, Copy, Debug)]
enum PatternScheme {

View file

@ -1,26 +1,43 @@
use std::ops::Deref;
use serde::Deserialize;
use yazi_fs::File;
use yazi_shared::event::Action;
use yazi_shared::{Id, event::Action};
use crate::{Pattern, Priority};
use crate::{Mixable, Pattern, Priority, plugin::fetcher_id};
#[derive(Debug, Deserialize)]
pub struct Fetcher {
#[serde(skip, default = "fetcher_id")]
pub id: Id,
#[serde(skip)]
pub idx: u8,
pub id: String,
pub url: Option<Pattern>,
pub mime: Option<Pattern>,
pub run: Action,
pub idx: u8,
pub url: Option<Pattern>,
pub mime: Option<Pattern>,
pub run: Action,
#[serde(default)]
pub prio: Priority,
pub prio: Priority,
pub group: String,
}
impl Deref for Fetcher {
type Target = Action;
fn deref(&self) -> &Self::Target { &self.run }
}
impl Fetcher {
#[inline]
pub fn matches(&self, file: &File, mime: &str) -> bool {
self.mime.as_ref().is_some_and(|p| p.match_mime(mime))
|| self.url.as_ref().is_some_and(|p| p.match_url(&file.url, file.is_dir()))
pub fn matches(&self, file: &File, mime: &str) -> bool { self.match_with(Some(file), Some(mime)) }
pub fn match_with(&self, file: Option<&File>, mime: Option<&str>) -> bool {
match (file, mime, &self.url, &self.mime) {
(Some(f), _, Some(p), _) => p.match_url(&f.url, f.is_dir()),
(_, Some(m), _, Some(p)) => p.match_mime(m),
(None, None, ..) => true,
_ => false,
}
}
}
impl Mixable for Fetcher {}

View file

@ -0,0 +1,104 @@
use std::{borrow::Cow, ops::Deref, sync::Arc};
use arc_swap::ArcSwap;
use hashbrown::HashSet;
use serde::Deserialize;
use tracing::warn;
use yazi_fs::File;
use yazi_shared::Id;
use yazi_shim::arc_swap::IntoPointee;
use super::{Fetcher, MAX_FETCHERS};
#[derive(Debug, Default, Deserialize)]
pub struct Fetchers(ArcSwap<Vec<Arc<Fetcher>>>);
impl Deref for Fetchers {
type Target = ArcSwap<Vec<Arc<Fetcher>>>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<Vec<Arc<Fetcher>>> for Fetchers {
fn from(inner: Vec<Arc<Fetcher>>) -> Self { Self(inner.into_pointee()) }
}
impl From<Arc<Vec<Arc<Fetcher>>>> for Fetchers {
fn from(inner: Arc<Vec<Arc<Fetcher>>>) -> Self { Self(inner.into()) }
}
impl Fetchers {
pub fn matches<'a>(&self, file: &'a File, mime: &'a str) -> FetcherMatcher<'a> {
self.matcher(Some(file), Some(mime))
}
pub fn matcher<'a, F, M>(&self, file: Option<F>, mime: Option<M>) -> FetcherMatcher<'a>
where
F: Into<Cow<'a, File>>,
M: Into<Cow<'a, str>>,
{
FetcherMatcher {
fetchers: self.load_full(),
id: Id::ZERO,
file: file.map(Into::into),
mime: mime.map(Into::into),
offset: 0,
seen: HashSet::new(),
}
}
pub fn mime(&self, files: Vec<File>) -> impl Iterator<Item = (Arc<Fetcher>, Vec<File>)> {
let mut tasks: [Vec<_>; MAX_FETCHERS as usize] = Default::default();
for file in files {
let found = self.matches(&file, "").find(|f| f.group == "mime");
if let Some(fetcher) = found {
tasks[fetcher.idx as usize].push(file);
} else {
warn!("No mime fetcher for {file:?}");
}
}
let fetchers = self.load();
tasks.into_iter().enumerate().filter_map(move |(i, tasks)| {
if tasks.is_empty() { None } else { Some((fetchers[i].clone(), tasks)) }
})
}
pub(crate) fn unwrap_unchecked(self) -> Vec<Arc<Fetcher>> {
Arc::try_unwrap(self.0.into_inner()).expect("unique fetchers arc")
}
}
// --- Matcher
pub struct FetcherMatcher<'a> {
pub fetchers: Arc<Vec<Arc<Fetcher>>>,
pub id: Id,
pub file: Option<Cow<'a, File>>,
pub mime: Option<Cow<'a, str>>,
pub offset: usize,
pub seen: HashSet<String>,
}
impl FetcherMatcher<'_> {
pub fn matches(&self, fetcher: &Fetcher) -> bool {
fetcher.id == self.id || fetcher.match_with(self.file.as_deref(), self.mime.as_deref())
}
}
impl Iterator for FetcherMatcher<'_> {
type Item = Arc<Fetcher>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(fetcher) = self.fetchers.get(self.offset) {
self.offset += 1;
if !self.matches(fetcher) {
continue;
}
if self.seen.insert(fetcher.group.clone()) {
return Some(fetcher.clone());
}
}
None
}
}

View file

@ -0,0 +1,21 @@
use yazi_shared::{Id, Ids};
pub fn fetcher_id() -> Id {
static IDS: Ids = Ids::new();
IDS.next()
}
pub fn preloader_id() -> Id {
static IDS: Ids = Ids::new();
IDS.next()
}
pub fn previewer_id() -> Id {
static IDS: Ids = Ids::new();
IDS.next()
}
pub fn spotter_id() -> Id {
static IDS: Ids = Ids::new();
IDS.next()
}

View file

@ -1,4 +1,4 @@
yazi_macro::mod_flat!(fetcher plugin preloader previewer spotter);
yazi_macro::mod_flat!(fetcher fetchers ids plugin preloader preloaders previewer previewers spotter spotters);
pub const MAX_FETCHERS: u8 = 16;
pub const MAX_PRELOADERS: u8 = 16;

View file

@ -1,139 +1,69 @@
use anyhow::{Result, bail};
use hashbrown::HashSet;
use serde::Deserialize;
use tracing::warn;
use yazi_codegen::DeserializeOver2;
use yazi_fs::File;
use std::sync::Arc;
use super::{Fetcher, Preloader, Previewer, Spotter};
use crate::{Preset, plugin::{MAX_FETCHERS, MAX_PRELOADERS}};
use anyhow::Result;
use serde::{Deserialize, de};
use yazi_codegen::DeserializeOver2;
use yazi_shim::toml::DeserializeOverHook;
use super::{Fetcher, Fetchers, Preloader, Preloaders, Previewer, Previewers, Spotter, Spotters};
use crate::{mix, plugin::{MAX_FETCHERS, MAX_PRELOADERS}};
#[derive(Default, Deserialize, DeserializeOver2)]
pub struct Plugin {
pub fetchers: Vec<Fetcher>,
pub fetchers: Fetchers,
#[serde(default)]
prepend_fetchers: Vec<Fetcher>,
#[serde(default)]
append_fetchers: Vec<Fetcher>,
pub spotters: Vec<Spotter>,
pub spotters: Spotters,
#[serde(default)]
prepend_spotters: Vec<Spotter>,
#[serde(default)]
append_spotters: Vec<Spotter>,
pub preloaders: Vec<Preloader>,
pub preloaders: Preloaders,
#[serde(default)]
prepend_preloaders: Vec<Preloader>,
#[serde(default)]
append_preloaders: Vec<Preloader>,
pub previewers: Vec<Previewer>,
pub previewers: Previewers,
#[serde(default)]
prepend_previewers: Vec<Previewer>,
#[serde(default)]
append_previewers: Vec<Previewer>,
}
impl Plugin {
pub fn fetchers<'a, 'b: 'a>(
&'b self,
file: &'a File,
mime: &'a str,
) -> impl Iterator<Item = &'b Fetcher> + 'a {
let mut seen = HashSet::new();
self.fetchers.iter().filter(move |&f| {
if seen.contains(&f.id) || !f.matches(file, mime) {
return false;
}
seen.insert(&f.id);
true
})
}
impl DeserializeOverHook for Plugin {
fn deserialize_over_hook(self) -> Result<Self, toml::de::Error> {
let mut fetchers: Vec<Arc<Fetcher>> =
mix(self.prepend_fetchers, self.fetchers.unwrap_unchecked(), self.append_fetchers);
let spotters: Vec<Arc<Spotter>> =
mix(self.prepend_spotters, self.spotters.unwrap_unchecked(), self.append_spotters);
let mut preloaders: Vec<Arc<Preloader>> =
mix(self.prepend_preloaders, self.preloaders.unwrap_unchecked(), self.append_preloaders);
let previewers: Vec<Arc<Previewer>> =
mix(self.prepend_previewers, self.previewers.unwrap_unchecked(), self.append_previewers);
pub fn mime_fetchers(&self, files: Vec<File>) -> impl Iterator<Item = (&Fetcher, Vec<File>)> {
let mut tasks: [Vec<_>; MAX_FETCHERS as usize] = Default::default();
for f in files {
let found = self.fetchers.iter().find(|&g| g.id == "mime" && g.matches(&f, ""));
if let Some(g) = found {
tasks[g.idx as usize].push(f);
} else {
warn!("No mime fetcher for {f:?}");
}
if fetchers.len() > MAX_FETCHERS as usize {
Err(de::Error::custom(format!("Fetchers exceed the limit of {MAX_FETCHERS}")))?;
} else if preloaders.len() > MAX_PRELOADERS as usize {
Err(de::Error::custom(format!("Preloaders exceed the limit of {MAX_FETCHERS}")))?;
}
tasks.into_iter().enumerate().filter_map(|(i, tasks)| {
if tasks.is_empty() { None } else { Some((&self.fetchers[i], tasks)) }
})
}
pub fn spotter(&self, file: &File, mime: &str) -> Option<&Spotter> {
self.spotters.iter().find(|&p| p.matches(file, mime))
}
pub fn preloaders<'a, 'b: 'a>(
&'b self,
file: &'a File,
mime: &'a str,
) -> impl Iterator<Item = &'b Preloader> + 'a {
let mut next = true;
self.preloaders.iter().filter(move |&p| {
if !next || !p.matches(file, mime) {
return false;
}
next = p.next;
true
})
}
pub fn previewer(&self, file: &File, mime: &str) -> Option<&Previewer> {
self.previewers.iter().find(|&p| p.matches(file, mime))
}
}
impl Plugin {
// TODO: remove .retain() and .collect()
pub(crate) fn reshape(mut self) -> Result<Self> {
if self.append_spotters.iter().any(|r| r.any_file()) {
self.spotters.retain(|r| !r.any_file());
for (i, p) in fetchers.iter_mut().enumerate() {
Arc::get_mut(p).ok_or_else(|| de::Error::custom("non-unique fetcher arc"))?.idx = i as u8;
}
if self.append_spotters.iter().any(|r| r.any_dir()) {
self.spotters.retain(|r| !r.any_dir());
}
if self.append_previewers.iter().any(|r| r.any_file()) {
self.previewers.retain(|r| !r.any_file());
}
if self.append_previewers.iter().any(|r| r.any_dir()) {
self.previewers.retain(|r| !r.any_dir());
}
self.fetchers =
Preset::mix(self.prepend_fetchers, self.fetchers, self.append_fetchers).collect();
self.spotters =
Preset::mix(self.prepend_spotters, self.spotters, self.append_spotters).collect();
self.preloaders =
Preset::mix(self.prepend_preloaders, self.preloaders, self.append_preloaders).collect();
self.previewers =
Preset::mix(self.prepend_previewers, self.previewers, self.append_previewers).collect();
if self.fetchers.len() > MAX_FETCHERS as usize {
bail!("Fetchers exceed the limit of {MAX_FETCHERS}");
} else if self.preloaders.len() > MAX_PRELOADERS as usize {
bail!("Preloaders exceed the limit of {MAX_PRELOADERS}");
}
for (i, p) in self.fetchers.iter_mut().enumerate() {
p.idx = i as u8;
}
for (i, p) in self.preloaders.iter_mut().enumerate() {
p.idx = i as u8;
for (i, p) in preloaders.iter_mut().enumerate() {
Arc::get_mut(p).ok_or_else(|| de::Error::custom("non-unique preloader arc"))?.idx = i as u8;
}
Ok(Self {
fetchers: self.fetchers,
spotters: self.spotters,
preloaders: self.preloaders,
previewers: self.previewers,
fetchers: fetchers.into(),
spotters: spotters.into(),
preloaders: preloaders.into(),
previewers: previewers.into(),
..Default::default()
})
}

View file

@ -1,14 +1,17 @@
use std::ops::Deref;
use serde::Deserialize;
use yazi_fs::File;
use yazi_shared::event::Action;
use yazi_shared::{Id, event::Action};
use crate::{Pattern, Priority};
use crate::{Mixable, Pattern, Priority, plugin::preloader_id};
#[derive(Debug, Deserialize)]
pub struct Preloader {
#[serde(skip, default = "preloader_id")]
pub id: Id,
#[serde(skip)]
pub idx: u8,
pub idx: u8,
pub url: Option<Pattern>,
pub mime: Option<Pattern>,
pub run: Action,
@ -18,10 +21,24 @@ pub struct Preloader {
pub prio: Priority,
}
impl Deref for Preloader {
type Target = Action;
fn deref(&self) -> &Self::Target { &self.run }
}
impl Preloader {
#[inline]
pub fn matches(&self, file: &File, mime: &str) -> bool {
self.mime.as_ref().is_some_and(|p| p.match_mime(mime))
|| self.url.as_ref().is_some_and(|p| p.match_url(&file.url, file.is_dir()))
pub fn matches(&self, file: &File, mime: &str) -> bool { self.match_with(Some(file), Some(mime)) }
pub fn match_with(&self, file: Option<&File>, mime: Option<&str>) -> bool {
match (file, mime, &self.url, &self.mime) {
(Some(f), _, Some(p), _) => p.match_url(&f.url, f.is_dir()),
(_, Some(m), _, Some(p)) => p.match_mime(m),
(None, None, ..) => true,
_ => false,
}
}
}
impl Mixable for Preloader {}

View file

@ -0,0 +1,86 @@
use std::{borrow::Cow, ops::Deref, sync::Arc};
use arc_swap::ArcSwap;
use serde::Deserialize;
use yazi_fs::File;
use yazi_shared::Id;
use yazi_shim::arc_swap::IntoPointee;
use super::Preloader;
#[derive(Debug, Default, Deserialize)]
pub struct Preloaders(ArcSwap<Vec<Arc<Preloader>>>);
impl Deref for Preloaders {
type Target = ArcSwap<Vec<Arc<Preloader>>>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<Vec<Arc<Preloader>>> for Preloaders {
fn from(inner: Vec<Arc<Preloader>>) -> Self { Self(inner.into_pointee()) }
}
impl From<Arc<Vec<Arc<Preloader>>>> for Preloaders {
fn from(inner: Arc<Vec<Arc<Preloader>>>) -> Self { Self(inner.into()) }
}
impl Preloaders {
pub fn matches<'a>(&self, file: &'a File, mime: &'a str) -> PreloaderMatcher<'a> {
self.matcher(Some(file), Some(mime))
}
pub fn matcher<'a, F, M>(&self, file: Option<F>, mime: Option<M>) -> PreloaderMatcher<'a>
where
F: Into<Cow<'a, File>>,
M: Into<Cow<'a, str>>,
{
PreloaderMatcher {
preloaders: self.load_full(),
id: Id::ZERO,
file: file.map(Into::into),
mime: mime.map(Into::into),
offset: 0,
next: true,
}
}
pub(crate) fn unwrap_unchecked(self) -> Vec<Arc<Preloader>> {
Arc::try_unwrap(self.0.into_inner()).expect("unique preloaders arc")
}
}
// --- Matcher
pub struct PreloaderMatcher<'a> {
pub preloaders: Arc<Vec<Arc<Preloader>>>,
pub id: Id,
pub file: Option<Cow<'a, File>>,
pub mime: Option<Cow<'a, str>>,
pub offset: usize,
pub next: bool,
}
impl PreloaderMatcher<'_> {
pub fn matches(&self, preloader: &Preloader) -> bool {
preloader.id == self.id || preloader.match_with(self.file.as_deref(), self.mime.as_deref())
}
}
impl Iterator for PreloaderMatcher<'_> {
type Item = Arc<Preloader>;
fn next(&mut self) -> Option<Self::Item> {
if !self.next {
return None;
}
while let Some(preloader) = self.preloaders.get(self.offset) {
self.offset += 1;
if self.matches(preloader) {
self.next = preloader.next;
return Some(preloader.clone());
}
}
None
}
}

View file

@ -1,26 +1,43 @@
use std::ops::Deref;
use serde::Deserialize;
use yazi_fs::File;
use yazi_shared::event::Action;
use yazi_shared::{Id, event::Action};
use crate::Pattern;
use crate::{Mixable, Pattern, plugin::previewer_id};
#[derive(Debug, Deserialize)]
pub struct Previewer {
#[serde(skip, default = "previewer_id")]
pub id: Id,
pub url: Option<Pattern>,
pub mime: Option<Pattern>,
pub run: Action,
}
impl Deref for Previewer {
type Target = Action;
#[inline]
fn deref(&self) -> &Self::Target { &self.run }
}
impl Previewer {
#[inline]
pub fn matches(&self, file: &File, mime: &str) -> bool {
self.mime.as_ref().is_some_and(|p| p.match_mime(mime))
|| self.url.as_ref().is_some_and(|p| p.match_url(&file.url, file.is_dir()))
pub fn matches(&self, file: &File, mime: &str) -> bool { self.match_with(Some(file), Some(mime)) }
pub fn match_with(&self, file: Option<&File>, mime: Option<&str>) -> bool {
match (file, mime, &self.url, &self.mime) {
(Some(f), _, Some(p), _) => p.match_url(&f.url, f.is_dir()),
(_, Some(m), _, Some(p)) => p.match_mime(m),
(None, None, ..) => true,
_ => false,
}
}
#[inline]
pub fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) }
#[inline]
pub fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) }
}
impl Mixable for Previewer {
fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) }
fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) }
}

View file

@ -0,0 +1,108 @@
use std::{borrow::Cow, ops::Deref, sync::Arc};
use arc_swap::ArcSwap;
use serde::Deserialize;
use yazi_fs::File;
use yazi_shared::Id;
use yazi_shim::{arc_swap::IntoPointee, vec::{IndexAtError, VecExt}};
use super::Previewer;
use crate::mix;
#[derive(Debug, Default, Deserialize)]
pub struct Previewers(ArcSwap<Vec<Arc<Previewer>>>);
impl Deref for Previewers {
type Target = ArcSwap<Vec<Arc<Previewer>>>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<Vec<Arc<Previewer>>> for Previewers {
fn from(inner: Vec<Arc<Previewer>>) -> Self { Self(inner.into_pointee()) }
}
impl Previewers {
pub fn matches(&self, file: &File, mime: &str) -> Option<Arc<Previewer>> {
self.matcher(Some(file), Some(mime)).next()
}
pub fn matcher<'a, F, M>(&self, file: Option<F>, mime: Option<M>) -> PreviewerMatcher<'a>
where
F: Into<Cow<'a, File>>,
M: Into<Cow<'a, str>>,
{
PreviewerMatcher {
previewers: self.0.load_full(),
id: Id::ZERO,
file: file.map(Into::into),
mime: mime.map(Into::into),
offset: 0,
}
}
pub fn insert(&self, index: isize, previewer: Arc<Previewer>) -> Result<(), IndexAtError> {
let mut err = None;
self.0.rcu(|previewers| match previewers.index_at(index) {
Ok(n) if n == previewers.len() => {
mix(Vec::<Previewer>::new(), previewers.iter().cloned(), [previewer.clone()])
}
Ok(n) => {
let (before, after) = previewers.split_at(n);
mix(
Vec::<Previewer>::new(),
before.iter().cloned().chain([previewer.clone()]).chain(after.iter().cloned()),
Vec::<Previewer>::new(),
)
}
Err(e) => {
err = Some(e);
Vec::clone(previewers)
}
});
err.map_or(Ok(()), Err)
}
pub fn remove(&self, matcher: PreviewerMatcher) {
self.0.rcu(|previewers| {
let mut next = Vec::clone(previewers);
next.retain(|previewer| !matcher.matches(previewer));
next
});
}
pub(crate) fn unwrap_unchecked(self) -> Vec<Arc<Previewer>> {
Arc::try_unwrap(self.0.into_inner()).expect("unique previewers arc")
}
}
// --- Matcher
pub struct PreviewerMatcher<'a> {
pub previewers: Arc<Vec<Arc<Previewer>>>,
pub id: Id,
pub file: Option<Cow<'a, File>>,
pub mime: Option<Cow<'a, str>>,
pub offset: usize,
}
impl PreviewerMatcher<'_> {
pub fn matches(&self, previewer: &Previewer) -> bool {
previewer.id == self.id || previewer.match_with(self.file.as_deref(), self.mime.as_deref())
}
}
impl Iterator for PreviewerMatcher<'_> {
type Item = Arc<Previewer>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(previewer) = self.previewers.get(self.offset) {
self.offset += 1;
if self.matches(previewer) {
return Some(previewer.clone());
}
}
None
}
}

View file

@ -1,26 +1,43 @@
use std::ops::Deref;
use serde::Deserialize;
use yazi_fs::File;
use yazi_shared::event::Action;
use yazi_shared::{Id, event::Action};
use crate::Pattern;
use crate::{Mixable, Pattern, plugin::spotter_id};
#[derive(Debug, Deserialize)]
pub struct Spotter {
#[serde(skip, default = "spotter_id")]
pub id: Id,
pub url: Option<Pattern>,
pub mime: Option<Pattern>,
pub run: Action,
}
impl Deref for Spotter {
type Target = Action;
#[inline]
fn deref(&self) -> &Self::Target { &self.run }
}
impl Spotter {
#[inline]
pub fn matches(&self, file: &File, mime: &str) -> bool {
self.mime.as_ref().is_some_and(|p| p.match_mime(mime))
|| self.url.as_ref().is_some_and(|p| p.match_url(&file.url, file.is_dir()))
pub fn matches(&self, file: &File, mime: &str) -> bool { self.match_with(Some(file), Some(mime)) }
pub fn match_with(&self, file: Option<&File>, mime: Option<&str>) -> bool {
match (file, mime, &self.url, &self.mime) {
(Some(f), _, Some(p), _) => p.match_url(&f.url, f.is_dir()),
(_, Some(m), _, Some(p)) => p.match_mime(m),
(None, None, ..) => true,
_ => false,
}
}
#[inline]
pub fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) }
#[inline]
pub fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) }
}
impl Mixable for Spotter {
fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) }
fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) }
}

View file

@ -0,0 +1,75 @@
use std::{borrow::Cow, ops::Deref, sync::Arc};
use arc_swap::ArcSwap;
use serde::Deserialize;
use yazi_fs::File;
use yazi_shared::Id;
use yazi_shim::arc_swap::IntoPointee;
use super::Spotter;
#[derive(Debug, Default, Deserialize)]
pub struct Spotters(ArcSwap<Vec<Arc<Spotter>>>);
impl Deref for Spotters {
type Target = ArcSwap<Vec<Arc<Spotter>>>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<Vec<Arc<Spotter>>> for Spotters {
fn from(inner: Vec<Arc<Spotter>>) -> Self { Self(inner.into_pointee()) }
}
impl Spotters {
pub fn matches(&self, file: &File, mime: &str) -> Option<Arc<Spotter>> {
self.matcher(Some(file), Some(mime)).next()
}
pub fn matcher<'a, F, M>(&self, file: Option<F>, mime: Option<M>) -> SpotterMatcher<'a>
where
F: Into<Cow<'a, File>>,
M: Into<Cow<'a, str>>,
{
SpotterMatcher {
spotters: self.load_full(),
id: Id::ZERO,
file: file.map(Into::into),
mime: mime.map(Into::into),
offset: 0,
}
}
pub(crate) fn unwrap_unchecked(self) -> Vec<Arc<Spotter>> {
Arc::try_unwrap(self.0.into_inner()).expect("unique spotters arc")
}
}
// --- Matcher
pub struct SpotterMatcher<'a> {
pub spotters: Arc<Vec<Arc<Spotter>>>,
pub id: Id,
pub file: Option<Cow<'a, File>>,
pub mime: Option<Cow<'a, str>>,
pub offset: usize,
}
impl SpotterMatcher<'_> {
pub fn matches(&self, spotter: &Spotter) -> bool {
spotter.id == self.id || spotter.match_with(self.file.as_deref(), self.mime.as_deref())
}
}
impl Iterator for SpotterMatcher<'_> {
type Item = Arc<Spotter>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(spotter) = self.spotters.get(self.offset) {
self.offset += 1;
if self.matches(spotter) {
return Some(spotter.clone());
}
}
None
}
}

View file

@ -1,10 +1,10 @@
use serde::Deserialize;
use yazi_codegen::DeserializeOver2;
use yazi_codegen::{DeserializeOver, DeserializeOver2};
use super::{Offset, Origin};
use crate::popup::Position;
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Confirm {
// trash
pub trash_title: String,

View file

@ -1,9 +1,9 @@
use serde::Deserialize;
use yazi_codegen::DeserializeOver2;
use yazi_codegen::{DeserializeOver, DeserializeOver2};
use super::{Offset, Origin};
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Input {
pub cursor_blink: bool,

View file

@ -1,9 +1,9 @@
use serde::Deserialize;
use yazi_codegen::DeserializeOver2;
use yazi_codegen::{DeserializeOver, DeserializeOver2};
use super::{Offset, Origin};
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Pick {
// open
pub open_title: String,

View file

@ -22,14 +22,4 @@ impl Preset {
pub(super) fn vfs() -> Result<Vfs, toml::de::Error> {
toml::from_str(&yazi_macro::config_preset!("vfs"))
}
#[inline]
pub(crate) fn mix<E, A, B, C>(a: A, b: B, c: C) -> impl Iterator<Item = E>
where
A: IntoIterator<Item = E>,
B: IntoIterator<Item = E>,
C: IntoIterator<Item = E>,
{
a.into_iter().chain(b).chain(c)
}
}

View file

@ -1,10 +1,11 @@
use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use anyhow::{Context, Result};
use serde::{Deserialize, Deserializer, Serialize};
use yazi_codegen::DeserializeOver2;
use yazi_fs::Xdg;
use yazi_shared::{SStr, timestamp_us};
use yazi_shim::toml::DeserializeOverHook;
use super::PreviewWrap;
use crate::normalize_path;
@ -16,10 +17,13 @@ pub struct Preview {
pub max_width: u16,
pub max_height: u16,
#[serde(deserialize_with = "deserialize_cache_dir")]
pub cache_dir: PathBuf,
#[serde(deserialize_with = "deserialize_image_delay")]
pub image_delay: u8,
pub image_filter: String,
#[serde(deserialize_with = "deserialize_image_quality")]
pub image_quality: u8,
pub ueberzug_scale: f32,
@ -43,25 +47,50 @@ impl Preview {
}
}
impl Preview {
pub(crate) fn reshape(mut self) -> Result<Self> {
if self.image_delay > 100 {
bail!("[preview].image_delay must be between 0 and 100.");
} else if self.image_quality < 50 || self.image_quality > 90 {
bail!("[preview].image_quality must be between 50 and 90.");
}
self.cache_dir = if self.cache_dir.as_os_str().is_empty() {
Xdg::cache_dir().to_owned()
} else if let Some(p) = normalize_path(self.cache_dir) {
p
} else {
bail!("[preview].cache_dir must be either empty or an absolute path.");
};
impl DeserializeOverHook for Preview {
fn deserialize_over_hook(self) -> Result<Self, toml::de::Error> {
std::fs::create_dir_all(&self.cache_dir)
.context(format!("Failed to create cache directory: {}", self.cache_dir.display()))?;
.context(format!("Failed to create cache directory: {}", self.cache_dir.display()))
.map_err(serde::de::Error::custom)?;
Ok(self)
}
}
fn deserialize_cache_dir<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
where
D: Deserializer<'de>,
{
let path = PathBuf::deserialize(deserializer)?;
if path.as_os_str().is_empty() {
Ok(Xdg::cache_dir().to_owned())
} else {
normalize_path(path).ok_or_else(|| {
serde::de::Error::custom("cache_dir must be either empty or an absolute path.")
})
}
}
fn deserialize_image_delay<'de, D>(deserializer: D) -> Result<u8, D::Error>
where
D: Deserializer<'de>,
{
let value = u8::deserialize(deserializer)?;
if value <= 100 {
Ok(value)
} else {
Err(serde::de::Error::custom("image_delay must be between 0 and 100."))
}
}
fn deserialize_image_quality<'de, D>(deserializer: D) -> Result<u8, D::Error>
where
D: Deserializer<'de>,
{
let value = u8::deserialize(deserializer)?;
if (50..=90).contains(&value) {
Ok(value)
} else {
Err(serde::de::Error::custom("image_quality must be between 50 and 90."))
}
}

View file

@ -1,38 +1,20 @@
use anyhow::{Result, bail};
use std::num::NonZeroU8;
use serde::Deserialize;
use yazi_codegen::DeserializeOver2;
use yazi_codegen::{DeserializeOver, DeserializeOver2};
#[derive(Debug, Deserialize, DeserializeOver2)]
#[derive(Debug, Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Tasks {
pub file_workers: u8,
pub plugin_workers: u8,
pub fetch_workers: u8,
pub preload_workers: u8,
pub process_workers: u8,
pub file_workers: NonZeroU8,
pub plugin_workers: NonZeroU8,
pub fetch_workers: NonZeroU8,
pub preload_workers: NonZeroU8,
pub process_workers: NonZeroU8,
pub bizarre_retry: u8,
pub bizarre_retry: NonZeroU8,
pub image_alloc: u32,
pub image_bound: [u16; 2],
pub suppress_preload: bool,
}
impl Tasks {
pub(crate) fn reshape(self) -> Result<Self> {
if self.file_workers < 1 {
bail!("[tasks].file_workers must be at least 1.");
} else if self.plugin_workers < 1 {
bail!("[tasks].plugin_workers must be at least 1.");
} else if self.fetch_workers < 1 {
bail!("[tasks].fetch_workers must be at least 1.");
} else if self.preload_workers < 1 {
bail!("[tasks].preload_workers must be at least 1.");
} else if self.process_workers < 1 {
bail!("[tasks].process_workers must be at least 1.");
} else if self.bizarre_retry < 1 {
bail!("[tasks].bizarre_retry must be at least 1.");
}
Ok(self)
}
}

View file

@ -1,13 +1,13 @@
use std::ops::Deref;
use serde::Deserialize;
use yazi_codegen::DeserializeOver2;
use yazi_codegen::{DeserializeOver, DeserializeOver2};
use yazi_fs::File;
use super::Is;
use crate::{Pattern, Style};
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Filetype {
rules: Vec<FiletypeRule>,
}

View file

@ -1,32 +1,34 @@
use std::path::PathBuf;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize, de::IntoDeserializer};
use arc_swap::ArcSwap;
use serde::{Deserialize, Serialize};
use toml::{Spanned, de::DeTable};
use yazi_codegen::DeserializeOver2;
use yazi_codegen::{DeserializeOver, DeserializeOver2};
use yazi_fs::Xdg;
use yazi_shim::toml::deserialize_spanned;
use crate::error_with_input;
#[derive(Default, Deserialize, DeserializeOver2, Serialize)]
#[derive(Default, Deserialize, DeserializeOver, DeserializeOver2, Serialize)]
pub struct Flavor {
#[serde(default)]
pub dark: String,
pub dark: ArcSwap<String>,
#[serde(default)]
pub light: String,
pub light: ArcSwap<String>,
}
impl Flavor {
pub(crate) fn from_theme(theme: &Spanned<DeTable>, input: &str) -> Result<Self, toml::de::Error> {
if let Some(value) = theme.get_ref().get("flavor").cloned() {
error_with_input(Self::deserialize(value.into_deserializer()), input)
error_with_input(deserialize_spanned(value), input)
} else {
Ok(Self::default())
}
}
pub(crate) fn read(&self, light: bool) -> Result<String> {
Ok(match if light { self.light.as_str() } else { self.dark.as_str() } {
Ok(match if light { self.light.load() } else { self.dark.load() }.as_str() {
"" => String::new(),
name => {
let p = Xdg::config_dir().join(format!("flavors/{name}.yazi/flavor.toml"));
@ -36,7 +38,7 @@ impl Flavor {
}
pub(crate) fn syntect_path(&self, light: bool) -> Option<PathBuf> {
match if light { self.light.as_str() } else { self.dark.as_str() } {
match if light { self.light.load() } else { self.dark.load() }.as_str() {
"" => None,
name => Some(Xdg::config_dir().join(format!("flavors/{name}.yazi/tmtheme.xml"))),
}

View file

@ -1,46 +1,47 @@
use std::ops::Deref;
use anyhow::Result;
use hashbrown::HashMap;
use ratatui::style::Color;
use serde::{Deserialize, Deserializer};
use yazi_codegen::DeserializeOver2;
use yazi_fs::File;
use yazi_shared::{Condition, url::UrlLike};
use yazi_shim::toml::DeserializeOverHook;
use crate::{Icon as I, Pattern, Style};
use crate::{Icon as I, Mixable, Pattern, mix};
#[derive(Default, Deserialize, DeserializeOver2)]
pub struct Icon {
globs: PatIcons,
globs: Vec<IconPat>,
#[serde(default)]
prepend_globs: PatIcons,
prepend_globs: Vec<IconPat>,
#[serde(default)]
append_globs: PatIcons,
append_globs: Vec<IconPat>,
dirs: StrIcons,
#[serde(default)]
prepend_dirs: StrIcons,
#[serde(default)]
append_dirs: StrIcons,
#[serde(deserialize_with = "deserialize_named_icons")]
dirs: HashMap<String, I>,
#[serde(default, deserialize_with = "deserialize_named_icons")]
prepend_dirs: HashMap<String, I>,
#[serde(default, deserialize_with = "deserialize_named_icons")]
append_dirs: HashMap<String, I>,
files: StrIcons,
#[serde(default)]
prepend_files: StrIcons,
#[serde(default)]
append_files: StrIcons,
#[serde(deserialize_with = "deserialize_named_icons")]
files: HashMap<String, I>,
#[serde(default, deserialize_with = "deserialize_named_icons")]
prepend_files: HashMap<String, I>,
#[serde(default, deserialize_with = "deserialize_named_icons")]
append_files: HashMap<String, I>,
exts: StrIcons,
#[serde(default)]
prepend_exts: StrIcons,
#[serde(default)]
append_exts: StrIcons,
#[serde(deserialize_with = "deserialize_named_icons")]
exts: HashMap<String, I>,
#[serde(default, deserialize_with = "deserialize_named_icons")]
prepend_exts: HashMap<String, I>,
#[serde(default, deserialize_with = "deserialize_named_icons")]
append_exts: HashMap<String, I>,
conds: CondIcons,
conds: Vec<IconCond>,
#[serde(default)]
prepend_conds: CondIcons,
prepend_conds: Vec<IconCond>,
#[serde(default)]
append_conds: CondIcons,
append_conds: Vec<IconCond>,
}
impl Icon {
@ -68,11 +69,11 @@ impl Icon {
"hovered" => hovered,
_ => false,
};
self.conds.iter().find(|(c, _)| c.eval(f) == Some(true)).map(|(_, i)| i)
self.conds.iter().find(|&c| c.r#if.eval(f) == Some(true)).map(|c| &c.icon)
}
fn match_by_glob(&self, file: &File) -> Option<&I> {
self.globs.iter().find(|(p, _)| p.match_url(&file.url, file.is_dir())).map(|(_, i)| i)
self.globs.iter().find(|&g| g.url.match_url(&file.url, file.is_dir())).map(|g| &g.icon)
}
fn match_by_name(&self, file: &File) -> Option<&I> {
@ -94,115 +95,59 @@ impl Icon {
}
}
impl Icon {
pub(crate) fn reshape(self) -> Result<Self> {
impl DeserializeOverHook for Icon {
fn deserialize_over_hook(self) -> Result<Self, toml::de::Error> {
Ok(Self {
globs: PatIcons(
self.prepend_globs.0.into_iter().chain(self.globs.0).chain(self.append_globs.0).collect(),
),
dirs: StrIcons(
self.append_dirs.0.into_iter().chain(self.dirs.0).chain(self.prepend_dirs.0).collect(),
),
files: StrIcons(
self.append_files.0.into_iter().chain(self.files.0).chain(self.prepend_files.0).collect(),
),
exts: StrIcons(
self.append_exts.0.into_iter().chain(self.exts.0).chain(self.prepend_exts.0).collect(),
),
conds: CondIcons(
self.prepend_conds.0.into_iter().chain(self.conds.0).chain(self.append_conds.0).collect(),
),
globs: mix(self.prepend_globs, self.globs, self.append_globs),
dirs: self.append_dirs.into_iter().chain(self.dirs).chain(self.prepend_dirs).collect(),
files: self.append_files.into_iter().chain(self.files).chain(self.prepend_files).collect(),
exts: self.append_exts.into_iter().chain(self.exts).chain(self.prepend_exts).collect(),
conds: mix(self.prepend_conds, self.conds, self.append_conds),
..Default::default()
})
}
}
#[derive(Default)]
pub struct PatIcons(Vec<(Pattern, I)>);
impl Deref for PatIcons {
type Target = Vec<(Pattern, I)>;
fn deref(&self) -> &Self::Target { &self.0 }
// --- IconPat
#[derive(Deserialize)]
pub struct IconPat {
pub url: Pattern,
#[serde(flatten)]
pub icon: I,
}
impl<'de> Deserialize<'de> for PatIcons {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Shadow {
url: Pattern,
text: String,
fg: Option<Color>,
}
impl Mixable for IconPat {
fn any_file(&self) -> bool { self.url.any_file() }
Ok(Self(
<Vec<Shadow>>::deserialize(deserializer)?
.into_iter()
.map(|s| (s.url, I { text: s.text, style: Style { fg: s.fg, ..Default::default() } }))
.collect(),
))
}
fn any_dir(&self) -> bool { self.url.any_dir() }
}
#[derive(Default)]
pub struct StrIcons(HashMap<String, I>);
impl Deref for StrIcons {
type Target = HashMap<String, I>;
fn deref(&self) -> &Self::Target { &self.0 }
// --- IconNamed
#[derive(Deserialize)]
struct IconNamed {
name: String,
#[serde(flatten)]
icon: I,
}
impl<'de> Deserialize<'de> for StrIcons {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Shadow {
name: String,
text: String,
fg: Option<Color>,
}
Ok(Self(
<Vec<Shadow>>::deserialize(deserializer)?
.into_iter()
.map(|s| (s.name, I { text: s.text, style: Style { fg: s.fg, ..Default::default() } }))
.collect(),
))
}
fn deserialize_named_icons<'de, D>(deserializer: D) -> Result<HashMap<String, I>, D::Error>
where
D: Deserializer<'de>,
{
Ok(
Vec::<IconNamed>::deserialize(deserializer)?
.into_iter()
.map(|entry| (entry.name, entry.icon))
.collect(),
)
}
#[derive(Default)]
pub struct CondIcons(Vec<(Condition, I)>);
impl Deref for CondIcons {
type Target = Vec<(Condition, I)>;
fn deref(&self) -> &Self::Target { &self.0 }
// --- IconCond
#[derive(Deserialize)]
pub struct IconCond {
pub r#if: Condition,
#[serde(flatten)]
pub icon: I,
}
impl<'de> Deserialize<'de> for CondIcons {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Shadow {
r#if: Condition,
text: String,
fg: Option<Color>,
}
Ok(Self(
<Vec<Shadow>>::deserialize(deserializer)?
.into_iter()
.map(|s| (s.r#if, I { text: s.text, style: Style { fg: s.fg, ..Default::default() } }))
.collect(),
))
}
}
impl Mixable for IconCond {}

View file

@ -1,9 +1,12 @@
use std::path::PathBuf;
use anyhow::{Context, Result, anyhow, bail};
use serde::Deserialize;
use anyhow::{Context, Result};
use arc_swap::ArcSwap;
use serde::{Deserialize, Deserializer, de};
use yazi_codegen::{DeserializeOver, DeserializeOver1, DeserializeOver2};
use yazi_fs::{Xdg, ok_or_not_found};
use yazi_shared::SyncCell;
use yazi_shim::arc_swap::IntoPointee;
use super::{Filetype, Flavor, Icon};
use crate::{Style, normalize_path};
@ -28,199 +31,199 @@ pub struct Theme {
pub help: Help,
// File-specific styles
#[serde(skip_serializing)]
pub filetype: Filetype,
#[serde(skip_serializing)]
pub icon: Icon,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct App {
pub overall: Style,
pub overall: SyncCell<Style>,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Mgr {
pub cwd: Style,
pub cwd: SyncCell<Style>,
// Find
pub find_keyword: Style,
pub find_position: Style,
pub find_keyword: SyncCell<Style>,
pub find_position: SyncCell<Style>,
// Symlink
pub symlink_target: Style,
pub symlink_target: SyncCell<Style>,
// Marker
pub marker_copied: Style,
pub marker_cut: Style,
pub marker_marked: Style,
pub marker_selected: Style,
pub marker_symbol: String,
pub marker_copied: SyncCell<Style>,
pub marker_cut: SyncCell<Style>,
pub marker_marked: SyncCell<Style>,
pub marker_selected: SyncCell<Style>,
pub marker_symbol: ArcSwap<String>,
// Count
pub count_copied: Style,
pub count_cut: Style,
pub count_selected: Style,
pub count_copied: SyncCell<Style>,
pub count_cut: SyncCell<Style>,
pub count_selected: SyncCell<Style>,
// Border
pub border_symbol: String,
pub border_style: Style,
pub border_symbol: ArcSwap<String>,
pub border_style: SyncCell<Style>,
// Highlighting
pub syntect_theme: PathBuf,
#[serde(deserialize_with = "deserialize_syntect_theme")]
pub syntect_theme: ArcSwap<PathBuf>,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Tabs {
pub active: Style,
pub inactive: Style,
pub active: SyncCell<Style>,
pub inactive: SyncCell<Style>,
pub sep_inner: TabsSep,
pub sep_outer: TabsSep,
pub sep_inner: ArcSwap<TabsSep>,
pub sep_outer: ArcSwap<TabsSep>,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize)]
pub struct TabsSep {
pub open: String,
pub close: String,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Mode {
pub normal_main: Style,
pub normal_alt: Style,
pub normal_main: SyncCell<Style>,
pub normal_alt: SyncCell<Style>,
pub select_main: Style,
pub select_alt: Style,
pub select_main: SyncCell<Style>,
pub select_alt: SyncCell<Style>,
pub unset_main: Style,
pub unset_alt: Style,
pub unset_main: SyncCell<Style>,
pub unset_alt: SyncCell<Style>,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Indicator {
pub parent: Style,
pub current: Style,
pub preview: Style,
pub padding: IndicatorPadding,
pub parent: SyncCell<Style>,
pub current: SyncCell<Style>,
pub preview: SyncCell<Style>,
pub padding: ArcSwap<IndicatorPadding>,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize)]
pub struct IndicatorPadding {
pub open: String,
pub close: String,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Status {
pub overall: Style,
pub sep_left: StatusSep,
pub sep_right: StatusSep,
pub overall: SyncCell<Style>,
pub sep_left: ArcSwap<StatusSep>,
pub sep_right: ArcSwap<StatusSep>,
// Permissions
pub perm_sep: Style,
pub perm_type: Style,
pub perm_read: Style,
pub perm_write: Style,
pub perm_exec: Style,
pub perm_sep: SyncCell<Style>,
pub perm_type: SyncCell<Style>,
pub perm_read: SyncCell<Style>,
pub perm_write: SyncCell<Style>,
pub perm_exec: SyncCell<Style>,
// Progress
pub progress_label: Style,
pub progress_normal: Style,
pub progress_error: Style,
pub progress_label: SyncCell<Style>,
pub progress_normal: SyncCell<Style>,
pub progress_error: SyncCell<Style>,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize)]
pub struct StatusSep {
pub open: String,
pub close: String,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Which {
pub cols: u8,
pub mask: Style,
pub cand: Style,
pub rest: Style,
pub desc: Style,
#[serde(deserialize_with = "deserialize_which_cols")]
pub cols: SyncCell<u8>,
pub mask: SyncCell<Style>,
pub cand: SyncCell<Style>,
pub rest: SyncCell<Style>,
pub desc: SyncCell<Style>,
pub separator: String,
pub separator_style: Style,
pub separator: ArcSwap<String>,
pub separator_style: SyncCell<Style>,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Confirm {
pub border: Style,
pub title: Style,
pub body: Style,
pub list: Style,
pub border: SyncCell<Style>,
pub title: SyncCell<Style>,
pub body: SyncCell<Style>,
pub list: SyncCell<Style>,
pub btn_yes: Style,
pub btn_no: Style,
pub btn_yes: SyncCell<Style>,
pub btn_no: SyncCell<Style>,
pub btn_labels: [String; 2],
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Spot {
pub border: Style,
pub title: Style,
pub border: SyncCell<Style>,
pub title: SyncCell<Style>,
pub tbl_col: Style,
pub tbl_cell: Style,
pub tbl_col: SyncCell<Style>,
pub tbl_cell: SyncCell<Style>,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Notify {
pub title_info: Style,
pub title_warn: Style,
pub title_error: Style,
pub title_info: SyncCell<Style>,
pub title_warn: SyncCell<Style>,
pub title_error: SyncCell<Style>,
pub icon_info: String,
pub icon_warn: String,
pub icon_error: String,
pub icon_info: ArcSwap<String>,
pub icon_warn: ArcSwap<String>,
pub icon_error: ArcSwap<String>,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Pick {
pub border: Style,
pub active: Style,
pub inactive: Style,
pub border: SyncCell<Style>,
pub active: SyncCell<Style>,
pub inactive: SyncCell<Style>,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Input {
pub border: Style,
pub title: Style,
pub value: Style,
pub selected: Style,
pub border: SyncCell<Style>,
pub title: SyncCell<Style>,
pub value: SyncCell<Style>,
pub selected: SyncCell<Style>,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Cmp {
pub border: Style,
pub active: Style,
pub inactive: Style,
pub border: SyncCell<Style>,
pub active: SyncCell<Style>,
pub inactive: SyncCell<Style>,
pub icon_file: String,
pub icon_folder: String,
pub icon_command: String,
pub icon_file: ArcSwap<String>,
pub icon_folder: ArcSwap<String>,
pub icon_command: ArcSwap<String>,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Tasks {
pub border: Style,
pub title: Style,
pub hovered: Style,
pub border: SyncCell<Style>,
pub title: SyncCell<Style>,
pub hovered: SyncCell<Style>,
}
#[derive(Deserialize, DeserializeOver2)]
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Help {
pub on: Style,
pub run: Style,
pub desc: Style,
pub on: SyncCell<Style>,
pub run: SyncCell<Style>,
pub desc: SyncCell<Style>,
pub hovered: Style,
pub footer: Style,
pub hovered: SyncCell<Style>,
pub footer: SyncCell<Style>,
}
impl Theme {
@ -230,23 +233,44 @@ impl Theme {
.with_context(|| format!("Failed to read theme {p:?}"))
}
// FIXME: remove
pub(crate) fn reshape(mut self, light: bool) -> Result<Self> {
if self.which.cols < 1 || self.which.cols > 3 {
bail!("[which].cols must be between 1 and 3");
if let Some(p) = self.flavor.syntect_path(light) {
self.mgr.syntect_theme = p.into_pointee();
}
self.icon = self.icon.reshape()?;
self.mgr.syntect_theme = self
.flavor
.syntect_path(light)
.or_else(|| normalize_path(self.mgr.syntect_theme))
.ok_or(anyhow!("[mgr].syntect_theme must be either empty or an absolute path."))?;
Ok(self)
}
}
impl App {
pub fn bg_color(&self) -> String { self.overall.bg.map(|c| c.to_string()).unwrap_or_default() }
pub fn bg_color(&self) -> String {
self.overall.get().bg.map(|c| c.to_string()).unwrap_or_default()
}
}
fn deserialize_syntect_theme<'de, D>(deserializer: D) -> Result<ArcSwap<PathBuf>, D::Error>
where
D: Deserializer<'de>,
{
let mut path = PathBuf::deserialize(deserializer)?;
if !path.as_os_str().is_empty() {
path = normalize_path(path).ok_or_else(|| {
de::Error::custom("syntect_theme must be either empty or an absolute path.")
})?;
}
Ok(path.into_pointee())
}
fn deserialize_which_cols<'de, D>(deserializer: D) -> Result<SyncCell<u8>, D::Error>
where
D: Deserializer<'de>,
{
let cols = u8::deserialize(deserializer)?;
if (1..=3).contains(&cols) {
Ok(SyncCell::new(cols))
} else {
Err(de::Error::custom("cols must be between 1 and 3"))
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(service services vfs);
yazi_macro::mod_flat!(service services sftp vfs);

View file

@ -1,8 +1,6 @@
use std::{io, mem, path::PathBuf};
use serde::{Deserialize, Serialize};
use crate::normalize_path;
use crate::vfs::ServiceSftp;
#[derive(Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
@ -19,56 +17,3 @@ impl TryFrom<&'static Service> for &'static ServiceSftp {
}
}
}
impl Service {
pub(super) fn reshape(&mut self) -> io::Result<()> {
match self {
Self::Sftp(p) => p.reshape(),
}
}
}
// --- SFTP
#[derive(Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct ServiceSftp {
pub host: String,
pub user: String,
pub port: u16,
pub password: Option<String>,
#[serde(default)]
pub key_file: PathBuf,
pub key_passphrase: Option<String>,
#[serde(default)]
pub cert_file: PathBuf,
#[serde(default)]
pub no_cert_verify: bool,
#[serde(default)]
pub identity_agent: PathBuf,
}
impl ServiceSftp {
fn reshape(&mut self) -> io::Result<()> {
if !self.key_file.as_os_str().is_empty() {
self.key_file = normalize_path(mem::take(&mut self.key_file))
.ok_or_else(|| io::Error::other("key_file must be either empty or an absolute path"))?;
}
if !self.cert_file.as_os_str().is_empty() {
self.cert_file = normalize_path(mem::take(&mut self.cert_file))
.ok_or_else(|| io::Error::other("cert_file must be either empty or an absolute path"))?;
}
self.identity_agent = if self.identity_agent.as_os_str().is_empty() {
std::env::var_os("SSH_AUTH_SOCK")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.unwrap_or_default()
} else {
normalize_path(mem::take(&mut self.identity_agent)).ok_or_else(|| {
io::Error::other("identity_agent must be either empty or an absolute path")
})?
};
Ok(())
}
}

View file

@ -1,14 +1,15 @@
use std::ops::Deref;
use anyhow::{Result, bail};
use anyhow::Result;
use hashbrown::HashMap;
use serde::{Deserialize, Serialize, de::IntoDeserializer};
use serde::{Deserialize, Serialize, de};
use toml::{Spanned, de::DeTable};
use yazi_codegen::DeserializeOver;
use yazi_shim::toml::{DeserializeOverWith, deserialize_spanned};
use crate::vfs::Service;
#[derive(Deserialize, Serialize, DeserializeOver)]
#[derive(Serialize, Deserialize, DeserializeOver)]
pub struct Services(HashMap<String, Service>);
impl Deref for Services {
@ -17,27 +18,20 @@ impl Deref for Services {
fn deref(&self) -> &Self::Target { &self.0 }
}
impl Services {
pub(super) fn reshape(mut self) -> Result<Self> {
for (name, service) in &mut self.0 {
if name.is_empty() || name.len() > 20 {
bail!("VFS name `{name}` must be between 1 and 20 characters");
} else if !name.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) {
bail!("VFS name `{name}` must be in kebab-case");
}
service.reshape()?;
}
Ok(self)
}
pub(super) fn deserialize_over_with<'de>(
impl DeserializeOverWith for Services {
fn deserialize_over_with<'de>(
mut self,
table: Spanned<DeTable<'de>>,
) -> Result<Self, toml::de::Error> {
for (key, value) in table.into_inner() {
self.0.insert(key.into_inner().into_owned(), <_>::deserialize(value.into_deserializer())?);
let key = key.into_inner();
if key.is_empty() || key.len() > 20 {
Err(de::Error::custom(format!("VFS name `{key}` must be between 1 and 20 characters")))?;
} else if !key.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) {
Err(de::Error::custom(format!("VFS name `{key}` must be in kebab-case")))?;
} else {
self.0.insert(key.into_owned(), deserialize_spanned(value)?);
}
}
Ok(self)

View file

@ -0,0 +1,52 @@
use std::{env, path::PathBuf};
use serde::{Deserialize, Deserializer, Serialize, de};
use crate::normalize_path;
#[derive(Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct ServiceSftp {
pub host: String,
pub user: String,
pub port: u16,
pub password: Option<String>,
#[serde(default, deserialize_with = "deserialize_path")]
pub key_file: PathBuf,
pub key_passphrase: Option<String>,
#[serde(default, deserialize_with = "deserialize_path")]
pub cert_file: PathBuf,
#[serde(default)]
pub no_cert_verify: bool,
#[serde(default = "default_identity_agent", deserialize_with = "deserialize_identity_agent")]
pub identity_agent: PathBuf,
}
fn deserialize_path<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
where
D: Deserializer<'de>,
{
let mut path = PathBuf::deserialize(deserializer)?;
if !path.as_os_str().is_empty() {
path = normalize_path(path)
.ok_or_else(|| de::Error::custom("path must be either empty or an absolute path"))?;
}
Ok(path)
}
fn default_identity_agent() -> PathBuf {
env::var_os("SSH_AUTH_SOCK").map(PathBuf::from).filter(|p| p.is_absolute()).unwrap_or_default()
}
fn deserialize_identity_agent<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
where
D: Deserializer<'de>,
{
let path = PathBuf::deserialize(deserializer)?;
if path.as_os_str().is_empty() {
Ok(default_identity_agent())
} else {
normalize_path(path)
.ok_or_else(|| de::Error::custom("identity_agent must be either empty or an absolute path"))
}
}

View file

@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
use tokio::sync::OnceCell;
use yazi_codegen::{DeserializeOver, DeserializeOver1};
use yazi_fs::{Xdg, ok_or_not_found};
use yazi_shim::toml::DeserializeOver;
use super::Service;
use crate::{Preset, vfs::Services};
@ -19,9 +20,11 @@ impl Vfs {
pub static LOADED: OnceCell<Vfs> = OnceCell::const_new();
async fn init() -> io::Result<Vfs> {
tokio::task::spawn_blocking(|| Preset::vfs()?.deserialize_over(&Vfs::read()?)?.reshape())
.await?
.map_err(io::Error::other)
tokio::task::spawn_blocking(|| -> anyhow::Result<Vfs> {
Ok(Preset::vfs()?.deserialize_over(&Vfs::read()?)?)
})
.await?
.map_err(io::Error::other)
}
LOADED.get_or_try_init(init).await
@ -45,6 +48,4 @@ impl Vfs {
ok_or_not_found(std::fs::read_to_string(&p))
.with_context(|| format!("Failed to read config {p:?}"))
}
fn reshape(self) -> Result<Self> { Ok(Self { services: self.services.reshape()? }) }
}

View file

@ -1,9 +1,9 @@
use serde::{Deserialize, Serialize};
use yazi_codegen::DeserializeOver2;
use yazi_codegen::{DeserializeOver, DeserializeOver2};
use super::SortBy;
#[derive(Debug, Deserialize, DeserializeOver2, Serialize)]
#[derive(Debug, Deserialize, DeserializeOver, DeserializeOver2, Serialize)]
pub struct Which {
// Sorting
pub sort_by: SortBy,

View file

@ -25,19 +25,4 @@ impl Yazi {
ok_or_not_found(std::fs::read_to_string(&p))
.with_context(|| format!("Failed to read config {p:?}"))
}
pub(super) fn reshape(self) -> Result<Self> {
Ok(Self {
mgr: self.mgr.reshape()?,
preview: self.preview.reshape()?,
opener: self.opener.reshape()?,
open: self.open.reshape()?,
tasks: self.tasks.reshape()?,
plugin: self.plugin.reshape()?,
input: self.input,
confirm: self.confirm,
pick: self.pick,
which: self.which,
})
}
}

View file

@ -144,7 +144,7 @@ impl Highlighter {
}
fn load() -> (Theme, SyntaxSet) {
let theme = std::fs::File::open(&THEME.mgr.syntect_theme)
let theme = std::fs::File::open(&**THEME.mgr.syntect_theme.load())
.map_err(LoadingError::Io)
.and_then(|f| ThemeSet::load_from_reader(&mut std::io::BufReader::new(f)))
.or_else(|_| ThemeSet::load_from_reader(&mut Cursor::new(yazi_prebuilt::ansi_theme())));

View file

@ -1,3 +1,5 @@
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use yazi_config::{Style, THEME};
@ -11,19 +13,19 @@ pub enum MessageLevel {
}
impl MessageLevel {
pub fn icon(self) -> &'static str {
pub fn icon(self) -> Arc<String> {
match self {
Self::Info => &THEME.notify.icon_info,
Self::Warn => &THEME.notify.icon_warn,
Self::Error => &THEME.notify.icon_error,
Self::Info => THEME.notify.icon_info.load_full(),
Self::Warn => THEME.notify.icon_info.load_full(),
Self::Error => THEME.notify.icon_info.load_full(),
}
}
pub fn style(self) -> Style {
match self {
Self::Info => THEME.notify.title_info,
Self::Warn => THEME.notify.title_warn,
Self::Error => THEME.notify.title_error,
Self::Info => THEME.notify.title_info.get(),
Self::Warn => THEME.notify.title_warn.get(),
Self::Error => THEME.notify.title_error.get(),
}
}
}

View file

@ -11,27 +11,29 @@ pub struct Message {
pub level: MessageLevel,
pub timeout: Duration,
pub instant: Instant,
pub percent: u8,
pub max_width: usize,
pub instant: Instant,
pub percent: u8,
title_width: usize, // Width of title without icon
content_width: usize, // Width of longest line in content
}
impl From<MessageOpt> for Message {
fn from(opt: MessageOpt) -> Self {
let title = opt.title.lines().next().unwrap_or_default();
let title_width = title.width() + (opt.level.icon().width() + /* Space */ 1);
let max_width = opt.content.lines().map(|s| s.width()).max().unwrap_or(0).max(title_width);
let content_width = opt.content.lines().map(|s| s.width()).max().unwrap_or(0);
Self {
title: title.to_owned(),
title: title.to_owned(),
content: opt.content,
level: opt.level,
level: opt.level,
timeout: opt.timeout,
instant: Instant::now(),
percent: 0,
max_width: max_width + NOTIFY_BORDER as usize,
instant: Instant::now(),
percent: 0,
title_width: title.width(),
content_width,
}
}
}
@ -43,6 +45,12 @@ impl PartialEq for Message {
}
impl Message {
pub fn width(&self) -> usize {
let icon_width = self.level.icon().width() + /* Space */ 1;
self.content_width.max(self.title_width + icon_width) + NOTIFY_BORDER as usize
}
pub fn height(&self, width: u16) -> usize {
let lines = ratatui::widgets::Paragraph::new(self.content.as_str())
.wrap(ratatui::widgets::Wrap { trim: false })

View file

@ -16,9 +16,8 @@ impl AppProxy {
}
pub fn plugin_peek(job: PeekJob) {
Self::plugin(PluginOpt::new_callback(&*job.action.name, move |_, plugin| {
plugin.call_method("peek", job)
}));
let name = job.previewer.name.clone();
Self::plugin(PluginOpt::new_callback(name, move |_, plugin| plugin.call_method("peek", job)));
}
pub fn update_progress(summary: TaskSummary) {

View file

@ -23,12 +23,12 @@ impl Spot {
return;
}
let Some(spotter) = YAZI.plugin.spotter(&file, &mime) else {
let Some(spotter) = YAZI.plugin.spotters.matches(&file, &mime) else {
return self.reset();
};
self.abort();
self.ct = Some(RUNNER.spot(&spotter.run, file, mime, self.skip));
self.ct = Some(RUNNER.spot(spotter, file, mime, self.skip));
}
pub fn visible(&self) -> bool { self.lock.is_some() }

View file

@ -30,12 +30,12 @@ impl Preview {
return;
}
let Some(previewer) = YAZI.plugin.previewer(&file, &mime) else {
let Some(previewer) = YAZI.plugin.previewers.matches(&file, &mime) else {
return self.reset();
};
self.abort();
let job = PeekJob { action: &previewer.run, file, mime, skip: self.skip };
let job = PeekJob { previewer, file, mime, skip: self.skip };
self.handle = Some(tokio::spawn(async move {
let mut rx = RUNNER.peek(&job).await;

View file

@ -10,7 +10,7 @@ impl Tasks {
let mut tasks: [Vec<_>; MAX_FETCHERS as usize] = Default::default();
for f in paged {
let hash = f.hash_u64();
for g in YAZI.plugin.fetchers(f, mimetype.get(&f.url).unwrap_or_default()) {
for g in YAZI.plugin.fetchers.matches(f, mimetype.get(&f.url).unwrap_or_default()) {
match loaded.get_mut(&hash) {
Some(n) if *n & (1 << g.idx) != 0 => continue,
Some(n) => *n |= 1 << g.idx,
@ -21,9 +21,10 @@ impl Tasks {
}
drop(loaded);
let fetchers = YAZI.plugin.fetchers.load();
for (i, tasks) in tasks.into_iter().enumerate() {
if !tasks.is_empty() {
self.scheduler.fetch_paged(&YAZI.plugin.fetchers[i], tasks);
self.scheduler.fetch_paged(fetchers[i].clone(), tasks);
}
}
}
@ -32,7 +33,7 @@ impl Tasks {
let mut loaded = self.scheduler.preload.loaded.lock();
for f in paged {
let hash = f.hash_u64();
for p in YAZI.plugin.preloaders(f, mimetype.get(&f.url).unwrap_or_default()) {
for p in YAZI.plugin.preloaders.matches(f, mimetype.get(&f.url).unwrap_or_default()) {
match loaded.get_mut(&hash) {
Some(n) if *n & (1 << p.idx) != 0 => continue,
Some(n) => *n |= 1 << p.idx,

View file

@ -36,6 +36,7 @@ impl Brand {
("foot", Self::Foot),
("ghostty", Self::Ghostty),
("Warp", Self::Warp),
("Rio ", Self::Rio),
("tmux ", Self::Tmux),
("libvterm", Self::VTerm),
("Bobcat", Self::Bobcat),

View file

@ -28,9 +28,9 @@ impl Widget for Cmp<'_> {
let mut item = ListItem::new(format!(" {icon} {}{slash}", x.name.display()));
if i == self.core.cmp.rel_cursor() {
item = item.style(THEME.cmp.active);
item = item.style(THEME.cmp.active.get());
} else {
item = item.style(THEME.cmp.inactive);
item = item.style(THEME.cmp.inactive.get());
}
item
@ -54,7 +54,9 @@ impl Widget for Cmp<'_> {
yazi_widgets::Clear.render(area, buf);
List::new(items)
.block(Block::bordered().border_type(BorderType::Rounded).border_style(THEME.cmp.border))
.block(
Block::bordered().border_type(BorderType::Rounded).border_style(THEME.cmp.border.get()),
)
.render(area, buf);
}
}

View file

@ -20,7 +20,7 @@ impl Widget for Body<'_> {
// Border
let block = if self.border {
Block::new().borders(Borders::BOTTOM).border_style(THEME.confirm.border)
Block::new().borders(Borders::BOTTOM).border_style(THEME.confirm.border.get())
} else {
Block::new()
};
@ -30,7 +30,7 @@ impl Widget for Body<'_> {
.clone()
.alignment(ratatui::layout::Alignment::Center)
.block(block)
.style(THEME.confirm.body.derive(Styled::style(&confirm.body)))
.style(THEME.confirm.body.get().derive(Styled::style(&confirm.body)))
.render(inner, buf);
}
}

View file

@ -8,10 +8,10 @@ impl Widget for Buttons {
let chunks =
ratatui::layout::Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).split(area);
Paragraph::new(Span::raw(&THEME.confirm.btn_labels[0]).style(THEME.confirm.btn_yes))
Paragraph::new(Span::raw(&THEME.confirm.btn_labels[0]).style(THEME.confirm.btn_yes.get()))
.centered()
.render(chunks[0], buf);
Paragraph::new(Span::raw(&THEME.confirm.btn_labels[1]).style(THEME.confirm.btn_no))
Paragraph::new(Span::raw(&THEME.confirm.btn_labels[1]).style(THEME.confirm.btn_no.get()))
.centered()
.render(chunks[1], buf);
}

View file

@ -19,8 +19,8 @@ impl Widget for Confirm<'_> {
Block::bordered()
.border_type(BorderType::Rounded)
.border_style(THEME.confirm.border)
.title(confirm.title.clone().style(THEME.confirm.title.derive(confirm.title.style)))
.border_style(THEME.confirm.border.get())
.title(confirm.title.clone().style(THEME.confirm.title.get().derive(confirm.title.style)))
.title_alignment(Alignment::Center)
.render(area, buf);

View file

@ -16,7 +16,7 @@ impl Widget for List<'_> {
let inner = area.inner(Margin::new(2, 0));
// Bottom border
let block = Block::new().borders(Borders::BOTTOM).border_style(THEME.confirm.border);
let block = Block::new().borders(Borders::BOTTOM).border_style(THEME.confirm.border.get());
block.clone().render(area.inner(Margin::new(1, 0)), buf);
let list = self
@ -26,7 +26,7 @@ impl Widget for List<'_> {
.clone()
.scroll((self.core.confirm.offset as u16, 0))
.block(block)
.style(THEME.confirm.list)
.style(THEME.confirm.list.get())
.wrap(Wrap { trim: false });
// Vertical scrollbar

View file

@ -19,16 +19,16 @@ impl Widget for Bindings<'_> {
// On
let col1: Vec<_> =
bindings.iter().map(|c| ListItem::new(c.on()).style(THEME.help.on)).collect();
bindings.iter().map(|c| ListItem::new(c.on()).style(THEME.help.on.get())).collect();
// Run
let col2: Vec<_> =
bindings.iter().map(|c| ListItem::new(c.run()).style(THEME.help.run)).collect();
bindings.iter().map(|c| ListItem::new(c.run()).style(THEME.help.run.get())).collect();
// Desc
let col3: Vec<_> = bindings
.iter()
.map(|c| ListItem::new(c.desc().unwrap_or("-".into())).style(THEME.help.desc))
.map(|c| ListItem::new(c.desc().unwrap_or("-".into())).style(THEME.help.desc.get()))
.collect();
let chunks = layout::Layout::horizontal([
@ -41,7 +41,7 @@ impl Widget for Bindings<'_> {
let cursor = self.core.help.rel_cursor() as u16;
buf.set_style(
Rect { x: area.x, y: area.y + cursor, width: area.width, height: 1 },
THEME.help.hovered,
THEME.help.hovered.get(),
);
List::new(col1).render(chunks[0], buf);

View file

@ -27,7 +27,7 @@ impl Widget for Help<'_> {
let chunks = layout::Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).split(area);
Line::styled(
help.keyword().unwrap_or_else(|| format!("{}.help{}", help.layer, Self::tips())),
THEME.help.footer,
THEME.help.footer.get(),
)
.render(chunks[1], buf);

View file

@ -19,8 +19,8 @@ impl Widget for Input<'_> {
Block::bordered()
.border_type(BorderType::Rounded)
.border_style(THEME.input.border)
.title(Line::styled(&input.title, THEME.input.title))
.border_style(THEME.input.border.get())
.title(Line::styled(&input.title, THEME.input.title.get()))
.render(area, buf);
input.render(area.inner(Margin::new(1, 1)), buf);

View file

@ -20,9 +20,9 @@ impl<'a> Notify<'a> {
.skip(1)
.zip(messages)
.map(|(&(mut r), m)| {
if r.width > m.max_width as u16 {
r.x = r.x.saturating_add(r.width - m.max_width as u16);
r.width = m.max_width as u16;
let w = m.width() as u16;
if let Some(sub) = r.width.checked_sub(w) {
(r.x, r.width) = (r.x.saturating_add(sub), w);
}
r
})

Some files were not shown because too many files have changed in this diff Show more