diff --git a/Cargo.lock b/Cargo.lock index 6975d57b..8e237bae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,9 +266,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" dependencies = [ "memchr", "serde", @@ -327,9 +327,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aeb932158bd710538c73702db6945cb68a8fb08c519e6e12706b94263b36db8" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "jobserver", "libc", @@ -366,9 +366,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ "clap_builder", "clap_derive", @@ -376,9 +376,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" dependencies = [ "anstream", "anstyle", @@ -388,9 +388,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.37" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11611dca53440593f38e6b25ec629de50b14cdfa63adc0fb856115a2c6d97595" +checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01" dependencies = [ "clap", ] @@ -429,9 +429,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] name = "clipboard-win" @@ -819,9 +819,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1237,12 +1237,6 @@ dependencies = [ "quick-error", ] -[[package]] -name = "imagesize" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" - [[package]] name = "imgref" version = "1.11.0" @@ -1402,9 +1396,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.162" +version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" [[package]] name = "libfuzzer-sys" @@ -2292,9 +2286,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -3100,7 +3094,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -3363,7 +3357,6 @@ dependencies = [ "crossterm", "futures", "image", - "imagesize", "ratatui", "scopeguard", "tokio", diff --git a/Cargo.toml b/Cargo.toml index c4def733..f454a037 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,20 +14,20 @@ ansi-to-tui = "7.0.0" anyhow = "1.0.93" base64 = "0.22.1" bitflags = "2.6.0" -clap = { version = "4.5.20", features = [ "derive" ] } +clap = { version = "4.5.21", features = [ "derive" ] } crossterm = { version = "0.28.1", features = [ "event-stream" ] } dirs = "5.0.1" futures = "0.3.31" globset = "0.4.15" -libc = "0.2.162" +libc = "0.2.164" md-5 = "0.10.6" mlua = { version = "0.10.1", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serialize" ] } parking_lot = "0.12.3" ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info" ] } regex = "1.11.1" scopeguard = "1.2.0" -serde = { version = "1.0.214", features = [ "derive" ] } -serde_json = "1.0.132" +serde = { version = "1.0.215", features = [ "derive" ] } +serde_json = "1.0.133" shell-words = "1.1.0" tokio = { version = "1.41.1", features = [ "full" ] } tokio-stream = "0.1.16" diff --git a/cspell.json b/cspell.json index 5d3663bc..fd758078 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","Sysinfo"],"language":"en","version":"0.2"} \ No newline at end of file +{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","Sysinfo"],"language":"en","version":"0.2"} diff --git a/yazi-adapter/Cargo.toml b/yazi-adapter/Cargo.toml index c7933825..274c6ae2 100644 --- a/yazi-adapter/Cargo.toml +++ b/yazi-adapter/Cargo.toml @@ -21,7 +21,6 @@ color_quant = "1.1.0" crossterm = { workspace = true } futures = { workspace = true } image = { version = "0.25.5", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "webp" ] } -imagesize = "0.13.0" ratatui = { workspace = true } scopeguard = { workspace = true } tokio = { workspace = true } diff --git a/yazi-adapter/src/info.rs b/yazi-adapter/src/info.rs new file mode 100644 index 00000000..cdc1d60f --- /dev/null +++ b/yazi-adapter/src/info.rs @@ -0,0 +1,44 @@ +use std::path::Path; + +use image::{ImageDecoder, ImageError}; + +pub type ImageFormat = image::ImageFormat; +pub type ImageColor = image::ColorType; +pub type ImageOrientation = image::metadata::Orientation; + +#[derive(Clone, Copy)] +pub struct ImageInfo { + pub format: ImageFormat, + pub width: u32, + pub height: u32, + pub color: ImageColor, + pub orientation: Option, +} + +impl ImageInfo { + pub async fn new(path: &Path) -> image::ImageResult { + let path = path.to_owned(); + tokio::task::spawn_blocking(move || { + let reader = image::ImageReader::open(path)?.with_guessed_format()?; + + let Some(format) = reader.format() else { + return Err(ImageError::IoError(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "unknown image format", + ))); + }; + + let mut decoder = reader.into_decoder()?; + let (width, height) = decoder.dimensions(); + Ok(Self { + format, + width, + height, + color: decoder.color_type(), + orientation: decoder.orientation().ok(), + }) + }) + .await + .map_err(|e| ImageError::IoError(e.into()))? + } +} diff --git a/yazi-adapter/src/lib.rs b/yazi-adapter/src/lib.rs index a9879185..3e263e3e 100644 --- a/yazi-adapter/src/lib.rs +++ b/yazi-adapter/src/lib.rs @@ -1,7 +1,7 @@ #![allow(clippy::unit_arg)] yazi_macro::mod_flat!( - adapter chafa dimension emulator iip image kgp kgp_old mux sixel ueberzug + adapter chafa dimension emulator iip image info kgp kgp_old mux sixel ueberzug ); use yazi_shared::{RoCell, SyncCell, env_exists, in_wsl}; diff --git a/yazi-adapter/src/ueberzug.rs b/yazi-adapter/src/ueberzug.rs index 6feb9a0d..91ae9057 100644 --- a/yazi-adapter/src/ueberzug.rs +++ b/yazi-adapter/src/ueberzug.rs @@ -1,7 +1,6 @@ use std::{path::{Path, PathBuf}, process::Stdio}; use anyhow::{Result, bail}; -use imagesize::ImageSize; use ratatui::layout::Rect; use tokio::{io::AsyncWriteExt, process::{Child, Command}, sync::mpsc::{self, UnboundedSender}}; use tracing::{debug, warn}; @@ -48,8 +47,7 @@ impl Ueberzug { }; let p = path.to_owned(); - let ImageSize { width: w, height: h } = - tokio::task::spawn_blocking(move || imagesize::size(p)).await??; + let (w, h) = tokio::task::spawn_blocking(move || image::image_dimensions(p)).await??; let area = Dimension::ratio() .map(|(r1, r2)| Rect { diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index 57edcace..aa7555df 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -21,7 +21,7 @@ serde = { workspace = true } [build-dependencies] clap = { workspace = true } -clap_complete = "4.5.37" +clap_complete = "4.5.38" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.5.4" vergen-gitcl = { version = "1.0.1", features = [ "build", "rustc" ] } diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index 915a0e7b..7c3d3322 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -27,7 +27,7 @@ toml_edit = "0.22.22" # External build dependencies anyhow = { workspace = true } clap = { workspace = true } -clap_complete = "4.5.37" +clap_complete = "4.5.38" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.5.4" serde_json = { workspace = true } diff --git a/yazi-config/preset/keymap.toml b/yazi-config/preset/keymap.toml index 0f59f98b..0a38f654 100644 --- a/yazi-config/preset/keymap.toml +++ b/yazi-config/preset/keymap.toml @@ -42,10 +42,6 @@ keymap = [ { on = "H", run = "back", desc = "Go back to the previous directory" }, { on = "L", run = "forward", desc = "Go forward to the next directory" }, - # Seeking - { on = "K", run = "seek -5", desc = "Seek up 5 units in the preview" }, - { on = "J", run = "seek 5", desc = "Seek down 5 units in the preview" }, - # Toggle { on = "", run = [ "toggle", "arrow 1" ], desc = "Toggle the current selection state" }, { on = "", run = "toggle_all on", desc = "Select all files" }, @@ -55,6 +51,13 @@ keymap = [ { on = "v", run = "visual_mode", desc = "Enter visual mode (selection mode)" }, { on = "V", run = "visual_mode --unset", desc = "Enter visual mode (unset mode)" }, + # Seeking + { on = "K", run = "seek -5", desc = "Seek up 5 units in the preview" }, + { on = "J", run = "seek 5", desc = "Seek down 5 units in the preview" }, + + # Spotting + { on = "", run = "spot", desc = "Spot hovered file" }, + # Operation { on = "o", run = "open", desc = "Open selected files" }, { on = "O", run = "open --interactive", desc = "Open selected files interactively" }, @@ -175,6 +178,32 @@ keymap = [ { on = "", run = "help", desc = "Open help" }, ] +[spot] + +keymap = [ + { on = "", run = "close", desc = "Close the spot" }, + { on = "", run = "close", desc = "Close the spot" }, + { on = "", run = "close", desc = "Close the spot" }, + { on = "", run = "close", desc = "Close the spot" }, + + { on = "k", run = "arrow -1", desc = "Move cursor up" }, + { on = "j", run = "arrow 1", desc = "Move cursor down" }, + { on = "h", run = "swipe -1", desc = "Swipe to the next file" }, + { on = "l", run = "swipe 1", desc = "Swipe to the previous file" }, + + { on = "", run = "arrow -1", desc = "Move cursor up" }, + { on = "", run = "arrow 1", desc = "Move cursor down" }, + { on = "", run = "swipe -1", desc = "Swipe to the next file" }, + { on = "", run = "swipe 1", desc = "Swipe to the previous file" }, + + # Copy + { on = [ "c", "c" ], run = "copy cell", desc = "Copy selected cell" }, + + # Help + { on = "~", run = "help", desc = "Open help" }, + { on = "", run = "help", desc = "Open help" }, +] + [pick] keymap = [ diff --git a/yazi-config/preset/yazi.toml b/yazi-config/preset/yazi.toml index 9c71da8b..e9d30cff 100644 --- a/yazi-config/preset/yazi.toml +++ b/yazi-config/preset/yazi.toml @@ -92,6 +92,19 @@ fetchers = [ # Mimetype { id = "mime", name = "*", run = "mime", if = "!mime", prio = "high" }, ] +spotters = [ + { name = "*/", run = "folder" }, + # Code + { mime = "text/*", run = "code" }, + { mime = "*/{xml,javascript,x-wine-extension-ini}", run = "code" }, + # Image + { mime = "image/{avif,hei?,jxl,svg+xml}", run = "magick" }, + { mime = "image/*", run = "image" }, + # Video + { mime = "video/*", run = "video" }, + # Fallback + { name = "*", run = "file" }, +] preloaders = [ # Image { mime = "image/{avif,hei?,jxl,svg+xml}", run = "magick" }, diff --git a/yazi-config/src/keymap/keymap.rs b/yazi-config/src/keymap/keymap.rs index ede06443..a39dc686 100644 --- a/yazi-config/src/keymap/keymap.rs +++ b/yazi-config/src/keymap/keymap.rs @@ -12,6 +12,7 @@ use crate::Preset; pub struct Keymap { pub manager: Vec, pub tasks: Vec, + pub spot: Vec, pub pick: Vec, pub input: Vec, pub confirm: Vec, @@ -26,6 +27,7 @@ impl Keymap { Layer::App => unreachable!(), Layer::Manager => &self.manager, Layer::Tasks => &self.tasks, + Layer::Spot => &self.spot, Layer::Pick => &self.pick, Layer::Input => &self.input, Layer::Confirm => &self.confirm, @@ -53,6 +55,7 @@ impl<'de> Deserialize<'de> for Keymap { struct Shadow { manager: Inner, tasks: Inner, + spot: Inner, pick: Inner, input: Inner, confirm: Inner, @@ -90,7 +93,9 @@ impl<'de> Deserialize<'de> for Keymap { #[rustfmt::skip] tasks: mix(shadow.tasks.prepend_keymap, shadow.tasks.keymap, shadow.tasks.append_keymap), #[rustfmt::skip] - pick: mix(shadow.pick.prepend_keymap, shadow.pick.keymap, shadow.pick.append_keymap), + spot: mix(shadow.spot.prepend_keymap, shadow.spot.keymap, shadow.spot.append_keymap), + #[rustfmt::skip] + pick: mix(shadow.pick.prepend_keymap, shadow.pick.keymap, shadow.pick.append_keymap), #[rustfmt::skip] input: mix(shadow.input.prepend_keymap, shadow.input.keymap, shadow.input.append_keymap), #[rustfmt::skip] diff --git a/yazi-config/src/plugin/fetcher.rs b/yazi-config/src/plugin/fetcher.rs index 73ce60ec..eb265952 100644 --- a/yazi-config/src/plugin/fetcher.rs +++ b/yazi-config/src/plugin/fetcher.rs @@ -32,12 +32,12 @@ impl Fetcher { #[derive(Debug, Clone)] pub struct FetcherProps { pub id: u8, - pub name: String, + pub name: &'static str, pub prio: Priority, } -impl From<&Fetcher> for FetcherProps { - fn from(fetcher: &Fetcher) -> Self { - Self { id: fetcher.idx, name: fetcher.run.name.to_owned(), prio: fetcher.prio } +impl From<&'static Fetcher> for FetcherProps { + fn from(fetcher: &'static Fetcher) -> Self { + Self { id: fetcher.idx, name: &fetcher.run.name, prio: fetcher.prio } } } diff --git a/yazi-config/src/plugin/mod.rs b/yazi-config/src/plugin/mod.rs index 97efd0eb..f3d85edb 100644 --- a/yazi-config/src/plugin/mod.rs +++ b/yazi-config/src/plugin/mod.rs @@ -1,3 +1,3 @@ -yazi_macro::mod_flat!(fetcher plugin preloader previewer); +yazi_macro::mod_flat!(fetcher plugin preloader previewer spotter); pub const MAX_PREWORKERS: u8 = 32; diff --git a/yazi-config/src/plugin/plugin.rs b/yazi-config/src/plugin/plugin.rs index 245c7e8f..5ffd1d9a 100644 --- a/yazi-config/src/plugin/plugin.rs +++ b/yazi-config/src/plugin/plugin.rs @@ -3,22 +3,23 @@ use std::{collections::HashSet, path::Path, str::FromStr}; use anyhow::Context; use serde::{Deserialize, Deserializer}; -use super::{Fetcher, Preloader, Previewer}; +use super::{Fetcher, Preloader, Previewer, Spotter}; use crate::{Preset, plugin::MAX_PREWORKERS}; pub struct Plugin { pub fetchers: Vec, + pub spotters: Vec, pub preloaders: Vec, pub previewers: Vec, } impl Plugin { - pub fn fetchers<'a>( - &'a self, + pub fn fetchers<'a, 'b: 'a>( + &'b self, path: &'a Path, mime: &'a str, - factor: impl Fn(&str) -> bool + Copy, - ) -> impl Iterator { + factor: impl Fn(&str) -> bool + Copy + 'a, + ) -> impl Iterator + 'a { let mut seen = HashSet::new(); self.fetchers.iter().filter(move |&f| { if seen.contains(&f.id) || !f.matches(path, mime, factor) { @@ -34,11 +35,15 @@ impl Plugin { self.fetchers.iter().fold(0, |n, f| if f.mime.is_some() { n } else { n | 1 << f.idx as u32 }) } - pub fn preloaders<'a>( - &'a self, + pub fn spotter(&self, path: &Path, mime: &str) -> Option<&Spotter> { + self.spotters.iter().find(|&p| p.matches(path, mime)) + } + + pub fn preloaders<'a, 'b: 'a>( + &'b self, path: &'a Path, mime: &'a str, - ) -> impl Iterator { + ) -> impl Iterator + 'a { let mut next = true; self.preloaders.iter().filter(move |&p| { if !next || !p.matches(path, mime) { @@ -80,6 +85,12 @@ impl<'de> Deserialize<'de> for Plugin { #[serde(default)] append_fetchers: Vec, + spotters: Vec, + #[serde(default)] + prepend_spotters: Vec, + #[serde(default)] + append_spotters: Vec, + preloaders: Vec, #[serde(default)] prepend_preloaders: Vec, @@ -94,6 +105,12 @@ impl<'de> Deserialize<'de> for Plugin { } let mut shadow = Outer::deserialize(deserializer)?.plugin; + if shadow.append_spotters.iter().any(|r| r.any_file()) { + shadow.spotters.retain(|r| !r.any_file()); + } + if shadow.append_spotters.iter().any(|r| r.any_dir()) { + shadow.spotters.retain(|r| !r.any_dir()); + } if shadow.append_previewers.iter().any(|r| r.any_file()) { shadow.previewers.retain(|r| !r.any_file()); } @@ -103,6 +120,8 @@ impl<'de> Deserialize<'de> for Plugin { shadow.fetchers = Preset::mix(shadow.prepend_fetchers, shadow.fetchers, shadow.append_fetchers).collect(); + shadow.spotters = + Preset::mix(shadow.prepend_spotters, shadow.spotters, shadow.append_spotters).collect(); shadow.preloaders = Preset::mix(shadow.prepend_preloaders, shadow.preloaders, shadow.append_preloaders).collect(); shadow.previewers = @@ -121,6 +140,7 @@ impl<'de> Deserialize<'de> for Plugin { Ok(Self { fetchers: shadow.fetchers, + spotters: shadow.spotters, preloaders: shadow.preloaders, previewers: shadow.previewers, }) diff --git a/yazi-config/src/plugin/preloader.rs b/yazi-config/src/plugin/preloader.rs index 577f1eee..5ed17751 100644 --- a/yazi-config/src/plugin/preloader.rs +++ b/yazi-config/src/plugin/preloader.rs @@ -30,12 +30,12 @@ impl Preloader { #[derive(Debug, Clone)] pub struct PreloaderProps { pub id: u8, - pub name: String, + pub name: &'static str, pub prio: Priority, } -impl From<&Preloader> for PreloaderProps { - fn from(preloader: &Preloader) -> Self { - Self { id: preloader.idx, name: preloader.run.name.to_owned(), prio: preloader.prio } +impl From<&'static Preloader> for PreloaderProps { + fn from(preloader: &'static Preloader) -> Self { + Self { id: preloader.idx, name: &preloader.run.name, prio: preloader.prio } } } diff --git a/yazi-config/src/plugin/spotter.rs b/yazi-config/src/plugin/spotter.rs new file mode 100644 index 00000000..86258166 --- /dev/null +++ b/yazi-config/src/plugin/spotter.rs @@ -0,0 +1,27 @@ +use std::path::Path; + +use serde::Deserialize; +use yazi_shared::{MIME_DIR, event::Cmd}; + +use crate::Pattern; + +#[derive(Debug, Deserialize)] +pub struct Spotter { + pub name: Option, + pub mime: Option, + pub run: Cmd, +} + +impl Spotter { + #[inline] + pub fn matches(&self, path: &Path, mime: &str) -> bool { + self.mime.as_ref().is_some_and(|p| p.match_mime(mime)) + || self.name.as_ref().is_some_and(|p| p.match_path(path, mime == MIME_DIR)) + } + + #[inline] + pub fn any_file(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_file()) } + + #[inline] + pub fn any_dir(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_dir()) } +} diff --git a/yazi-core/src/lib.rs b/yazi-core/src/lib.rs index 8d7616ef..6c2d14eb 100644 --- a/yazi-core/src/lib.rs +++ b/yazi-core/src/lib.rs @@ -6,7 +6,7 @@ clippy::unit_arg )] -yazi_macro::mod_pub!(completion confirm help input manager notify pick tab tasks which); +yazi_macro::mod_pub!(completion confirm help input manager notify pick spot tab tasks which); pub fn init() { manager::WATCHED.with(<_>::default); diff --git a/yazi-core/src/manager/commands/mod.rs b/yazi-core/src/manager/commands/mod.rs index cf1f8b73..c184cbe9 100644 --- a/yazi-core/src/manager/commands/mod.rs +++ b/yazi-core/src/manager/commands/mod.rs @@ -13,6 +13,7 @@ yazi_macro::mod_flat!( remove rename seek + spot suspend tab_close tab_create diff --git a/yazi-core/src/manager/commands/spot.rs b/yazi-core/src/manager/commands/spot.rs new file mode 100644 index 00000000..621db820 --- /dev/null +++ b/yazi-core/src/manager/commands/spot.rs @@ -0,0 +1,40 @@ +use std::borrow::Cow; + +use yazi_shared::{MIME_DIR, event::{Cmd, Data}}; + +use crate::manager::Manager; + +struct Opt { + skip: Option, +} + +impl From for Opt { + fn from(c: Cmd) -> Self { Self { skip: c.get("skip").and_then(Data::as_usize) } } +} + +impl Manager { + #[yazi_codegen::command] + pub fn spot(&mut self, opt: Opt) { + let Some(hovered) = self.hovered().cloned() else { + return; + }; + + let mime = if hovered.is_dir() { + Cow::Borrowed(MIME_DIR) + } else { + Cow::Owned(self.mimetype.get_owned(&hovered.url).unwrap_or_default()) + }; + + if !self.active().spot.same_file(&hovered, &mime) { + // self.active_mut().spot.reset(); + } + + if let Some(skip) = opt.skip { + self.active_mut().spot.skip = skip; + } else if !self.active().spot.same_url(&hovered.url) { + self.active_mut().spot.skip = 0; + } + + self.active_mut().spot.go(hovered, mime); + } +} diff --git a/yazi-core/src/manager/commands/tab_create.rs b/yazi-core/src/manager/commands/tab_create.rs index 39993e73..ed4169b3 100644 --- a/yazi-core/src/manager/commands/tab_create.rs +++ b/yazi-core/src/manager/commands/tab_create.rs @@ -40,11 +40,11 @@ impl Tabs { if !opt.current { tab.cd(opt.url); } else if let Some(h) = self.active().hovered() { - tab.conf = self.active().conf.clone(); + tab.pref = self.active().pref.clone(); tab.apply_files_attrs(); tab.reveal(h.url.to_regular()); } else { - tab.conf = self.active().conf.clone(); + tab.pref = self.active().pref.clone(); tab.apply_files_attrs(); tab.cd(self.active().cwd().to_regular()); } diff --git a/yazi-core/src/spot/commands/arrow.rs b/yazi-core/src/spot/commands/arrow.rs new file mode 100644 index 00000000..f815c12b --- /dev/null +++ b/yazi-core/src/spot/commands/arrow.rs @@ -0,0 +1,31 @@ +use yazi_macro::render; +use yazi_proxy::ManagerProxy; +use yazi_shared::event::{Cmd, Data}; + +use crate::spot::Spot; + +struct Opt { + step: isize, +} + +impl From for Opt { + fn from(c: Cmd) -> Self { Self { step: c.first().and_then(Data::as_isize).unwrap_or(0) } } +} + +impl Spot { + #[yazi_codegen::command] + pub fn arrow(&mut self, opt: Opt) { + let Some(lock) = &mut self.lock else { return }; + + let Some(old) = lock.selected() else { + let new = self.skip.saturating_add_signed(opt.step); + return ManagerProxy::spot(Some(new)); + }; + + lock.select(Some(old.saturating_add_signed(opt.step))); + let new = lock.selected().unwrap(); + + self.skip = new; + render!(new != old); + } +} diff --git a/yazi-core/src/spot/commands/close.rs b/yazi-core/src/spot/commands/close.rs new file mode 100644 index 00000000..06dd6de3 --- /dev/null +++ b/yazi-core/src/spot/commands/close.rs @@ -0,0 +1,21 @@ +use yazi_macro::render; +use yazi_shared::event::Cmd; + +use crate::spot::Spot; + +struct Opt; + +impl From for Opt { + fn from(_: Cmd) -> Self { Self } +} +impl From<()> for Opt { + fn from(_: ()) -> Self { Self } +} + +impl Spot { + #[yazi_codegen::command] + pub fn close(&mut self, _: Opt) { + self.ct.take().map(|h| h.cancel()); + render!(self.lock.take().is_some()); + } +} diff --git a/yazi-core/src/spot/commands/copy.rs b/yazi-core/src/spot/commands/copy.rs new file mode 100644 index 00000000..6d01d8a7 --- /dev/null +++ b/yazi-core/src/spot/commands/copy.rs @@ -0,0 +1,34 @@ +use yazi_plugin::CLIPBOARD; +use yazi_shared::event::Cmd; + +use crate::spot::Spot; + +struct Opt { + type_: String, +} + +impl From for Opt { + fn from(mut c: Cmd) -> Self { Self { type_: c.take_first_str().unwrap_or_default() } } +} + +impl Spot { + #[yazi_codegen::command] + pub fn copy(&mut self, opt: Opt) { + let Some(lock) = &self.lock else { return }; + let Some(table) = lock.table() else { return }; + + let mut s = String::new(); + match opt.type_.as_str() { + "cell" => { + let Some(cell) = table.selected_cell() else { return }; + s = cell.to_string(); + } + "line" => { + // TODO + } + _ => {} + } + + futures::executor::block_on(CLIPBOARD.set(s)); + } +} diff --git a/yazi-core/src/spot/commands/mod.rs b/yazi-core/src/spot/commands/mod.rs new file mode 100644 index 00000000..ac9dae93 --- /dev/null +++ b/yazi-core/src/spot/commands/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(arrow close copy swipe); diff --git a/yazi-core/src/spot/commands/swipe.rs b/yazi-core/src/spot/commands/swipe.rs new file mode 100644 index 00000000..420d1679 --- /dev/null +++ b/yazi-core/src/spot/commands/swipe.rs @@ -0,0 +1,20 @@ +use yazi_proxy::{ManagerProxy, TabProxy}; +use yazi_shared::event::{Cmd, Data}; + +use crate::spot::Spot; + +struct Opt { + step: isize, +} + +impl From for Opt { + fn from(c: Cmd) -> Self { Self { step: c.first().and_then(Data::as_isize).unwrap_or(0) } } +} + +impl Spot { + #[yazi_codegen::command] + pub fn swipe(&mut self, opt: Opt) { + TabProxy::arrow(opt.step); + ManagerProxy::spot(None); + } +} diff --git a/yazi-core/src/spot/mod.rs b/yazi-core/src/spot/mod.rs new file mode 100644 index 00000000..593f2f5d --- /dev/null +++ b/yazi-core/src/spot/mod.rs @@ -0,0 +1,3 @@ +yazi_macro::mod_pub!(commands); + +yazi_macro::mod_flat!(spot); diff --git a/yazi-core/src/spot/spot.rs b/yazi-core/src/spot/spot.rs new file mode 100644 index 00000000..66201013 --- /dev/null +++ b/yazi-core/src/spot/spot.rs @@ -0,0 +1,54 @@ +use std::borrow::Cow; + +use tokio_util::sync::CancellationToken; +use yazi_config::PLUGIN; +use yazi_macro::render; +use yazi_plugin::{isolate, utils::SpotLock}; +use yazi_shared::fs::{File, Url}; + +#[derive(Default)] +pub struct Spot { + pub lock: Option, + pub skip: usize, + + pub(super) ct: Option, +} + +impl Spot { + pub fn go(&mut self, file: File, mime: Cow<'static, str>) { + if mime.is_empty() { + return; // Wait till mimetype is resolved to avoid flickering + } else if self.same_lock(&file, &mime) { + return; + } + + let Some(spotter) = PLUGIN.spotter(&file.url, &mime) else { + return self.reset(); + }; + + self.ct = Some(isolate::spot(&spotter.run, file, mime, self.skip)); + } + + #[inline] + pub fn visible(&self) -> bool { self.lock.is_some() } + + #[inline] + pub fn reset(&mut self) { + self.ct.take().map(|ct| ct.cancel()); + render!(self.lock.take().is_some()); + } + + #[inline] + pub fn same_url(&self, url: &Url) -> bool { self.lock.as_ref().is_some_and(|l| *url == l.url) } + + #[inline] + pub fn same_file(&self, file: &File, mime: &str) -> bool { + self.same_url(&file.url) + && self.lock.as_ref().is_some_and(|l| file.cha.hits(l.cha) && mime == l.mime) + } + + #[inline] + pub fn same_lock(&self, file: &File, mime: &str) -> bool { + self.same_file(file, mime) && self.lock.as_ref().is_some_and(|l| self.skip == l.skip) + } +} diff --git a/yazi-core/src/tab/commands/hidden.rs b/yazi-core/src/tab/commands/hidden.rs index 266b119d..fc1fa651 100644 --- a/yazi-core/src/tab/commands/hidden.rs +++ b/yazi-core/src/tab/commands/hidden.rs @@ -5,10 +5,10 @@ use crate::tab::Tab; impl Tab { pub fn hidden(&mut self, mut c: Cmd) { - self.conf.show_hidden = match c.take_first_str().as_deref() { + self.pref.show_hidden = match c.take_first_str().as_deref() { Some("show") => true, Some("hide") => false, - _ => !self.conf.show_hidden, + _ => !self.pref.show_hidden, }; let hovered = self.hovered().map(|f| f.url_owned()); diff --git a/yazi-core/src/tab/commands/linemode.rs b/yazi-core/src/tab/commands/linemode.rs index dced281e..736020fc 100644 --- a/yazi-core/src/tab/commands/linemode.rs +++ b/yazi-core/src/tab/commands/linemode.rs @@ -5,7 +5,7 @@ use crate::tab::Tab; impl Tab { pub fn linemode(&mut self, mut c: Cmd) { - render!(self.conf.patch(|new| { + render!(self.pref.patch(|new| { let Some(mode) = c.take_first_str() else { return; }; diff --git a/yazi-core/src/tab/commands/mod.rs b/yazi-core/src/tab/commands/mod.rs index 5021dcb0..60c498ca 100644 --- a/yazi-core/src/tab/commands/mod.rs +++ b/yazi-core/src/tab/commands/mod.rs @@ -20,7 +20,6 @@ yazi_macro::mod_flat!( select_all shell sort - spot toggle toggle_all update_peeked diff --git a/yazi-core/src/tab/commands/search.rs b/yazi-core/src/tab/commands/search.rs index d3550db2..068cff57 100644 --- a/yazi-core/src/tab/commands/search.rs +++ b/yazi-core/src/tab/commands/search.rs @@ -45,7 +45,7 @@ impl Tab { } let cwd = self.cwd().to_search(&opt.subject); - let hidden = self.conf.show_hidden; + let hidden = self.pref.show_hidden; self.search = Some(tokio::spawn(async move { let rx = if opt.via == SearchOptVia::Rg { diff --git a/yazi-core/src/tab/commands/sort.rs b/yazi-core/src/tab/commands/sort.rs index 1325eaea..25acec97 100644 --- a/yazi-core/src/tab/commands/sort.rs +++ b/yazi-core/src/tab/commands/sort.rs @@ -8,15 +8,15 @@ use crate::{tab::Tab, tasks::Tasks}; impl Tab { pub fn sort(&mut self, mut c: Cmd, tasks: &Tasks) { - let conf = &mut self.conf; + let pref = &mut self.pref; if let Some(by) = c.take_first_str() { - conf.sort_by = SortBy::from_str(&by).unwrap_or_default(); + pref.sort_by = SortBy::from_str(&by).unwrap_or_default(); } - conf.sort_reverse = c.maybe_bool("reverse").unwrap_or(conf.sort_reverse); - conf.sort_dir_first = c.maybe_bool("dir-first").unwrap_or(conf.sort_dir_first); - conf.sort_sensitive = c.maybe_bool("sensitive").unwrap_or(conf.sort_sensitive); - conf.sort_translit = c.maybe_bool("translit").unwrap_or(conf.sort_translit); + pref.sort_reverse = c.maybe_bool("reverse").unwrap_or(pref.sort_reverse); + pref.sort_dir_first = c.maybe_bool("dir-first").unwrap_or(pref.sort_dir_first); + pref.sort_sensitive = c.maybe_bool("sensitive").unwrap_or(pref.sort_sensitive); + pref.sort_translit = c.maybe_bool("translit").unwrap_or(pref.sort_translit); self.apply_files_attrs(); ManagerProxy::update_paged(); diff --git a/yazi-core/src/tab/commands/spot.rs b/yazi-core/src/tab/commands/spot.rs deleted file mode 100644 index 9623f90d..00000000 --- a/yazi-core/src/tab/commands/spot.rs +++ /dev/null @@ -1,20 +0,0 @@ -use yazi_shared::event::{Cmd, Data}; - -use crate::tab::Tab; - -struct Opt { - skip: Option, -} - -impl From for Opt { - fn from(c: Cmd) -> Self { Self { skip: c.first().and_then(Data::as_usize) } } -} - -impl Tab { - #[yazi_codegen::command] - pub fn spot(&mut self, c: Cmd) { - let Some(hovered) = self.hovered().cloned() else { - return self.preview.reset(); - }; - } -} diff --git a/yazi-core/src/tab/commands/update_spotted.rs b/yazi-core/src/tab/commands/update_spotted.rs index 04c16eb4..05eea446 100644 --- a/yazi-core/src/tab/commands/update_spotted.rs +++ b/yazi-core/src/tab/commands/update_spotted.rs @@ -1,10 +1,11 @@ -use yazi_plugin::utils::PreviewLock; +use yazi_macro::render; +use yazi_plugin::utils::SpotLock; use yazi_shared::event::Cmd; use crate::tab::Tab; pub struct Opt { - lock: PreviewLock, + lock: SpotLock, } impl TryFrom for Opt { @@ -17,6 +18,20 @@ impl TryFrom for Opt { impl Tab { pub fn update_spotted(&mut self, opt: impl TryInto) { - todo!(); + let Some(hovered) = self.hovered().map(|h| &h.url) else { + return self.preview.reset(); + }; + + let Ok(opt) = opt.try_into() else { + return; + }; + + if opt.lock.url != *hovered { + return; + } + + self.spot.skip = opt.lock.selected().unwrap_or_default(); + self.spot.lock = Some(opt.lock); + render!(); } } diff --git a/yazi-core/src/tab/mod.rs b/yazi-core/src/tab/mod.rs index 9c34efe4..268c5946 100644 --- a/yazi-core/src/tab/mod.rs +++ b/yazi-core/src/tab/mod.rs @@ -1,3 +1,3 @@ yazi_macro::mod_pub!(commands); -yazi_macro::mod_flat!(backstack config finder history mode preview selected tab); +yazi_macro::mod_flat!(backstack preference finder history mode preview selected tab); diff --git a/yazi-core/src/tab/config.rs b/yazi-core/src/tab/preference.rs similarity index 93% rename from yazi-core/src/tab/config.rs rename to yazi-core/src/tab/preference.rs index 292a5f0d..e8c258fc 100644 --- a/yazi-core/src/tab/config.rs +++ b/yazi-core/src/tab/preference.rs @@ -2,7 +2,7 @@ use yazi_config::{MANAGER, manager::SortBy}; use yazi_fs::FilesSorter; #[derive(Clone, PartialEq)] -pub struct Config { +pub struct Preference { // Sorting pub sort_by: SortBy, pub sort_sensitive: bool, @@ -15,7 +15,7 @@ pub struct Config { pub show_hidden: bool, } -impl Default for Config { +impl Default for Preference { fn default() -> Self { Self { // Sorting @@ -32,7 +32,7 @@ impl Default for Config { } } -impl Config { +impl Preference { pub(super) fn patch(&mut self, f: F) -> bool { let old = self.clone(); f(self); diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index abd55ca8..e6e3aa2d 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -9,13 +9,13 @@ use yazi_fs::{Folder, FolderStage}; use yazi_macro::render; use yazi_shared::{Id, Ids, fs::{File, Url}}; -use super::{Backstack, Config, Finder, History, Mode, Preview}; -use crate::tab::Selected; +use super::{Backstack, Finder, History, Mode, Preference, Preview}; +use crate::{spot::Spot, tab::Selected}; pub struct Tab { pub id: Id, pub mode: Mode, - pub conf: Config, + pub pref: Preference, pub current: Folder, pub parent: Option, @@ -23,6 +23,7 @@ pub struct Tab { pub history: History, pub selected: Selected, + pub spot: Spot, pub preview: Preview, pub finder: Option, pub search: Option>>, @@ -35,7 +36,7 @@ impl Default for Tab { Self { id: IDS.next(), mode: Default::default(), - conf: Default::default(), + pref: Default::default(), current: Default::default(), parent: Default::default(), @@ -43,6 +44,7 @@ impl Default for Tab { history: Default::default(), selected: Default::default(), + spot: Default::default(), preview: Default::default(), finder: Default::default(), search: Default::default(), @@ -123,8 +125,8 @@ impl Tab { } let hovered = f.hovered().filter(|_| f.tracing).map(|h| h.urn_owned()); - f.files.set_show_hidden(self.conf.show_hidden); - f.files.set_sorter(self.conf.sorter()); + f.files.set_show_hidden(self.pref.show_hidden); + f.files.set_sorter(self.pref.sorter()); render!(f.files.catchup_revision()); render!(f.repos(hovered.as_ref().map(|u| u.as_urn()))); diff --git a/yazi-fm/src/app/commands/render.rs b/yazi-fm/src/app/commands/render.rs index 597abf7a..cfd30c30 100644 --- a/yazi-fm/src/app/commands/render.rs +++ b/yazi-fm/src/app/commands/render.rs @@ -49,7 +49,7 @@ impl App { let frame = term .draw_partial(|f| { _ = Lives::scope(&self.cx, || { - f.render_widget(crate::tasks::Progress, f.area()); + f.render_widget(crate::tasks::Progress::new(&self.cx), f.area()); f.render_widget(crate::notify::Notify::new(&self.cx), f.area()); Ok(()) }); diff --git a/yazi-fm/src/app/commands/resize.rs b/yazi-fm/src/app/commands/resize.rs index 250226c7..ce2ca020 100644 --- a/yazi-fm/src/app/commands/resize.rs +++ b/yazi-fm/src/app/commands/resize.rs @@ -15,10 +15,10 @@ impl From<()> for Opt { impl App { #[yazi_codegen::command] pub fn resize(&mut self, _: Opt) { - self.cx.manager.active_mut().preview.reset(); + self.cx.active_mut().preview.reset(); self.reflow(()); - self.cx.manager.current_mut().sync_page(true); + self.cx.current_mut().sync_page(true); self.cx.manager.hover(None); } } diff --git a/yazi-fm/src/app/commands/resume.rs b/yazi-fm/src/app/commands/resume.rs index fb3c5986..71c94e18 100644 --- a/yazi-fm/src/app/commands/resume.rs +++ b/yazi-fm/src/app/commands/resume.rs @@ -4,7 +4,7 @@ use crate::{Term, app::App}; impl App { pub(crate) fn resume(&mut self, _: Cmd) { - self.cx.manager.active_mut().preview.reset_image(); + self.cx.active_mut().preview.reset_image(); self.term = Some(Term::start().unwrap()); // While the app resumes, it's possible that the terminal size has changed. diff --git a/yazi-fm/src/app/commands/stop.rs b/yazi-fm/src/app/commands/stop.rs index 4342c388..fed1c1df 100644 --- a/yazi-fm/src/app/commands/stop.rs +++ b/yazi-fm/src/app/commands/stop.rs @@ -14,7 +14,7 @@ impl From for Opt { impl App { #[yazi_codegen::command] pub fn stop(&mut self, opt: Opt) { - self.cx.manager.active_mut().preview.reset_image(); + self.cx.active_mut().preview.reset_image(); // We need to destroy the `term` first before stopping the `signals` // to prevent any signal from triggering the term to render again diff --git a/yazi-fm/src/context.rs b/yazi-fm/src/context.rs index 3d46b539..8e4c1b4d 100644 --- a/yazi-fm/src/context.rs +++ b/yazi-fm/src/context.rs @@ -1,5 +1,6 @@ use ratatui::layout::Rect; -use yazi_core::{completion::Completion, confirm::Confirm, help::Help, input::Input, manager::Manager, notify::Notify, pick::Pick, tasks::Tasks, which::Which}; +use yazi_core::{completion::Completion, confirm::Confirm, help::Help, input::Input, manager::Manager, notify::Notify, pick::Pick, tab::Tab, tasks::Tasks, which::Which}; +use yazi_fs::Folder; pub struct Ctx { pub manager: Manager, @@ -40,3 +41,17 @@ impl Ctx { None } } + +impl Ctx { + #[inline] + pub fn active(&self) -> &Tab { self.manager.active() } + + #[inline] + pub fn active_mut(&mut self) -> &mut Tab { self.manager.active_mut() } + + #[inline] + pub fn current(&self) -> &Folder { self.manager.current() } + + #[inline] + pub fn current_mut(&mut self) -> &mut Folder { self.manager.current_mut() } +} diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 3768a469..2c3b4bc5 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -17,6 +17,7 @@ impl<'a> Executor<'a> { Layer::App => self.app(cmd), Layer::Manager => self.manager(cmd), Layer::Tasks => self.tasks(cmd), + Layer::Spot => self.spot(cmd), Layer::Pick => self.pick(cmd), Layer::Input => self.input(cmd), Layer::Confirm => self.confirm(cmd), @@ -79,7 +80,7 @@ impl<'a> Executor<'a> { on!(MANAGER, hover); on!(MANAGER, peek); on!(MANAGER, seek); - on!(ACTIVE, spot); + on!(MANAGER, spot); on!(MANAGER, refresh, &self.app.cx.tasks); on!(MANAGER, quit, &self.app.cx.tasks); on!(MANAGER, close, &self.app.cx.tasks); @@ -182,6 +183,29 @@ impl<'a> Executor<'a> { } } + fn spot(&mut self, cmd: Cmd) { + macro_rules! on { + ($name:ident) => { + if cmd.name == stringify!($name) { + return self.app.cx.active_mut().spot.$name(cmd); + } + }; + } + + on!(arrow); + on!(close); + on!(swipe); + on!(copy); + + match cmd.name.as_str() { + // Help + "help" => self.app.cx.help.toggle(Layer::Spot), + // Plugin + "plugin" => self.app.plugin(cmd), + _ => {} + } + } + fn pick(&mut self, cmd: Cmd) { macro_rules! on { ($name:ident) => { diff --git a/yazi-fm/src/lives/file.rs b/yazi-fm/src/lives/file.rs index fa062da7..f07854fd 100644 --- a/yazi-fm/src/lives/file.rs +++ b/yazi-fm/src/lives/file.rs @@ -110,7 +110,7 @@ impl UserData for File { }); methods.add_method("found", |lua, me, ()| { lua.named_registry_value::("cx")?.borrow_scoped(|cx: &Ctx| { - let Some(finder) = &cx.manager.active().finder else { + let Some(finder) = &cx.active().finder else { return Ok(None); }; @@ -124,7 +124,7 @@ impl UserData for File { }); methods.add_method("highlights", |lua, me, ()| { lua.named_registry_value::("cx")?.borrow_scoped(|cx: &Ctx| { - let Some(finder) = &cx.manager.active().finder else { + let Some(finder) = &cx.active().finder else { return None; }; if me.folder().url != me.tab().current.url { diff --git a/yazi-fm/src/lives/lives.rs b/yazi-fm/src/lives/lives.rs index 7ffd4d80..8a323253 100644 --- a/yazi-fm/src/lives/lives.rs +++ b/yazi-fm/src/lives/lives.rs @@ -30,7 +30,7 @@ impl Lives { LUA.globals().raw_set( "cx", LUA.create_table_from([ - ("active", super::Tab::make(cx.manager.active())?), + ("active", super::Tab::make(cx.active())?), ("tabs", super::Tabs::make(&cx.manager.tabs)?), ("tasks", super::Tasks::make(&cx.tasks)?), ("yanked", super::Yanked::make(&cx.manager.yanked)?), diff --git a/yazi-fm/src/lives/mod.rs b/yazi-fm/src/lives/mod.rs index 2b8b33e6..9ad65f0f 100644 --- a/yazi-fm/src/lives/mod.rs +++ b/yazi-fm/src/lives/mod.rs @@ -1,6 +1,6 @@ #![allow(clippy::module_inception)] yazi_macro::mod_flat!( - config file files filter finder folder iter lives mode preview selected tab tabs + preference file files filter finder folder iter lives mode preview selected tab tabs tasks yanked ); diff --git a/yazi-fm/src/lives/config.rs b/yazi-fm/src/lives/preference.rs similarity index 74% rename from yazi-fm/src/lives/config.rs rename to yazi-fm/src/lives/preference.rs index 6761ddf1..4ee7c459 100644 --- a/yazi-fm/src/lives/config.rs +++ b/yazi-fm/src/lives/preference.rs @@ -4,24 +4,24 @@ use mlua::{AnyUserData, UserData, UserDataFields}; use super::Lives; -pub(super) struct Config { - inner: *const yazi_core::tab::Config, +pub(super) struct Preference { + inner: *const yazi_core::tab::Preference, } -impl Deref for Config { - type Target = yazi_core::tab::Config; +impl Deref for Preference { + type Target = yazi_core::tab::Preference; fn deref(&self) -> &Self::Target { unsafe { &*self.inner } } } -impl Config { +impl Preference { #[inline] - pub(super) fn make(inner: &yazi_core::tab::Config) -> mlua::Result { + pub(super) fn make(inner: &yazi_core::tab::Preference) -> mlua::Result { Lives::scoped_userdata(Self { inner }) } } -impl UserData for Config { +impl UserData for Preference { fn add_fields>(fields: &mut F) { fields.add_field_method_get("sort_by", |_, me| Ok(me.sort_by.to_string())); fields.add_field_method_get("sort_sensitive", |_, me| Ok(me.sort_sensitive)); diff --git a/yazi-fm/src/lives/tab.rs b/yazi-fm/src/lives/tab.rs index c0ebaa13..f7aa3f22 100644 --- a/yazi-fm/src/lives/tab.rs +++ b/yazi-fm/src/lives/tab.rs @@ -3,7 +3,7 @@ use std::ops::Deref; use mlua::{AnyUserData, UserData, UserDataFields, UserDataMethods}; use yazi_plugin::url::UrlRef; -use super::{Config, Finder, Folder, Lives, Mode, Preview, Selected}; +use super::{Finder, Folder, Lives, Mode, Preference, Preview, Selected}; pub(super) struct Tab { inner: *const yazi_core::tab::Tab, @@ -26,7 +26,29 @@ impl UserData for Tab { fn add_fields>(fields: &mut F) { fields.add_field_method_get("id", |_, me| Ok(me.id.get())); fields.add_field_method_get("mode", |_, me| Mode::make(&me.mode)); - fields.add_field_method_get("conf", |_, me| Config::make(&me.conf)); + // TODO: remove `conf` once v0.4 is released + fields.add_field_method_get("conf", |lua, me| { + static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + + if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) { + let id = match lua.named_registry_value::("rt")?.current() { + Some(id) => format!("`{id}.yazi` plugin"), + None => "`init.lua` config".to_owned(), + }; + let s = "The `conf` property of `tab::Tab` has been deprecated in Yazi v0.4. + +Please use `pref` instead, e.g. `cx.active.conf` => `cx.active.pref`, in your {id}."; + yazi_proxy::AppProxy::notify(yazi_proxy::options::NotifyOpt { + title: "Deprecated API".to_owned(), + content: s.replace("{id}", &id), + level: yazi_proxy::options::NotifyLevel::Warn, + timeout: std::time::Duration::from_secs(20), + }); + } + + Preference::make(&me.pref) + }); + fields.add_field_method_get("pref", |_, me| Preference::make(&me.pref)); fields.add_field_method_get("current", |_, me| Folder::make(None, &me.current, me)); fields.add_field_method_get("parent", |_, me| { me.parent.as_ref().map(|f| Folder::make(None, f, me)).transpose() diff --git a/yazi-fm/src/main.rs b/yazi-fm/src/main.rs index 026f66a9..ef71acee 100644 --- a/yazi-fm/src/main.rs +++ b/yazi-fm/src/main.rs @@ -5,7 +5,7 @@ #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; -yazi_macro::mod_pub!(app completion confirm help input lives manager notify pick tasks which); +yazi_macro::mod_pub!(app completion confirm help input lives manager notify pick spot tasks which); yazi_macro::mod_flat!(context executor logs panic root router signals term); diff --git a/yazi-fm/src/manager/preview.rs b/yazi-fm/src/manager/preview.rs index 897a9f13..b1ae5fcf 100644 --- a/yazi-fm/src/manager/preview.rs +++ b/yazi-fm/src/manager/preview.rs @@ -1,4 +1,5 @@ use ratatui::{buffer::Buffer, widgets::Widget}; +use yazi_config::LAYOUT; use crate::Ctx; @@ -12,18 +13,17 @@ impl<'a> Preview<'a> { } impl Widget for Preview<'_> { - fn render(self, area: ratatui::layout::Rect, buf: &mut Buffer) { - let preview = &self.cx.manager.active().preview; - let Some(lock) = &preview.lock else { + fn render(self, _: ratatui::layout::Rect, buf: &mut Buffer) { + let Some(lock) = &self.cx.active().preview.lock else { return; }; - if (lock.window.rows, lock.window.cols) != (area.height, area.width) { + if *lock.area != LAYOUT.get().preview { return; } for w in &lock.data { - w.clone_render(buf); + w.clone().render(buf, |p| self.cx.manager.area(p)); } } } diff --git a/yazi-fm/src/root.rs b/yazi-fm/src/root.rs index 53362ab6..844cc483 100644 --- a/yazi-fm/src/root.rs +++ b/yazi-fm/src/root.rs @@ -1,9 +1,9 @@ use mlua::{ObjectLike, Table}; use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; use tracing::error; -use yazi_plugin::{LUA, elements::render_widgets}; +use yazi_plugin::{LUA, elements::render_once}; -use super::{completion, confirm, help, input, manager, pick, tasks, which}; +use super::{completion, confirm, help, input, manager, pick, spot, tasks, which}; use crate::Ctx; pub(super) struct Root<'a> { @@ -26,7 +26,7 @@ impl Widget for Root<'_> { let area = yazi_plugin::elements::Rect::from(area); let root = LUA.globals().raw_get::("Root")?.call_method::
("new", area)?; - render_widgets(root.call_method("redraw", ())?, buf); + render_once(root.call_method("redraw", ())?, buf, |p| self.cx.manager.area(p)); Ok::<_, mlua::Error>(()) }; if let Err(e) = f() { @@ -39,6 +39,10 @@ impl Widget for Root<'_> { tasks::Tasks::new(self.cx).render(area, buf); } + if self.cx.active().spot.visible() { + spot::Spot::new(self.cx).render(area, buf); + } + if self.cx.pick.visible { pick::Pick::new(self.cx).render(area, buf); } diff --git a/yazi-fm/src/router.rs b/yazi-fm/src/router.rs index 1f45da53..0a71cce4 100644 --- a/yazi-fm/src/router.rs +++ b/yazi-fm/src/router.rs @@ -36,6 +36,8 @@ impl<'a> Router<'a> { self.matches(Layer::Confirm, key) } else if cx.pick.visible { self.matches(Layer::Pick, key) + } else if cx.active().spot.visible() { + self.matches(Layer::Spot, key) } else if cx.tasks.visible { self.matches(Layer::Tasks, key) } else { diff --git a/yazi-fm/src/spot/mod.rs b/yazi-fm/src/spot/mod.rs new file mode 100644 index 00000000..9681606b --- /dev/null +++ b/yazi-fm/src/spot/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(spot); diff --git a/yazi-fm/src/spot/spot.rs b/yazi-fm/src/spot/spot.rs new file mode 100644 index 00000000..4221c8b0 --- /dev/null +++ b/yazi-fm/src/spot/spot.rs @@ -0,0 +1,23 @@ +use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; + +use crate::Ctx; + +pub(crate) struct Spot<'a> { + cx: &'a Ctx, +} + +impl<'a> Spot<'a> { + pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } } +} + +impl Widget for Spot<'_> { + fn render(self, _: Rect, buf: &mut Buffer) { + let Some(lock) = &self.cx.active().spot.lock else { + return; + }; + + for w in &lock.data { + w.clone().render(buf, |p| self.cx.manager.area(p)); + } + } +} diff --git a/yazi-fm/src/tasks/progress.rs b/yazi-fm/src/tasks/progress.rs index cb58dc05..e751ba3d 100644 --- a/yazi-fm/src/tasks/progress.rs +++ b/yazi-fm/src/tasks/progress.rs @@ -2,18 +2,26 @@ use mlua::{ObjectLike, Table}; use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; use tracing::error; use yazi_config::LAYOUT; -use yazi_plugin::{LUA, elements::render_widgets}; +use yazi_plugin::{LUA, elements::render_once}; -pub(crate) struct Progress; +use crate::Ctx; -impl Widget for Progress { +pub(crate) struct Progress<'a> { + cx: &'a Ctx, +} + +impl<'a> Progress<'a> { + pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } } +} + +impl Widget for Progress<'_> { fn render(self, _: Rect, buf: &mut Buffer) { let mut f = || { let area = yazi_plugin::elements::Rect::from(LAYOUT.get().progress); let progress = LUA.globals().raw_get::
("Progress")?.call_method::
("use", area)?; - render_widgets(progress.call_method("redraw", ())?, buf); + render_once(progress.call_method("redraw", ())?, buf, |p| self.cx.manager.area(p)); Ok::<_, mlua::Error>(()) }; if let Err(e) = f() { diff --git a/yazi-plugin/preset/components/linemode.lua b/yazi-plugin/preset/components/linemode.lua index 6d59c423..831fafc4 100644 --- a/yazi-plugin/preset/components/linemode.lua +++ b/yazi-plugin/preset/components/linemode.lua @@ -11,7 +11,7 @@ function Linemode:new(file) return setmetatable({ _file = file }, { __index = se function Linemode:space() return ui.Line(" ") end function Linemode:solo() - local mode = cx.active.conf.linemode + local mode = cx.active.pref.linemode if mode == "none" or mode == "solo" then return ui.Line("") elseif not self[mode] then diff --git a/yazi-plugin/preset/components/root.lua b/yazi-plugin/preset/components/root.lua index d373bf42..6922c47b 100644 --- a/yazi-plugin/preset/components/root.lua +++ b/yazi-plugin/preset/components/root.lua @@ -47,17 +47,17 @@ end -- Mouse events function Root:click(event, up) - local c = ya.child_at(ui.Position { x = event.x, y = event.y }, self._children) + local c = ya.child_at(ui.Rect { x = event.x, y = event.y }, self._children) return c and c:click(event, up) end function Root:scroll(event, step) - local c = ya.child_at(ui.Position { x = event.x, y = event.y }, self._children) + local c = ya.child_at(ui.Rect { x = event.x, y = event.y }, self._children) return c and c:scroll(event, step) end function Root:touch(event, step) - local c = ya.child_at(ui.Position { x = event.x, y = event.y }, self._children) + local c = ya.child_at(ui.Rect { x = event.x, y = event.y }, self._children) return c and c:touch(event, step) end diff --git a/yazi-plugin/preset/components/status.lua b/yazi-plugin/preset/components/status.lua index ddc6cbaf..e31a49f2 100644 --- a/yazi-plugin/preset/components/status.lua +++ b/yazi-plugin/preset/components/status.lua @@ -64,7 +64,7 @@ function Status:name() return ui.Line {} end - return ui.Line(" " .. h.name) + return ui.Line(" " .. h.name:gsub("\r", "?", 1)) end function Status:perm() diff --git a/yazi-plugin/preset/components/tab.lua b/yazi-plugin/preset/components/tab.lua index 1246f110..1a1d1534 100644 --- a/yazi-plugin/preset/components/tab.lua +++ b/yazi-plugin/preset/components/tab.lua @@ -47,16 +47,16 @@ end -- Mouse events function Tab:click(event, up) - local c = ya.child_at(ui.Position { x = event.x, y = event.y }, self._children) + local c = ya.child_at(ui.Rect { x = event.x, y = event.y }, self._children) return c and c:click(event, up) end function Tab:scroll(event, step) - local c = ya.child_at(ui.Position { x = event.x, y = event.y }, self._children) + local c = ya.child_at(ui.Rect { x = event.x, y = event.y }, self._children) return c and c:scroll(event, step) end function Tab:touch(event, step) - local c = ya.child_at(ui.Position { x = event.x, y = event.y }, self._children) + local c = ya.child_at(ui.Rect { x = event.x, y = event.y }, self._children) return c and c:touch(event, step) end diff --git a/yazi-plugin/preset/plugins/code.lua b/yazi-plugin/preset/plugins/code.lua index 5efdd8a1..78f49576 100644 --- a/yazi-plugin/preset/plugins/code.lua +++ b/yazi-plugin/preset/plugins/code.lua @@ -26,4 +26,6 @@ function M:seek(units) }) end +function M:spot(args) require("file"):spot(args) end + return M diff --git a/yazi-plugin/preset/plugins/file.lua b/yazi-plugin/preset/plugins/file.lua index f271f9aa..23d8cd4f 100644 --- a/yazi-plugin/preset/plugins/file.lua +++ b/yazi-plugin/preset/plugins/file.lua @@ -16,13 +16,63 @@ end function M:seek() end -function M:spot(skip) - local rect = ui.Rect { x = 10, y = 10, w = 20, h = 20 } +-- TODO: remove this +local hovered_mime = ya.sync(function() + local h = cx.active.current.hovered + if not h then + return nil + elseif h.cha.is_dir then + return "inode/directory" + else + return h:mime() + end +end) - ya.spot_widgets(self, { - ui.Clear(rect), - ui.Table(rect), - }) +function M:spot(args) + local mime = hovered_mime() + if not mime then + return + end + + local file = args.file + local spotter = PLUGIN.spotter(file.url, mime) + local previewer = PLUGIN.previewer(file.url, mime) + local fetchers = PLUGIN.fetchers(file.url, mime) + local preloaders = PLUGIN.preloaders(file.url, mime) + + for i, v in ipairs(fetchers) do + fetchers[i] = v.cmd + end + for i, v in ipairs(preloaders) do + preloaders[i] = v.cmd + end + + local rows = {} + local row = function(key, value) + local h = type(value) == "table" and #value or 1 + rows[#rows + 1] = ui.Row({ key, value }):height(h) + end + + rows[#rows + 1] = ui.Row({ "Metadata", "" }):style(ui.Style():fg("red")) + row(" Created:", file.cha.btime and os.date("%y/%m/%d %H:%M", math.floor(file.cha.btime)) or "-") + row(" Modified:", file.cha.mtime and os.date("%y/%m/%d %H:%M", math.floor(file.cha.mtime)) or "-") + row(" Mimetype:", mime) + rows[#rows + 1] = ui.Row({ { "", "Plugins" }, "" }):height(2):style(ui.Style():fg("red")) + row(" Spotter:", spotter and spotter.cmd or "-") + row(" Previewer:", previewer and previewer.cmd or "-") + row(" Fetchers:", #fetchers ~= 0 and fetchers or "-") + row(" Preloaders:", #preloaders ~= 0 and preloaders or "-") + + ya.spot_table( + args, + ui.Table(rows) + :area(ui.Pos { "center", w = 60, h = 20 }) + :row(1) + :col(1) + :col_style(ui.Style():fg("blue")) + :cell_style(ui.Style():fg("yellow"):reverse()) + :widths { ui.Constraint.Length(14), ui.Constraint.Fill(1) } + ) end return M diff --git a/yazi-plugin/preset/plugins/folder.lua b/yazi-plugin/preset/plugins/folder.lua index ab8f96ea..a890e1ed 100644 --- a/yazi-plugin/preset/plugins/folder.lua +++ b/yazi-plugin/preset/plugins/folder.lua @@ -40,4 +40,6 @@ function M:seek(units) end end +function M:spot(args) require("file"):spot(args) end + return M diff --git a/yazi-plugin/preset/plugins/image.lua b/yazi-plugin/preset/plugins/image.lua index a5a04b41..3b398ec1 100644 --- a/yazi-plugin/preset/plugins/image.lua +++ b/yazi-plugin/preset/plugins/image.lua @@ -22,4 +22,30 @@ function M:preload() return ya.image_precache(self.file.url, cache) and 1 or 2 end +function M:spot(args) + local info = ya.image_info(args.file.url) + + local rows = {} + local row = function(key, value) + local h = type(value) == "table" and #value or 1 + rows[#rows + 1] = ui.Row({ key, value }):height(h) + end + + row("Format:", tostring(info.format)) + row("Width:", string.format("%dpx", info.w)) + row("Height:", string.format("%dpx", info.h)) + row("Color:", tostring(info.color)) + + ya.spot_table( + args, + ui.Table(rows) + :area(ui.Pos { "center", w = 60, h = 20 }) + :row(args.skip) + :col(1) + :col_style(ui.Style():fg("blue")) + :cell_style(ui.Style():fg("yellow"):reverse()) + :widths { ui.Constraint.Length(12), ui.Constraint.Fill(1) } + ) +end + return M diff --git a/yazi-plugin/preset/plugins/magick.lua b/yazi-plugin/preset/plugins/magick.lua index c432d3af..c188bbf5 100644 --- a/yazi-plugin/preset/plugins/magick.lua +++ b/yazi-plugin/preset/plugins/magick.lua @@ -41,4 +41,6 @@ function M:preload() return status and status.success and 1 or 2 end +function M:spot(args) require("file"):spot(args) end + return M diff --git a/yazi-plugin/preset/plugins/mime.lua b/yazi-plugin/preset/plugins/mime.lua index 9d6772e6..7ce3833f 100644 --- a/yazi-plugin/preset/plugins/mime.lua +++ b/yazi-plugin/preset/plugins/mime.lua @@ -9,9 +9,9 @@ local function match_mimetype(s) end end -function M:fetch() +function M:fetch(args) local urls = {} - for _, file in ipairs(self.files) do + for _, file in ipairs(args.files) do urls[#urls + 1] = tostring(file.url) end diff --git a/yazi-plugin/preset/plugins/video.lua b/yazi-plugin/preset/plugins/video.lua index 74e462ff..52a34c99 100644 --- a/yazi-plugin/preset/plugins/video.lua +++ b/yazi-plugin/preset/plugins/video.lua @@ -62,4 +62,6 @@ function M:preload() return status and status.success and 1 or 2 end +function M:spot(args) require("file"):spot(args) end + return M diff --git a/yazi-plugin/preset/ya.lua b/yazi-plugin/preset/ya.lua index 1fc04e13..03d6d492 100644 --- a/yazi-plugin/preset/ya.lua +++ b/yazi-plugin/preset/ya.lua @@ -47,9 +47,9 @@ function ya.readable_path(path) end end -function ya.child_at(position, children) +function ya.child_at(pos, children) for i = #children, 1, -1 do - if children[i]._area:contains(position) then + if children[i]._area:contains(pos) then return children[i] end end diff --git a/yazi-plugin/src/bindings/image.rs b/yazi-plugin/src/bindings/image.rs new file mode 100644 index 00000000..3d2a7b10 --- /dev/null +++ b/yazi-plugin/src/bindings/image.rs @@ -0,0 +1,83 @@ +use std::ops::Deref; + +use mlua::{MetaMethod, UserData}; + +pub struct ImageInfo(yazi_adapter::ImageInfo); + +impl Deref for ImageInfo { + type Target = yazi_adapter::ImageInfo; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl From for ImageInfo { + fn from(value: yazi_adapter::ImageInfo) -> Self { Self(value) } +} + +impl UserData for ImageInfo { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("w", |_, me| Ok(me.width)); + fields.add_field_method_get("h", |_, me| Ok(me.height)); + fields.add_field_method_get("ori", |_, me| Ok(me.orientation.map(|o| o.to_exif()))); + fields.add_field_method_get("format", |_, me| Ok(ImageFormat(me.format))); + fields.add_field_method_get("color", |_, me| Ok(ImageColor(me.color))); + } +} + +// --- ImageFormat +struct ImageFormat(yazi_adapter::ImageFormat); + +impl UserData for ImageFormat { + fn add_methods>(methods: &mut M) { + methods.add_meta_method(MetaMethod::ToString, |_, me, ()| { + use yazi_adapter::ImageFormat as F; + + Ok(match me.0 { + F::Png => "PNG", + F::Jpeg => "JPEG", + F::Gif => "GIF", + F::WebP => "WEBP", + F::Pnm => "PNM", + F::Tiff => "TIFF", + F::Tga => "TGA", + F::Dds => "DDS", + F::Bmp => "BMP", + F::Ico => "ICO", + F::Hdr => "HDR", + F::OpenExr => "OpenEXR", + F::Farbfeld => "Farbfeld", + F::Avif => "AVIF", + F::Qoi => "QOI", + F::Pcx => "PCX", + _ => "Unknown", + }) + }); + } +} + +// --- ImageColor +struct ImageColor(yazi_adapter::ImageColor); + +impl UserData for ImageColor { + fn add_methods>(methods: &mut M) { + methods.add_meta_method(MetaMethod::ToString, |_, me, ()| { + use yazi_adapter::ImageColor as C; + + Ok(match me.0 { + C::L8 => "L8", + C::La8 => "La8", + C::Rgb8 => "Rgb8", + C::Rgba8 => "Rgba8", + + C::L16 => "L16", + C::La16 => "La16", + C::Rgb16 => "Rgb16", + C::Rgba16 => "Rgba16", + + C::Rgb32F => "Rgb32F", + C::Rgba32F => "Rgba32F", + _ => "Unknown", + }) + }); + } +} diff --git a/yazi-plugin/src/bindings/mod.rs b/yazi-plugin/src/bindings/mod.rs index 39ca3b3f..7667ebe1 100644 --- a/yazi-plugin/src/bindings/mod.rs +++ b/yazi-plugin/src/bindings/mod.rs @@ -1,3 +1,3 @@ #![allow(clippy::module_inception)] -yazi_macro::mod_flat!(bindings cha icon input mouse permit position range window); +yazi_macro::mod_flat!(bindings cha icon image input mouse permit range window); diff --git a/yazi-plugin/src/bindings/position.rs b/yazi-plugin/src/bindings/position.rs deleted file mode 100644 index 95aa033c..00000000 --- a/yazi-plugin/src/bindings/position.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::{ops::Deref, str::FromStr}; - -use mlua::{ExternalResult, IntoLua, Lua}; - -pub struct Position(yazi_config::popup::Position); - -impl Deref for Position { - type Target = yazi_config::popup::Position; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl From for yazi_config::popup::Position { - fn from(value: Position) -> Self { value.0 } -} - -impl TryFrom for Position { - type Error = mlua::Error; - - fn try_from(t: mlua::Table) -> Result { - use yazi_config::popup::{Offset, Origin, Position}; - - Ok(Self(Position { - origin: Origin::from_str(&t.raw_get::(1)?.to_str()?).into_lua_err()?, - offset: Offset { - x: t.raw_get("x").unwrap_or_default(), - y: t.raw_get("y").unwrap_or_default(), - width: t.raw_get("w")?, - height: t.raw_get("h").unwrap_or(3), - }, - })) - } -} - -impl IntoLua for Position { - fn into_lua(self, lua: &Lua) -> mlua::Result { - lua - .create_table_from([ - (1.into_lua(lua)?, self.origin.to_string().into_lua(lua)?), - ("x".into_lua(lua)?, self.offset.x.into_lua(lua)?), - ("y".into_lua(lua)?, self.offset.y.into_lua(lua)?), - ("w".into_lua(lua)?, self.offset.width.into_lua(lua)?), - ("h".into_lua(lua)?, self.offset.height.into_lua(lua)?), - ])? - .into_lua(lua) - } -} diff --git a/yazi-plugin/src/bindings/window.rs b/yazi-plugin/src/bindings/window.rs index 86bf1ab5..f0688414 100644 --- a/yazi-plugin/src/bindings/window.rs +++ b/yazi-plugin/src/bindings/window.rs @@ -9,13 +9,24 @@ pub struct Window { pub height: u16, } -impl Default for Window { - fn default() -> Self { +impl Window { + pub fn get() -> Self { let ws = Dimension::available(); Self { rows: ws.rows, cols: ws.columns, width: ws.width, height: ws.height } } } +impl From for crossterm::terminal::WindowSize { + fn from(value: Window) -> Self { + crossterm::terminal::WindowSize { + rows: value.rows, + columns: value.cols, + width: value.width, + height: value.height, + } + } +} + impl UserData for Window { fn add_fields>(fields: &mut F) { fields.add_field_method_get("rows", |_, me| Ok(me.rows)); diff --git a/yazi-plugin/src/cast.rs b/yazi-plugin/src/cast.rs deleted file mode 100644 index 87a45aaf..00000000 --- a/yazi-plugin/src/cast.rs +++ /dev/null @@ -1,23 +0,0 @@ -use mlua::AnyUserData; - -use crate::elements::Renderable; - -pub fn cast_to_renderable(ud: &AnyUserData) -> Option> { - if let Ok(c) = ud.take::() { - Some(Box::new(c)) - } else if let Ok(c) = ud.take::() { - Some(Box::new(c)) - } else if let Ok(c) = ud.take::() { - Some(Box::new(c)) - } else if let Ok(c) = ud.take::() { - Some(Box::new(c)) - } else if let Ok(c) = ud.take::() { - Some(Box::new(c)) - } else if let Ok(c) = ud.take::() { - Some(Box::new(c)) - } else if let Ok(c) = ud.take::() { - Some(Box::new(c)) - } else { - None - } -} diff --git a/yazi-plugin/src/config.rs b/yazi-plugin/src/config/config.rs similarity index 54% rename from yazi-plugin/src/config.rs rename to yazi-plugin/src/config/config.rs index cad3a119..50dc3b6b 100644 --- a/yazi-plugin/src/config.rs +++ b/yazi-plugin/src/config/config.rs @@ -1,7 +1,9 @@ -use mlua::{Lua, LuaSerdeExt, SerializeOptions}; +use mlua::{IntoLua, Lua, LuaSerdeExt, SerializeOptions, Table, Value}; use yazi_boot::BOOT; use yazi_config::{MANAGER, PREVIEW, THEME}; +use super::Plugin; + const OPTIONS: SerializeOptions = SerializeOptions::new().serialize_none_to_null(false).serialize_unit_to_null(false); @@ -31,4 +33,26 @@ impl<'a> Config<'a> { self.lua.globals().raw_set("PREVIEW", self.lua.to_value_with(&*PREVIEW, OPTIONS)?)?; Ok(self) } + + pub fn install_plugin(self) -> mlua::Result { + let index = self.lua.create_function(|lua, (ts, key): (Table, mlua::String)| { + let value = match key.as_bytes().as_ref() { + b"fetchers" => Plugin::fetchers(lua)?, + b"spotter" => Plugin::spotter(lua)?, + b"preloaders" => Plugin::preloaders(lua)?, + b"previewer" => Plugin::previewer(lua)?, + _ => return Ok(Value::Nil), + } + .into_lua(lua)?; + + ts.raw_set(key, value.clone())?; + Ok(value) + })?; + + let fetcher = self.lua.create_table()?; + fetcher.set_metatable(Some(self.lua.create_table_from([("__index", index)])?)); + + self.lua.globals().raw_set("PLUGIN", fetcher)?; + Ok(self) + } } diff --git a/yazi-plugin/src/config/mod.rs b/yazi-plugin/src/config/mod.rs new file mode 100644 index 00000000..9c9b77ba --- /dev/null +++ b/yazi-plugin/src/config/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(config plugin); diff --git a/yazi-plugin/src/config/plugin.rs b/yazi-plugin/src/config/plugin.rs new file mode 100644 index 00000000..2d0cd265 --- /dev/null +++ b/yazi-plugin/src/config/plugin.rs @@ -0,0 +1,72 @@ +use mlua::{Function, Lua, UserData}; +use yazi_config::PLUGIN; + +use crate::url::UrlRef; + +pub(super) struct Plugin; + +impl Plugin { + pub(super) fn fetchers(lua: &Lua) -> mlua::Result { + lua.create_function(|lua, (url, mime): (UrlRef, mlua::String)| { + let factors = |s: &str| match s { + "mime" => !mime.as_bytes().is_empty(), + _ => false, + }; + lua.create_sequence_from(PLUGIN.fetchers(&url, &mime.to_str()?, factors).map(Fetcher)) + }) + } + + pub(super) fn spotter(lua: &Lua) -> mlua::Result { + lua.create_function(|_, (url, mime): (UrlRef, mlua::String)| { + Ok(PLUGIN.spotter(&url, &mime.to_str()?).map(Spotter)) + }) + } + + pub(super) fn preloaders(lua: &Lua) -> mlua::Result { + lua.create_function(|lua, (url, mime): (UrlRef, mlua::String)| { + lua.create_sequence_from(PLUGIN.preloaders(&url, &mime.to_str()?).map(Preloader)) + }) + } + + pub(super) fn previewer(lua: &Lua) -> mlua::Result { + lua.create_function(|_, (url, mime): (UrlRef, mlua::String)| { + Ok(PLUGIN.previewer(&url, &mime.to_str()?).map(Previewer)) + }) + } +} + +// --- Fetcher +struct Fetcher(&'static yazi_config::plugin::Fetcher); + +impl UserData for Fetcher { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("cmd", |lua, me| lua.create_string(&me.0.run.name)); + } +} + +// --- Spotter +struct Spotter(&'static yazi_config::plugin::Spotter); + +impl UserData for Spotter { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("cmd", |lua, me| lua.create_string(&me.0.run.name)); + } +} + +// --- Preloader +struct Preloader(&'static yazi_config::plugin::Preloader); + +impl UserData for Preloader { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("cmd", |lua, me| lua.create_string(&me.0.run.name)); + } +} + +// --- Previewer +struct Previewer(&'static yazi_config::plugin::Previewer); + +impl UserData for Previewer { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("cmd", |lua, me| lua.create_string(&me.0.run.name)); + } +} diff --git a/yazi-plugin/src/elements/area.rs b/yazi-plugin/src/elements/area.rs new file mode 100644 index 00000000..91db89c4 --- /dev/null +++ b/yazi-plugin/src/elements/area.rs @@ -0,0 +1,92 @@ +use std::fmt::Debug; + +use mlua::{AnyUserData, ExternalError, FromLua, IntoLua, Value}; + +use super::{Pos, Rect}; + +const EXPECTED: &str = "expected a Pos or Rect"; + +#[derive(Clone, Copy)] +pub enum Area { + Pos(Pos), + Rect(Rect), +} + +impl Default for Area { + fn default() -> Self { Self::Rect(Default::default()) } +} + +impl Area { + #[inline] + pub fn size(self) -> ratatui::layout::Size { + match self { + Self::Pos(pos) => { + ratatui::layout::Size { width: pos.offset.width, height: pos.offset.height } + } + Self::Rect(rect) => (*rect).into(), + } + } + + #[inline] + pub fn inner(self, padding: ratatui::widgets::Padding) -> Self { + match self { + Self::Pos(mut pos) => { + pos.pad += padding; + Self::Pos(pos) + } + Self::Rect(rect) => Self::Rect(rect.pad(padding.into())), + } + } + + #[inline] + pub fn transform( + self, + f: impl FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect, + ) -> ratatui::layout::Rect { + match self { + Self::Pos(pos) => *Rect(f(*pos)).pad(pos.pad), + Self::Rect(rect) => *rect, + } + } +} + +impl TryFrom for Area { + type Error = mlua::Error; + + fn try_from(value: AnyUserData) -> Result { + Ok(if let Ok(rect) = value.borrow::() { + Self::Rect(*rect) + } else if let Ok(pos) = value.borrow::() { + Self::Pos(*pos) + } else { + return Err(EXPECTED.into_lua_err()); + }) + } +} + +impl FromLua for Area { + fn from_lua(value: Value, _: &mlua::Lua) -> mlua::Result { + match value { + Value::UserData(ud) => Self::try_from(ud), + _ => Err(EXPECTED.into_lua_err()), + } + } +} + +impl IntoLua for Area { + fn into_lua(self, lua: &mlua::Lua) -> mlua::Result { + match self { + Self::Pos(pos) => pos.into_lua(lua), + Self::Rect(rect) => rect.into_lua(lua), + } + } +} + +impl Debug for Area { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Pos(pos) => write!(f, "{:?}", **pos), + Self::Rect(rect) => write!(f, "{:?}", **rect), + } + } +} diff --git a/yazi-plugin/src/elements/bar.rs b/yazi-plugin/src/elements/bar.rs index 66b4e8ca..50f3a046 100644 --- a/yazi-plugin/src/elements/bar.rs +++ b/yazi-plugin/src/elements/bar.rs @@ -1,11 +1,11 @@ -use mlua::{AnyUserData, Lua, Table, UserData}; +use mlua::{Lua, Table, UserData}; use ratatui::widgets::Borders; -use super::{Rect, Renderable}; +use super::Area; #[derive(Clone, Default)] pub struct Bar { - area: Rect, + area: Area, direction: ratatui::widgets::Borders, symbol: String, @@ -31,6 +31,50 @@ impl Bar { bar.set_metatable(Some(lua.create_table_from([("__call", new)])?)); Ok(bar) } + + pub(super) fn render( + self, + buf: &mut ratatui::buffer::Buffer, + trans: impl FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect, + ) { + let rect = self.area.transform(trans); + if rect.area() == 0 { + return; + } + + let symbol = if !self.symbol.is_empty() { + &self.symbol + } else if self.direction.intersects(Borders::TOP | Borders::BOTTOM) { + "─" + } else if self.direction.intersects(Borders::LEFT | Borders::RIGHT) { + "│" + } else { + " " + }; + + if self.direction.contains(Borders::LEFT) { + for y in rect.top()..rect.bottom() { + buf[(rect.left(), y)].set_style(self.style).set_symbol(symbol); + } + } + if self.direction.contains(Borders::TOP) { + for x in rect.left()..rect.right() { + buf[(x, rect.top())].set_style(self.style).set_symbol(symbol); + } + } + if self.direction.contains(Borders::RIGHT) { + let x = rect.right() - 1; + for y in rect.top()..rect.bottom() { + buf[(x, y)].set_style(self.style).set_symbol(symbol); + } + } + if self.direction.contains(Borders::BOTTOM) { + let y = rect.bottom() - 1; + for x in rect.left()..rect.right() { + buf[(x, y)].set_style(self.style).set_symbol(symbol); + } + } + } } impl UserData for Bar { @@ -48,48 +92,3 @@ impl UserData for Bar { }); } } - -impl Renderable for Bar { - fn area(&self) -> ratatui::layout::Rect { *self.area } - - fn render(self: Box, buf: &mut ratatui::buffer::Buffer) { - if self.area.area() == 0 { - return; - } - - let symbol = if !self.symbol.is_empty() { - &self.symbol - } else if self.direction.intersects(Borders::TOP | Borders::BOTTOM) { - "─" - } else if self.direction.intersects(Borders::LEFT | Borders::RIGHT) { - "│" - } else { - " " - }; - - if self.direction.contains(Borders::LEFT) { - for y in self.area.top()..self.area.bottom() { - buf[(self.area.left(), y)].set_style(self.style).set_symbol(symbol); - } - } - if self.direction.contains(Borders::TOP) { - for x in self.area.left()..self.area.right() { - buf[(x, self.area.top())].set_style(self.style).set_symbol(symbol); - } - } - if self.direction.contains(Borders::RIGHT) { - let x = self.area.right() - 1; - for y in self.area.top()..self.area.bottom() { - buf[(x, y)].set_style(self.style).set_symbol(symbol); - } - } - if self.direction.contains(Borders::BOTTOM) { - let y = self.area.bottom() - 1; - for x in self.area.left()..self.area.right() { - buf[(x, y)].set_style(self.style).set_symbol(symbol); - } - } - } - - fn clone_render(&self, buf: &mut ratatui::buffer::Buffer) { Box::new(self.clone()).render(buf) } -} diff --git a/yazi-plugin/src/elements/border.rs b/yazi-plugin/src/elements/border.rs index 9debe008..0eea9f33 100644 --- a/yazi-plugin/src/elements/border.rs +++ b/yazi-plugin/src/elements/border.rs @@ -1,7 +1,8 @@ -use mlua::{AnyUserData, Lua, Table, UserData}; +use mlua::{Lua, Table, UserData, Value}; use ratatui::widgets::{Borders, Widget}; -use super::{Rect, Renderable}; +use super::Area; +use crate::elements::Line; // Type const PLAIN: u8 = 0; @@ -13,11 +14,13 @@ const QUADRANT_OUTSIDE: u8 = 5; #[derive(Clone, Default)] pub struct Border { - area: Rect, + pub(crate) area: Area, - position: ratatui::widgets::Borders, - type_: ratatui::widgets::BorderType, - style: ratatui::style::Style, + pub(crate) position: ratatui::widgets::Borders, + pub(crate) type_: ratatui::widgets::BorderType, + pub(crate) style: ratatui::style::Style, + + pub(crate) titles: Vec<(ratatui::widgets::block::Position, ratatui::text::Line<'static>)>, } impl Border { @@ -49,6 +52,26 @@ impl Border { border.set_metatable(Some(lua.create_table_from([("__call", new)])?)); Ok(border) } + + pub(super) fn render( + self, + buf: &mut ratatui::buffer::Buffer, + trans: impl FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect, + ) { + let mut block = ratatui::widgets::Block::default() + .borders(self.position) + .border_type(self.type_) + .border_style(self.style); + + for title in self.titles { + block = match title { + (ratatui::widgets::block::Position::Top, line) => block.title(line), + (ratatui::widgets::block::Position::Bottom, line) => block.title(line), + }; + } + + block.render(self.area.transform(trans), buf); + } } impl UserData for Border { @@ -56,10 +79,6 @@ impl UserData for Border { crate::impl_area_method!(methods); crate::impl_style_method!(methods, style); - methods.add_function_mut("position", |_, (ud, position): (AnyUserData, u8)| { - ud.borrow_mut::()?.position = ratatui::widgets::Borders::from_bits_truncate(position); - Ok(ud) - }); methods.add_function_mut("type", |_, (ud, value): (AnyUserData, u8)| { ud.borrow_mut::()?.type_ = match value { ROUNDED => ratatui::widgets::BorderType::Rounded, @@ -71,19 +90,22 @@ impl UserData for Border { }; Ok(ud) }); + methods.add_function_mut( + "title", + |_, (ud, line, position): (AnyUserData, Value, Option)| { + let position = if position == Some(Borders::BOTTOM.bits()) { + ratatui::widgets::block::Position::Bottom + } else { + ratatui::widgets::block::Position::Top + }; + + ud.borrow_mut::()?.titles.push((position, Line::try_from(line)?.0)); + Ok(ud) + }, + ); + methods.add_function_mut("position", |_, (ud, position): (AnyUserData, u8)| { + ud.borrow_mut::()?.position = ratatui::widgets::Borders::from_bits_truncate(position); + Ok(ud) + }); } } - -impl Renderable for Border { - fn area(&self) -> ratatui::layout::Rect { *self.area } - - fn render(self: Box, buf: &mut ratatui::buffer::Buffer) { - ratatui::widgets::Block::default() - .borders(self.position) - .border_type(self.type_) - .border_style(self.style) - .render(*self.area, buf); - } - - fn clone_render(&self, buf: &mut ratatui::buffer::Buffer) { Box::new(self.clone()).render(buf); } -} diff --git a/yazi-plugin/src/elements/cell.rs b/yazi-plugin/src/elements/cell.rs new file mode 100644 index 00000000..591b7633 --- /dev/null +++ b/yazi-plugin/src/elements/cell.rs @@ -0,0 +1,20 @@ +use mlua::{ExternalError, FromLua}; + +use super::Text; + +const EXPECTED: &str = "expected a table of strings, Texts, Lines or Spans"; + +#[derive(Clone)] +pub struct Cell { + pub(super) text: ratatui::text::Text<'static>, +} + +impl From for ratatui::widgets::Cell<'static> { + fn from(value: Cell) -> Self { Self::new(value.text) } +} + +impl FromLua for Cell { + fn from_lua(value: mlua::Value, _: &mlua::Lua) -> mlua::Result { + Ok(Self { text: Text::try_from(value).map_err(|_| EXPECTED.into_lua_err())?.inner }) + } +} diff --git a/yazi-plugin/src/elements/clear.rs b/yazi-plugin/src/elements/clear.rs index a8ed37c6..709ba812 100644 --- a/yazi-plugin/src/elements/clear.rs +++ b/yazi-plugin/src/elements/clear.rs @@ -3,24 +3,32 @@ use std::sync::atomic::{AtomicBool, Ordering}; use mlua::{Lua, Table, UserData}; use yazi_adapter::ADAPTOR; -use super::{Rect, Renderable}; +use super::Area; pub static COLLISION: AtomicBool = AtomicBool::new(false); #[derive(Clone, Copy, Default)] pub struct Clear { - pub area: Rect, + pub area: Area, } impl Clear { pub fn compose(lua: &Lua) -> mlua::Result
{ - let new = lua.create_function(|_, (_, area): (Table, Rect)| Ok(Clear { area }))?; + let new = lua.create_function(|_, (_, area): (Table, Area)| Ok(Clear { area }))?; let clear = lua.create_table()?; clear.set_metatable(Some(lua.create_table_from([("__call", new)])?)); Ok(clear) } + + pub(super) fn render( + self, + buf: &mut ratatui::buffer::Buffer, + trans: impl FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect, + ) { + ::render(Default::default(), self.area.transform(trans), buf); + } } impl ratatui::widgets::Widget for Clear { @@ -44,16 +52,6 @@ impl ratatui::widgets::Widget for Clear { } } -impl Renderable for Clear { - fn area(&self) -> ratatui::layout::Rect { *self.area } - - fn render(self: Box, buf: &mut ratatui::buffer::Buffer) { - ::render(Default::default(), *self.area, buf); - } - - fn clone_render(&self, buf: &mut ratatui::buffer::Buffer) { Box::new(*self).render(buf); } -} - impl UserData for Clear { fn add_methods>(methods: &mut M) { crate::impl_area_method!(methods); diff --git a/yazi-plugin/src/elements/elements.rs b/yazi-plugin/src/elements/elements.rs index 3d5feefb..028b10d5 100644 --- a/yazi-plugin/src/elements/elements.rs +++ b/yazi-plugin/src/elements/elements.rs @@ -1,7 +1,7 @@ use mlua::{AnyUserData, IntoLua, Lua, Table, Value}; use tracing::error; -use crate::cast_to_renderable; +use super::Renderable; pub fn compose(lua: &Lua) -> mlua::Result
{ let index = lua.create_function(|lua, (ts, key): (Table, mlua::String)| { @@ -14,14 +14,16 @@ pub fn compose(lua: &Lua) -> mlua::Result
{ b"Layout" => super::Layout::compose(lua)?, b"Line" => super::Line::compose(lua)?, b"List" => super::List::compose(lua)?, - b"Padding" => super::Padding::compose(lua)?, + b"Pad" => super::Pad::compose(lua, true)?, + // TODO: deprecate this + b"Padding" => super::Pad::compose(lua, false)?, b"Paragraph" => super::Paragraph::compose(lua)?, - b"Position" => super::Position::compose(lua)?, + b"Pos" => super::Pos::compose(lua)?, b"Rect" => super::Rect::compose(lua)?, + b"Row" => super::Row::compose(lua)?, b"Span" => super::Span::compose(lua)?, b"Style" => super::Style::compose(lua)?, b"Table" => super::Table::compose(lua)?, - b"TableRow" => super::TableRow::compose(lua)?, b"Text" => super::Text::compose(lua)?, _ => return Ok(Value::Nil), } @@ -37,24 +39,19 @@ pub fn compose(lua: &Lua) -> mlua::Result
{ Ok(ui) } -pub trait Renderable { - fn area(&self) -> ratatui::layout::Rect; - - fn render(self: Box, buf: &mut ratatui::buffer::Buffer); - - fn clone_render(&self, buf: &mut ratatui::buffer::Buffer); -} - -pub fn render_widgets(widgets: Table, buf: &mut ratatui::buffer::Buffer) { +pub fn render_once(widgets: Table, buf: &mut ratatui::buffer::Buffer, trans: F) +where + F: Fn(yazi_config::popup::Position) -> ratatui::layout::Rect + Copy, +{ for widget in widgets.sequence_values::() { let Ok(widget) = widget else { error!("Failed to convert to renderable UserData: {}", widget.unwrap_err()); continue; }; - match cast_to_renderable(&widget) { - Some(w) => w.render(buf), - None => error!("Only the UserData of renderable element is accepted: {widget:#?}"), + match Renderable::try_from(widget) { + Ok(w) => w.render(buf, trans), + Err(e) => error!("{e}"), } } } diff --git a/yazi-plugin/src/elements/gauge.rs b/yazi-plugin/src/elements/gauge.rs index a69658d9..f43bacb8 100644 --- a/yazi-plugin/src/elements/gauge.rs +++ b/yazi-plugin/src/elements/gauge.rs @@ -1,12 +1,12 @@ -use mlua::{AnyUserData, ExternalError, Lua, Table, UserData, UserDataMethods, Value}; +use mlua::{ExternalError, Lua, Table, UserData, UserDataMethods, Value}; use ratatui::widgets::Widget; -use super::{Rect, Renderable, Span}; +use super::{Area, Span}; use crate::elements::Style; #[derive(Clone, Default)] pub struct Gauge { - area: Rect, + area: Area, ratio: f64, label: Option>, @@ -23,6 +23,23 @@ impl Gauge { Ok(gauge) } + + pub(super) fn render( + self, + buf: &mut ratatui::buffer::Buffer, + trans: impl FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect, + ) { + let mut gauge = ratatui::widgets::Gauge::default() + .ratio(self.ratio) + .style(self.style) + .gauge_style(self.gauge_style); + + if let Some(s) = self.label { + gauge = gauge.label(s) + } + + gauge.render(self.area.transform(trans), buf); + } } impl UserData for Gauge { @@ -59,22 +76,3 @@ impl UserData for Gauge { }); } } - -impl Renderable for Gauge { - fn area(&self) -> ratatui::layout::Rect { *self.area } - - fn render(self: Box, buf: &mut ratatui::buffer::Buffer) { - let mut gauge = ratatui::widgets::Gauge::default() - .ratio(self.ratio) - .style(self.style) - .gauge_style(self.gauge_style); - - if let Some(s) = self.label { - gauge = gauge.label(s) - } - - gauge.render(*self.area, buf); - } - - fn clone_render(&self, buf: &mut ratatui::buffer::Buffer) { Box::new(self.clone()).render(buf) } -} diff --git a/yazi-plugin/src/elements/line.rs b/yazi-plugin/src/elements/line.rs index 4c4ebf84..01282d75 100644 --- a/yazi-plugin/src/elements/line.rs +++ b/yazi-plugin/src/elements/line.rs @@ -10,7 +10,7 @@ const LEFT: u8 = 0; const CENTER: u8 = 1; const RIGHT: u8 = 2; -const EXPECTED: &str = "expected a string, ui.Span, ui.Line, or a table of them"; +const EXPECTED: &str = "expected a string, Span, Line, or a table of them"; #[derive(Clone, FromLua)] pub struct Line(pub(super) ratatui::text::Line<'static>); diff --git a/yazi-plugin/src/elements/list.rs b/yazi-plugin/src/elements/list.rs index 213a84f7..92ca0bd2 100644 --- a/yazi-plugin/src/elements/list.rs +++ b/yazi-plugin/src/elements/list.rs @@ -1,12 +1,14 @@ use mlua::{ExternalError, Lua, Table, UserData, Value}; use ratatui::widgets::Widget; -use super::{Rect, Renderable, Text}; +use super::{Area, Text}; + +const EXPECTED: &str = "expected a table of strings, Texts, Lines or Spans"; // --- List #[derive(Clone, Default)] pub struct List { - area: Rect, + area: Area, inner: ratatui::widgets::List<'static>, } @@ -16,10 +18,7 @@ impl List { let new = lua.create_function(|_, (_, seq): (Table, Table)| { let mut items = Vec::with_capacity(seq.raw_len()); for v in seq.sequence_values::() { - match v? { - Value::Table(_) => Err("Nested table not supported".into_lua_err())?, - v => items.push(Text::try_from(v)?), - } + items.push(Text::try_from(v?).map_err(|_| EXPECTED.into_lua_err())?); } Ok(Self { inner: ratatui::widgets::List::new(items), ..Default::default() }) @@ -30,6 +29,14 @@ impl List { Ok(list) } + + pub(super) fn render( + self, + buf: &mut ratatui::buffer::Buffer, + trans: impl FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect, + ) { + self.inner.render(self.area.transform(trans), buf); + } } impl UserData for List { @@ -37,13 +44,3 @@ impl UserData for List { crate::impl_area_method!(methods); } } - -impl Renderable for List { - fn area(&self) -> ratatui::layout::Rect { *self.area } - - fn render(self: Box, buf: &mut ratatui::buffer::Buffer) { - self.inner.render(*self.area, buf); - } - - fn clone_render(&self, buf: &mut ratatui::buffer::Buffer) { Box::new(self.clone()).render(buf) } -} diff --git a/yazi-plugin/src/elements/mod.rs b/yazi-plugin/src/elements/mod.rs index 59439aa7..0c0a6e2c 100644 --- a/yazi-plugin/src/elements/mod.rs +++ b/yazi-plugin/src/elements/mod.rs @@ -1,3 +1,3 @@ #![allow(clippy::module_inception)] -yazi_macro::mod_flat!(bar border clear constraint elements gauge layout line list padding paragraph position rect span style table text); +yazi_macro::mod_flat!(area bar border cell clear constraint elements gauge layout line list pad paragraph pos rect renderable row span style table text); diff --git a/yazi-plugin/src/elements/padding.rs b/yazi-plugin/src/elements/pad.rs similarity index 54% rename from yazi-plugin/src/elements/padding.rs rename to yazi-plugin/src/elements/pad.rs index a2016495..112aab8f 100644 --- a/yazi-plugin/src/elements/padding.rs +++ b/yazi-plugin/src/elements/pad.rs @@ -1,24 +1,33 @@ -use std::ops::Deref; +use std::ops::{Add, AddAssign, Deref}; use mlua::{FromLua, Lua, Table, UserData}; -#[derive(Clone, Copy, FromLua)] -pub struct Padding(ratatui::widgets::Padding); +#[derive(Clone, Copy, Default, FromLua)] +pub struct Pad(ratatui::widgets::Padding); -impl Deref for Padding { +impl Deref for Pad { type Target = ratatui::widgets::Padding; fn deref(&self) -> &Self::Target { &self.0 } } -impl Padding { - pub fn compose(lua: &Lua) -> mlua::Result
{ - let new = +impl From for Pad { + fn from(pad: ratatui::widgets::Padding) -> Self { Self(pad) } +} + +impl Pad { + pub fn compose(lua: &Lua, v4: bool) -> mlua::Result
{ + let new = if v4 { + lua.create_function(|_, (_, top, right, bottom, left): (Table, u16, u16, u16, u16)| { + Ok(Self(ratatui::widgets::Padding::new(left, right, top, bottom))) + })? + } else { lua.create_function(|_, (_, left, right, top, bottom): (Table, u16, u16, u16, u16)| { Ok(Self(ratatui::widgets::Padding::new(left, right, top, bottom))) - })?; + })? + }; - let padding = lua.create_table_from([ + let pad = lua.create_table_from([ ( "left", lua.create_function(|_, left: u16| Ok(Self(ratatui::widgets::Padding::left(left))))?, @@ -42,12 +51,12 @@ impl Padding { ), ])?; - padding.set_metatable(Some(lua.create_table_from([("__call", new)])?)); - Ok(padding) + pad.set_metatable(Some(lua.create_table_from([("__call", new)])?)); + Ok(pad) } } -impl UserData for Padding { +impl UserData for Pad { fn add_fields>(fields: &mut F) { fields.add_field_method_get("left", |_, me| Ok(me.left)); fields.add_field_method_get("right", |_, me| Ok(me.right)); @@ -55,3 +64,20 @@ impl UserData for Padding { fields.add_field_method_get("bottom", |_, me| Ok(me.bottom)); } } + +impl Add for Pad { + type Output = Self; + + fn add(self, rhs: ratatui::widgets::Padding) -> Self::Output { + Self(ratatui::widgets::Padding::new( + self.left.saturating_add(rhs.left), + self.right.saturating_add(rhs.right), + self.top.saturating_add(rhs.top), + self.bottom.saturating_add(rhs.bottom), + )) + } +} + +impl AddAssign for Pad { + fn add_assign(&mut self, rhs: ratatui::widgets::Padding) { *self = *self + rhs; } +} diff --git a/yazi-plugin/src/elements/pos.rs b/yazi-plugin/src/elements/pos.rs new file mode 100644 index 00000000..1e299db6 --- /dev/null +++ b/yazi-plugin/src/elements/pos.rs @@ -0,0 +1,80 @@ +use std::{ops::Deref, str::FromStr}; + +use mlua::{AnyUserData, ExternalResult, Lua, Table, UserData}; + +use super::Pad; + +#[derive(Clone, Copy, Default)] +pub struct Pos { + inner: yazi_config::popup::Position, + + pub(super) pad: Pad, +} + +impl Deref for Pos { + type Target = yazi_config::popup::Position; + + fn deref(&self) -> &Self::Target { &self.inner } +} + +impl From for Pos { + fn from(value: yazi_config::popup::Position) -> Self { + Self { inner: value, ..Default::default() } + } +} + +impl From for yazi_config::popup::Position { + fn from(value: Pos) -> Self { value.inner } +} + +impl TryFrom for Pos { + type Error = mlua::Error; + + fn try_from(t: mlua::Table) -> Result { + use yazi_config::popup::{Offset, Origin, Position}; + + Ok(Self::from(Position { + origin: Origin::from_str(&t.raw_get::(1)?.to_str()?).into_lua_err()?, + offset: Offset { + x: t.raw_get("x").unwrap_or_default(), + y: t.raw_get("y").unwrap_or_default(), + width: t.raw_get("w").unwrap_or_default(), + height: t.raw_get("h").unwrap_or_default(), + }, + })) + } +} + +impl Pos { + pub fn compose(lua: &Lua) -> mlua::Result
{ + let new = lua.create_function(|_, (_, t): (Table, Table)| Self::try_from(t))?; + + let position = lua.create_table()?; + position.set_metatable(Some(lua.create_table_from([("__call", new)])?)); + + Ok(position) + } + + pub fn new_input(t: mlua::Table) -> mlua::Result { + let mut p = Self::try_from(t)?; + p.inner.offset.height = 3; + Ok(p) + } +} + +impl UserData for Pos { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get(1, |_, me| Ok(me.origin.to_string())); + fields.add_field_method_get("x", |_, me| Ok(me.offset.x)); + fields.add_field_method_get("y", |_, me| Ok(me.offset.y)); + fields.add_field_method_get("w", |_, me| Ok(me.offset.width)); + fields.add_field_method_get("h", |_, me| Ok(me.offset.height)); + } + + fn add_methods>(methods: &mut M) { + methods.add_function_mut("pad", |_, (ud, pad): (AnyUserData, Pad)| { + ud.borrow_mut::()?.pad = pad; + Ok(ud) + }); + } +} diff --git a/yazi-plugin/src/elements/position.rs b/yazi-plugin/src/elements/position.rs deleted file mode 100644 index 6838c522..00000000 --- a/yazi-plugin/src/elements/position.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::ops::Deref; - -use mlua::{FromLua, Lua, Table, UserData, UserDataFields}; - -#[derive(Clone, Copy, FromLua)] -pub struct Position(ratatui::layout::Position); - -impl Deref for Position { - type Target = ratatui::layout::Position; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl From for Position { - fn from(rect: ratatui::layout::Rect) -> Self { Self(rect.as_position()) } -} - -impl Position { - pub fn compose(lua: &Lua) -> mlua::Result
{ - let new = lua.create_function(|_, (_, args): (Table, Table)| { - Ok(Self(ratatui::layout::Position { x: args.raw_get("x")?, y: args.raw_get("y")? })) - })?; - - let position = lua.create_table_from([("default", Self(Default::default()))])?; - - position.set_metatable(Some(lua.create_table_from([("__call", new)])?)); - Ok(position) - } -} - -impl UserData for Position { - fn add_fields>(fields: &mut F) { - fields.add_field_method_get("x", |_, me| Ok(me.x)); - fields.add_field_method_get("y", |_, me| Ok(me.y)); - } -} diff --git a/yazi-plugin/src/elements/rect.rs b/yazi-plugin/src/elements/rect.rs index 0cbb495f..a94fe709 100644 --- a/yazi-plugin/src/elements/rect.rs +++ b/yazi-plugin/src/elements/rect.rs @@ -2,10 +2,10 @@ use std::ops::Deref; use mlua::{FromLua, Lua, Table, UserData}; -use super::{Padding, Position}; +use super::Pad; #[derive(Clone, Copy, Default, FromLua)] -pub struct Rect(ratatui::layout::Rect); +pub struct Rect(pub(super) ratatui::layout::Rect); impl Deref for Rect { type Target = ratatui::layout::Rect; @@ -27,10 +27,10 @@ impl Rect { pub fn compose(lua: &Lua) -> mlua::Result
{ let new = lua.create_function(|_, (_, args): (Table, Table)| { Ok(Self(ratatui::layout::Rect { - x: args.raw_get("x")?, - y: args.raw_get("y")?, - width: args.raw_get("w")?, - height: args.raw_get("h")?, + x: args.raw_get("x").unwrap_or_default(), + y: args.raw_get("y").unwrap_or_default(), + width: args.raw_get("w").unwrap_or_default(), + height: args.raw_get("h").unwrap_or_default(), })) })?; @@ -39,6 +39,16 @@ impl Rect { rect.set_metatable(Some(lua.create_table_from([("__call", new)])?)); Ok(rect) } + + pub(super) fn pad(self, pad: Pad) -> Self { + let mut r = *self; + r.x = r.x.saturating_add(pad.left); + r.y = r.y.saturating_add(pad.top); + + r.width = r.width.saturating_sub(pad.left + pad.right); + r.height = r.height.saturating_sub(pad.top + pad.bottom); + Self(r) + } } impl UserData for Rect { @@ -55,16 +65,9 @@ impl UserData for Rect { } fn add_methods>(methods: &mut M) { - methods.add_method("padding", |_, me, padding: Padding| { - let mut r = **me; - r.x = r.x.saturating_add(padding.left); - r.y = r.y.saturating_add(padding.top); - - r.width = r.width.saturating_sub(padding.left + padding.right); - r.height = r.height.saturating_sub(padding.top + padding.bottom); - Ok(Self(r)) - }); - methods.add_method("position", |_, me, ()| Ok(Position::from(**me))); - methods.add_method("contains", |_, me, position: Position| Ok(me.contains(*position))); + methods.add_method("pad", |_, me, pad: Pad| Ok(me.pad(pad))); + // TODO: deprecate this + methods.add_method("padding", |_, me, pad: Pad| Ok(me.pad(pad))); + methods.add_method("contains", |_, me, Rect(rect)| Ok(me.contains(rect.into()))); } } diff --git a/yazi-plugin/src/elements/renderable.rs b/yazi-plugin/src/elements/renderable.rs new file mode 100644 index 00000000..ead60605 --- /dev/null +++ b/yazi-plugin/src/elements/renderable.rs @@ -0,0 +1,58 @@ +use mlua::{AnyUserData, ExternalError}; + +use super::{Bar, Border, Clear, Gauge, List, Table, Text}; + +#[derive(Clone)] +pub enum Renderable { + Text(Text), + List(List), + Bar(Bar), + Clear(Clear), + Border(Border), + Gauge(Gauge), + Table(Table), +} + +impl Renderable { + pub fn render( + self, + buf: &mut ratatui::buffer::Buffer, + trans: impl Fn(yazi_config::popup::Position) -> ratatui::layout::Rect, + ) { + match self { + Self::Text(text) => text.render(buf, trans), + Self::List(list) => list.render(buf, trans), + Self::Bar(bar) => bar.render(buf, trans), + Self::Clear(clear) => clear.render(buf, trans), + Self::Border(border) => border.render(buf, trans), + Self::Gauge(gauge) => gauge.render(buf, trans), + Self::Table(table) => table.render(buf, trans), + } + } +} + +impl TryFrom for Renderable { + type Error = mlua::Error; + + fn try_from(ud: AnyUserData) -> Result { + Ok(if let Ok(c) = ud.take::() { + Self::Text(c) + } else if let Ok(c) = ud.take::() { + Self::List(c) + } else if let Ok(c) = ud.take::() { + Self::Bar(c) + } else if let Ok(c) = ud.take::() { + Self::Clear(c) + } else if let Ok(c) = ud.take::() { + Self::Border(c) + } else if let Ok(c) = ud.take::() { + Self::Gauge(c) + } else if let Ok(c) = ud.take::() { + Self::Table(c) + } else { + return Err( + format!("expected a UserData of renderable element, not: {ud:#?}").into_lua_err(), + ); + }) + } +} diff --git a/yazi-plugin/src/elements/row.rs b/yazi-plugin/src/elements/row.rs new file mode 100644 index 00000000..f9d23194 --- /dev/null +++ b/yazi-plugin/src/elements/row.rs @@ -0,0 +1,54 @@ +use mlua::{AnyUserData, FromLua, Lua, Table, UserData}; + +use super::Cell; + +#[derive(Clone, Default, FromLua)] +pub struct Row { + pub(super) cells: Vec, + height: u16, + top_margin: u16, + bottom_margin: u16, + style: ratatui::style::Style, +} + +impl Row { + pub fn compose(lua: &Lua) -> mlua::Result
{ + let new = lua.create_function(|_, (_, cells): (Table, Vec)| { + Ok(Self { cells, ..Default::default() }) + })?; + + let row = lua.create_table()?; + row.set_metatable(Some(lua.create_table_from([("__call", new)])?)); + + Ok(row) + } +} + +impl From for ratatui::widgets::Row<'static> { + fn from(value: Row) -> Self { + Self::new(value.cells) + .height(value.height.max(1)) + .top_margin(value.top_margin) + .bottom_margin(value.bottom_margin) + .style(value.style) + } +} + +impl UserData for Row { + fn add_methods>(methods: &mut M) { + crate::impl_style_method!(methods, style); + + methods.add_function_mut("height", |_, (ud, value): (AnyUserData, u16)| { + ud.borrow_mut::()?.height = value; + Ok(ud) + }); + methods.add_function_mut("margin_t", |_, (ud, value): (AnyUserData, u16)| { + ud.borrow_mut::()?.top_margin = value; + Ok(ud) + }); + methods.add_function_mut("margin_b", |_, (ud, value): (AnyUserData, u16)| { + ud.borrow_mut::()?.bottom_margin = value; + Ok(ud) + }); + } +} diff --git a/yazi-plugin/src/elements/span.rs b/yazi-plugin/src/elements/span.rs index 11810f3b..147de23e 100644 --- a/yazi-plugin/src/elements/span.rs +++ b/yazi-plugin/src/elements/span.rs @@ -1,7 +1,7 @@ use mlua::{ExternalError, FromLua, Lua, Table, UserData, UserDataMethods, Value}; use unicode_width::UnicodeWidthChar; -const EXPECTED: &str = "expected a string or ui.Span"; +const EXPECTED: &str = "expected a string or Span"; #[derive(Clone, FromLua)] pub struct Span(pub(super) ratatui::text::Span<'static>); diff --git a/yazi-plugin/src/elements/table.rs b/yazi-plugin/src/elements/table.rs index c8b08d9a..7a145b06 100644 --- a/yazi-plugin/src/elements/table.rs +++ b/yazi-plugin/src/elements/table.rs @@ -1,33 +1,38 @@ -use mlua::{AnyUserData, FromLua, Lua, UserData}; +use mlua::{AnyUserData, Lua, UserData, Value}; use ratatui::widgets::StatefulWidget; -use super::{Rect, Renderable, Text}; -use crate::elements::Constraint; +use super::{Area, Row}; +use crate::elements::{Constraint, Style}; // --- Table #[derive(Clone, Default)] pub struct Table { - area: Rect, + pub(crate) area: Area, - rows: Vec>, - header: Option>, - footer: Option>, - widths: Vec, - column_spacing: u16, - block: Option>, - style: ratatui::style::Style, - row_highlight_style: ratatui::style::Style, - highlight_symbol: ratatui::text::Text<'static>, - highlight_spacing: ratatui::widgets::HighlightSpacing, - flex: ratatui::layout::Flex, + rows: Vec, + header: Option>, + footer: Option>, + widths: Vec, + column_spacing: u16, + block: Option>, // TODO + + style: ratatui::style::Style, + row_highlight_style: ratatui::style::Style, + column_highlight_style: ratatui::style::Style, + cell_highlight_style: ratatui::style::Style, + + highlight_symbol: ratatui::text::Text<'static>, // TODO + highlight_spacing: ratatui::widgets::HighlightSpacing, // TODO + + flex: ratatui::layout::Flex, state: ratatui::widgets::TableState, } impl Table { pub fn compose(lua: &Lua) -> mlua::Result { - let new = lua.create_function(|_, (_, area, rows): (mlua::Table, Rect, Vec)| { - Ok(Self { area, rows: rows.into_iter().map(Into::into).collect(), ..Default::default() }) + let new = lua.create_function(|_, (_, rows): (mlua::Table, Vec)| { + Ok(Self { rows, ..Default::default() }) })?; let table = lua.create_table()?; @@ -35,35 +40,24 @@ impl Table { Ok(table) } -} -impl UserData for Table { - fn add_methods>(methods: &mut M) { - crate::impl_area_method!(methods); - - methods.add_function_mut("header", |_, (ud, row): (AnyUserData, TableRow)| { - ud.borrow_mut::()?.header = Some(row.into()); - Ok(ud) - }); - methods.add_function_mut("footer", |_, (ud, row): (AnyUserData, TableRow)| { - ud.borrow_mut::()?.footer = Some(row.into()); - Ok(ud) - }); - methods.add_function_mut("widths", |_, (ud, widths): (AnyUserData, Vec)| { - ud.borrow_mut::()?.widths = widths.into_iter().map(Into::into).collect(); - Ok(ud) - }); + pub fn selected_cell(&self) -> Option<&ratatui::text::Text> { + let row = &self.rows[self.selected()?]; + let col = self.state.selected_column()?; + if row.cells.is_empty() { None } else { Some(&row.cells[col.min(row.cells.len() - 1)].text) } } -} -impl Renderable for Table { - fn area(&self) -> ratatui::layout::Rect { *self.area } - - fn render(mut self: Box, buf: &mut ratatui::buffer::Buffer) { + pub(super) fn render( + mut self, + buf: &mut ratatui::buffer::Buffer, + trans: impl FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect, + ) { let mut table = ratatui::widgets::Table::new(self.rows, self.widths) .column_spacing(self.column_spacing) .style(self.style) .row_highlight_style(self.row_highlight_style) + .column_highlight_style(self.column_highlight_style) + .cell_highlight_style(self.cell_highlight_style) .highlight_symbol(self.highlight_symbol) .highlight_spacing(self.highlight_spacing) .flex(self.flex); @@ -78,59 +72,70 @@ impl Renderable for Table { table = table.block(block); } - table.render(*self.area, buf, &mut self.state); + table.render(self.area.transform(trans), buf, &mut self.state); } - fn clone_render(&self, buf: &mut ratatui::buffer::Buffer) { Box::new(self.clone()).render(buf) } -} + pub(crate) fn select(&mut self, idx: Option) { + self + .state + .select(idx.map(|i| if self.rows.is_empty() { 0 } else { i.min(self.rows.len() - 1) })); + } -// --- TableRow -#[derive(Clone, Default, FromLua)] -pub struct TableRow { - cells: Vec>, - height: u16, - top_margin: u16, - bottom_margin: u16, - style: ratatui::style::Style, -} - -impl TableRow { - pub fn compose(lua: &Lua) -> mlua::Result { - let new = lua.create_function(|_, (_, cols): (mlua::Table, Vec)| { - Ok(Self { cells: cols.into_iter().map(Into::into).collect(), ..Default::default() }) - })?; - - let row = lua.create_table()?; - row.set_metatable(Some(lua.create_table_from([("__call", new)])?)); - - Ok(row) + pub(crate) fn selected(&self) -> Option { + if self.rows.is_empty() { None } else { Some(self.state.selected()?.min(self.rows.len() - 1)) } } } -impl From for ratatui::widgets::Row<'static> { - fn from(value: TableRow) -> Self { - Self::new(value.cells) - .height(value.height) - .top_margin(value.top_margin) - .bottom_margin(value.bottom_margin) - .style(value.style) - } +impl TryFrom for Table { + type Error = mlua::Error; + + fn try_from(value: AnyUserData) -> Result { value.take() } } -impl UserData for TableRow { +impl UserData for Table { fn add_methods>(methods: &mut M) { - crate::impl_style_method!(methods, style); + crate::impl_area_method!(methods); - methods.add_function_mut("height", |_, (ud, value): (AnyUserData, u16)| { - ud.borrow_mut::()?.height = value; + methods.add_function_mut("header", |_, (ud, row): (AnyUserData, Row)| { + ud.borrow_mut::()?.header = Some(row.into()); Ok(ud) }); - methods.add_function_mut("margin_t", |_, (ud, value): (AnyUserData, u16)| { - ud.borrow_mut::()?.top_margin = value; + methods.add_function_mut("footer", |_, (ud, row): (AnyUserData, Row)| { + ud.borrow_mut::()?.footer = Some(row.into()); Ok(ud) }); - methods.add_function_mut("margin_b", |_, (ud, value): (AnyUserData, u16)| { - ud.borrow_mut::()?.bottom_margin = value; + methods.add_function_mut("widths", |_, (ud, widths): (AnyUserData, Vec)| { + ud.borrow_mut::()?.widths = widths.into_iter().map(Into::into).collect(); + Ok(ud) + }); + methods.add_function_mut("spacing", |_, (ud, spacing): (AnyUserData, u16)| { + ud.borrow_mut::()?.column_spacing = spacing; + Ok(ud) + }); + + methods.add_function_mut("row", |_, (ud, idx): (AnyUserData, Option)| { + ud.borrow_mut::()?.state.select(idx); + Ok(ud) + }); + methods.add_function_mut("col", |_, (ud, idx): (AnyUserData, Option)| { + ud.borrow_mut::()?.state.select_column(idx); + Ok(ud) + }); + + methods.add_function_mut("style", |_, (ud, value): (AnyUserData, Value)| { + ud.borrow_mut::()?.style = Style::try_from(value)?.0; + Ok(ud) + }); + methods.add_function_mut("row_style", |_, (ud, value): (AnyUserData, Value)| { + ud.borrow_mut::()?.row_highlight_style = Style::try_from(value)?.0; + Ok(ud) + }); + methods.add_function_mut("col_style", |_, (ud, value): (AnyUserData, Value)| { + ud.borrow_mut::()?.column_highlight_style = Style::try_from(value)?.0; + Ok(ud) + }); + methods.add_function_mut("cell_style", |_, (ud, value): (AnyUserData, Value)| { + ud.borrow_mut::()?.cell_highlight_style = Style::try_from(value)?.0; Ok(ud) }); } diff --git a/yazi-plugin/src/elements/text.rs b/yazi-plugin/src/elements/text.rs index 753f2ce7..a195062d 100644 --- a/yazi-plugin/src/elements/text.rs +++ b/yazi-plugin/src/elements/text.rs @@ -1,8 +1,8 @@ use ansi_to_tui::IntoText; -use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, Table, UserData, Value}; +use mlua::{ExternalError, ExternalResult, FromLua, IntoLua, Lua, Table, UserData, Value}; use ratatui::widgets::Widget; -use super::{Line, Rect, Renderable, Span}; +use super::{Area, Line, Span}; // Alignment pub(super) const LEFT: u8 = 0; @@ -14,11 +14,11 @@ pub const WRAP_NO: u8 = 0; pub const WRAP: u8 = 1; pub const WRAP_TRIM: u8 = 2; -const EXPECTED: &str = "expected a string, ui.Line, ui.Span or a table of them"; +const EXPECTED: &str = "expected a string, Line, Span, or a table of them"; #[derive(Clone, Default, FromLua)] pub struct Text { - pub area: Rect, + pub area: Area, // TODO: block pub inner: ratatui::text::Text<'static>, @@ -49,6 +49,16 @@ impl Text { text.set_metatable(Some(lua.create_table_from([("__call", new)])?)); Ok(text) } + + pub(super) fn render( + self, + buf: &mut ratatui::buffer::Buffer, + trans: impl Fn(yazi_config::popup::Position) -> ratatui::layout::Rect, + ) { + let rect = self.area.transform(trans); + let p: ratatui::widgets::Paragraph = self.into(); + p.render(rect, buf); + } } impl TryFrom for Text { @@ -63,6 +73,8 @@ impl TryFrom for Text { line.into() } else if let Ok(Span(span)) = ud.take() { span.into() + } else if let Ok(text) = ud.take() { + return Ok(text); } else { Err(EXPECTED.into_lua_err())? } @@ -131,24 +143,12 @@ impl UserData for Text { methods.add_function_mut("wrap", |_, (ud, wrap): (AnyUserData, u8)| { ud.borrow_mut::()?.wrap = match wrap { w @ (WRAP | WRAP_TRIM | WRAP_NO) => w, - _ => return Err("expected a WRAP or WRAP_TRIM or WRAP_OFF".into_lua_err()), + _ => return Err("expected a WRAP, WRAP_TRIM or WRAP_NO".into_lua_err()), }; Ok(ud) }); methods.add_method("max_width", |_, me, ()| { - Ok(me.inner.lines.iter().take(me.area.height as usize).map(|l| l.width()).max()) + Ok(me.inner.lines.iter().take(me.area.size().height as usize).map(|l| l.width()).max()) }); } } - -impl Renderable for Text { - fn area(&self) -> ratatui::layout::Rect { *self.area } - - fn render(self: Box, buf: &mut ratatui::buffer::Buffer) { - let area = *self.area; - let p: ratatui::widgets::Paragraph = (*self).into(); - p.render(area, buf); - } - - fn clone_render(&self, buf: &mut ratatui::buffer::Buffer) { Box::new(self.clone()).render(buf) } -} diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index 47267723..75d37937 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, io::Cursor, mem, path::{Path, PathBuf}, sync::OnceLock}; use anyhow::{Result, anyhow}; -use ratatui::{layout::Rect, text::{Line, Span, Text}}; +use ratatui::{layout::Size, text::{Line, Span, Text}}; use syntect::{LoadingError, dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}}; use tokio::{fs::File, io::{AsyncBufReadExt, BufReader}}; use yazi_config::{PREVIEW, THEME, preview::PreviewWrap}; @@ -36,14 +36,14 @@ impl Highlighter { #[inline] pub fn abort() { INCR.next(); } - pub async fn highlight(&self, skip: usize, area: Rect) -> Result, PeekError> { + pub async fn highlight(&self, skip: usize, size: Size) -> Result, PeekError> { let mut reader = BufReader::new(File::open(&self.path).await?); let syntax = Self::find_syntax(&self.path).await; let mut plain = syntax.is_err(); let mut before = Vec::with_capacity(if plain { 0 } else { skip }); - let mut after = Vec::with_capacity(area.height as _); + let mut after = Vec::with_capacity(size.height as _); let mut i = 0; let mut buf = vec![]; @@ -67,24 +67,24 @@ impl Highlighter { i += if i >= skip { buf.iter_mut().for_each(Self::carriage_return_to_line_feed); after.push(String::from_utf8_lossy(&buf).into_owned()); - Self::line_height(&after[after.len() - 1], area.width) + Self::line_height(&after[after.len() - 1], size.width) } else if !plain { before.push(String::from_utf8_lossy(&buf).into_owned()); - Self::line_height(&before[before.len() - 1], area.width) + Self::line_height(&before[before.len() - 1], size.width) } else if PREVIEW.wrap == PreviewWrap::Yes { - Self::line_height(&String::from_utf8_lossy(&buf), area.width) + Self::line_height(&String::from_utf8_lossy(&buf), size.width) } else { 1 }; buf.clear(); - if i > skip + area.height as usize { + if i > skip + size.height as usize { break; } } - if skip > 0 && i < skip + area.height as usize { - return Err(PeekError::Exceed(i.saturating_sub(area.height as _))); + if skip > 0 && i < skip + size.height as usize { + return Err(PeekError::Exceed(i.saturating_sub(size.height as _))); } Ok(if plain { diff --git a/yazi-plugin/src/isolate/fetch.rs b/yazi-plugin/src/isolate/fetch.rs index 2b665241..def75f1b 100644 --- a/yazi-plugin/src/isolate/fetch.rs +++ b/yazi-plugin/src/isolate/fetch.rs @@ -1,4 +1,4 @@ -use mlua::{ExternalError, ExternalResult, ObjectLike, Table}; +use mlua::{ExternalError, ExternalResult, IntoLua, ObjectLike, Table}; use tokio::runtime::Handle; use yazi_config::LAYOUT; @@ -20,16 +20,20 @@ pub async fn fetch(name: &str, files: Vec) -> mlua::Resul return Err("unloaded plugin".into_lua_err()); }; - let files = files.into_iter().filter_map(|f| File::cast(&lua, f).ok()).collect::>(); - if files.is_empty() { - return Err("no files".into_lua_err()); + let files = + lua.create_sequence_from(files.into_iter().filter_map(|f| File::cast(&lua, f).ok()))?; + + if files.raw_len() == 0 { + return Ok(1); } - plugin.raw_set("skip", 0)?; - plugin.raw_set("area", Rect::from(LAYOUT.get().preview))?; - plugin.raw_set("files", files)?; - - Handle::current().block_on(plugin.call_async_method("fetch", ())) + Handle::current().block_on(plugin.call_async_method( + "fetch", + lua.create_table_from([ + ("area", Rect::from(LAYOUT.get().preview).into_lua(&lua)?), + ("files", files.into_lua(&lua)?), + ])?, + )) }) .await .into_lua_err()? diff --git a/yazi-plugin/src/isolate/isolate.rs b/yazi-plugin/src/isolate/isolate.rs index 7d831a5a..c4a902d6 100644 --- a/yazi-plugin/src/isolate/isolate.rs +++ b/yazi-plugin/src/isolate/isolate.rs @@ -6,7 +6,7 @@ use crate::runtime::Runtime; pub fn slim_lua(name: &str) -> mlua::Result { let lua = Lua::new(); lua.set_named_registry_value("rt", Runtime::new(name))?; - crate::Config::new(&lua).install_preview()?; + crate::config::Config::new(&lua).install_preview()?.install_plugin()?; // Base let globals = lua.globals(); diff --git a/yazi-plugin/src/isolate/mod.rs b/yazi-plugin/src/isolate/mod.rs index b8b126b6..3fb51d26 100644 --- a/yazi-plugin/src/isolate/mod.rs +++ b/yazi-plugin/src/isolate/mod.rs @@ -1,3 +1,3 @@ #![allow(clippy::module_inception)] -yazi_macro::mod_flat!(entry fetch isolate peek preload seek); +yazi_macro::mod_flat!(entry fetch isolate peek preload seek spot); diff --git a/yazi-plugin/src/isolate/peek.rs b/yazi-plugin/src/isolate/peek.rs index 3435f43d..6fd878d4 100644 --- a/yazi-plugin/src/isolate/peek.rs +++ b/yazi-plugin/src/isolate/peek.rs @@ -18,9 +18,9 @@ pub fn peek( skip: usize, ) -> CancellationToken { let ct = CancellationToken::new(); + let (ct1, ct2) = (ct.clone(), ct.clone()); let name = cmd.name.to_owned(); - let (ct1, ct2) = (ct.clone(), ct.clone()); tokio::task::spawn_blocking(move || { let future = async { LOADER.ensure(&name).await.into_lua_err()?; @@ -43,10 +43,12 @@ pub fn peek( return Err("unloaded plugin".into_lua_err()); }; plugin.raw_set("file", File::cast(&lua, file)?)?; - plugin.raw_set("_mime", mime)?; + plugin.raw_set("mime", mime)?; plugin.raw_set("skip", skip)?; plugin.raw_set("area", Rect::from(LAYOUT.get().preview))?; - plugin.raw_set("window", Window::default())?; + // TODO: remove this as it's not safe in async context, + // there may be race conditions between the `window` and `area` + plugin.raw_set("window", Window::get())?; if ct2.is_cancelled() { Ok(()) } else { plugin.call_async_method("peek", ()).await } }; @@ -71,10 +73,12 @@ pub fn peek( pub fn peek_sync(cmd: &Cmd, file: yazi_shared::fs::File, mime: Cow<'static, str>, skip: usize) { let cb: PluginCallback = Box::new(move |_, plugin| { plugin.raw_set("file", File::cast(&LUA, file)?)?; - plugin.raw_set("_mime", mime)?; + plugin.raw_set("mime", mime)?; plugin.raw_set("skip", skip)?; plugin.raw_set("area", Rect::from(LAYOUT.get().preview))?; - plugin.raw_set("window", Window::default())?; + // TODO: remove this as it's not safe in async context, + // there may be race conditions between the `window` and `area` + plugin.raw_set("window", Window::get())?; plugin.call_method("peek", ()) }); diff --git a/yazi-plugin/src/isolate/spot.rs b/yazi-plugin/src/isolate/spot.rs new file mode 100644 index 00000000..c1690c17 --- /dev/null +++ b/yazi-plugin/src/isolate/spot.rs @@ -0,0 +1,67 @@ +use std::borrow::Cow; + +use mlua::{ExternalError, ExternalResult, HookTriggers, IntoLua, ObjectLike, Table, VmState}; +use tokio::{runtime::Handle, select}; +use tokio_util::sync::CancellationToken; +use tracing::error; +use yazi_shared::event::Cmd; + +use super::slim_lua; +use crate::{bindings::Cast, file::File, loader::LOADER}; + +pub fn spot( + cmd: &Cmd, + file: yazi_shared::fs::File, + mime: Cow<'static, str>, + skip: usize, +) -> CancellationToken { + let ct = CancellationToken::new(); + let (ct1, ct2) = (ct.clone(), ct.clone()); + + let name = cmd.name.to_owned(); + tokio::task::spawn_blocking(move || { + let future = async { + LOADER.ensure(&name).await.into_lua_err()?; + + let lua = slim_lua(&name)?; + lua.set_hook( + HookTriggers::new().on_calls().on_returns().every_nth_instruction(2000), + move |_, _| { + if ct1.is_cancelled() { + Err("Spot task cancelled".into_lua_err()) + } else { + Ok(VmState::Continue) + } + }, + ); + + let plugin: Table = if let Some(b) = LOADER.read().get(&name) { + lua.load(b.as_bytes()).set_name(name).call(())? + } else { + return Err("unloaded plugin".into_lua_err()); + }; + + let args = lua.create_table_from([ + ("file", File::cast(&lua, file)?.into_lua(&lua)?), + ("mime", mime.into_lua(&lua)?), + ("skip", skip.into_lua(&lua)?), + ])?; + if ct2.is_cancelled() { Ok(()) } else { plugin.call_async_method("spot", args).await } + }; + + let result = Handle::current().block_on(async { + select! { + _ = ct2.cancelled() => Ok(()), + r = future => r, + } + }); + + if let Err(e) = result { + if !e.to_string().contains("Spot task cancelled") { + error!("{e:?}"); + } + } + }); + + ct +} diff --git a/yazi-plugin/src/lib.rs b/yazi-plugin/src/lib.rs index e66d1d20..d610c2e9 100644 --- a/yazi-plugin/src/lib.rs +++ b/yazi-plugin/src/lib.rs @@ -6,7 +6,7 @@ yazi_macro::mod_pub!( bindings, elements, external, file, fs, isolate, loader, process, pubsub, url, utils ); -yazi_macro::mod_flat!(cast clipboard config lua runtime); +yazi_macro::mod_flat!(clipboard config lua runtime); pub fn init() -> anyhow::Result<()> { CLIPBOARD.with(<_>::default); diff --git a/yazi-plugin/src/macros.rs b/yazi-plugin/src/macros.rs index 1be887fd..99a32f77 100644 --- a/yazi-plugin/src/macros.rs +++ b/yazi-plugin/src/macros.rs @@ -11,12 +11,11 @@ macro_rules! impl_style_method { #[macro_export] macro_rules! impl_area_method { ($methods:ident) => { - use mlua::IntoLua; - use $crate::elements::Rect; + use mlua::{AnyUserData, IntoLua}; - $methods.add_function_mut("area", |lua, (ud, area): (mlua::AnyUserData, Option)| { - if let Some(r) = area { - ud.borrow_mut::()?.area = r; + $methods.add_function_mut("area", |lua, (ud, area): (AnyUserData, Option)| { + if let Some(v) = area { + ud.borrow_mut::()?.area = $crate::elements::Area::try_from(v)?; ud.into_lua(lua) } else { ud.borrow::()?.area.into_lua(lua) diff --git a/yazi-plugin/src/utils/image.rs b/yazi-plugin/src/utils/image.rs index 20196492..4b80dc59 100644 --- a/yazi-plugin/src/utils/image.rs +++ b/yazi-plugin/src/utils/image.rs @@ -2,9 +2,19 @@ use mlua::{Function, IntoLua, Lua, Value}; use yazi_adapter::{ADAPTOR, Image}; use super::Utils; -use crate::{elements::Rect, url::UrlRef}; +use crate::{bindings::ImageInfo, elements::Rect, url::UrlRef}; impl Utils { + pub(super) fn image_info(lua: &Lua) -> mlua::Result { + lua.create_async_function(|lua, url: UrlRef| async move { + if let Ok(info) = yazi_adapter::ImageInfo::new(&url).await { + ImageInfo::from(info).into_lua(&lua) + } else { + Value::Nil.into_lua(&lua) + } + }) + } + pub(super) fn image_show(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (url, rect): (UrlRef, Rect)| async move { if let Ok(area) = ADAPTOR.image_show(&url, *rect).await { diff --git a/yazi-plugin/src/utils/layer.rs b/yazi-plugin/src/utils/layer.rs index 46d63aef..816341fc 100644 --- a/yazi-plugin/src/utils/layer.rs +++ b/yazi-plugin/src/utils/layer.rs @@ -9,7 +9,7 @@ use yazi_proxy::{AppProxy, InputProxy}; use yazi_shared::{Debounce, Layer, event::Cmd}; use super::Utils; -use crate::bindings::{InputRx, Position}; +use crate::{bindings::InputRx, elements::Pos}; impl Utils { pub(super) fn which(lua: &Lua) -> mlua::Result { @@ -46,7 +46,7 @@ impl Utils { title: t.raw_get("title")?, value: t.raw_get("value").unwrap_or_default(), cursor: None, // TODO - position: Position::try_from(t.raw_get::
("position")?)?.into(), + position: Pos::new_input(t.raw_get::
("position")?)?.into(), realtime, completion: false, highlight: false, diff --git a/yazi-plugin/src/utils/mod.rs b/yazi-plugin/src/utils/mod.rs index 984b8af3..e2afc13a 100644 --- a/yazi-plugin/src/utils/mod.rs +++ b/yazi-plugin/src/utils/mod.rs @@ -1,5 +1,5 @@ #![allow(clippy::module_inception)] yazi_macro::mod_flat!( - app cache call image layer log preview sync target text time user utils + app cache call image layer log preview spot sync target text time user utils ); diff --git a/yazi-plugin/src/utils/preview.rs b/yazi-plugin/src/utils/preview.rs index 8d90db08..e62a46e1 100644 --- a/yazi-plugin/src/utils/preview.rs +++ b/yazi-plugin/src/utils/preview.rs @@ -4,31 +4,31 @@ use yazi_macro::emit; use yazi_shared::{Layer, errors::PeekError, event::Cmd}; use super::Utils; -use crate::{bindings::Window, cast_to_renderable, elements::{Rect, Renderable, Text, WRAP, WRAP_NO}, external::Highlighter, file::FileRef}; +use crate::{elements::{Area, Rect, Renderable, Text, WRAP, WRAP_NO}, external::Highlighter, file::FileRef}; pub struct PreviewLock { pub url: yazi_shared::fs::Url, pub cha: yazi_shared::fs::Cha, pub mime: String, - pub skip: usize, - pub window: Window, - pub data: Vec>, + pub skip: usize, + pub area: Rect, + pub data: Vec, } impl TryFrom
for PreviewLock { type Error = mlua::Error; fn try_from(t: Table) -> Result { - let file: FileRef = t.raw_get("file")?; // TODO: use `_file` instead of `file` + let file: FileRef = t.raw_get("file")?; Ok(Self { url: file.url_owned(), cha: file.cha, - mime: t.raw_get("_mime")?, + mime: t.raw_get("mime")?, - skip: t.raw_get("skip")?, - window: t.raw_get("window")?, - data: Default::default(), + skip: t.raw_get("skip")?, + area: t.raw_get("area")?, + data: Default::default(), }) } } @@ -36,10 +36,10 @@ impl TryFrom
for PreviewLock { impl Utils { pub(super) fn preview_code(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, t: Table| async move { - let area: Rect = t.raw_get("area")?; + let area: Area = t.raw_get("area")?; let mut lock = PreviewLock::try_from(t)?; - let inner = match Highlighter::new(&lock.url).highlight(lock.skip, *area).await { + let inner = match Highlighter::new(&lock.url).highlight(lock.skip, area.size()).await { Ok(text) => text, Err(e @ PeekError::Exceed(max)) => return (e.to_string(), max).into_lua_multi(&lua), Err(e @ PeekError::Unexpected(_)) => { @@ -47,7 +47,7 @@ impl Utils { } }; - lock.data = vec![Box::new(Text { + lock.data = vec![Renderable::Text(Text { area, inner, wrap: if PREVIEW.wrap == PreviewWrap::Yes { WRAP } else { WRAP_NO }, @@ -61,7 +61,7 @@ impl Utils { pub(super) fn preview_widgets(lua: &Lua) -> mlua::Result { lua.create_async_function(|_, (t, widgets): (Table, Vec)| async move { let mut lock = PreviewLock::try_from(t)?; - lock.data = widgets.into_iter().filter_map(|ud| cast_to_renderable(&ud)).collect(); + lock.data = widgets.into_iter().map(Renderable::try_from).collect::>()?; emit!(Call(Cmd::new("update_peeked").with_any("lock", lock), Layer::Manager)); Ok(()) diff --git a/yazi-plugin/src/utils/spot.rs b/yazi-plugin/src/utils/spot.rs new file mode 100644 index 00000000..40b0fafa --- /dev/null +++ b/yazi-plugin/src/utils/spot.rs @@ -0,0 +1,96 @@ +use mlua::{AnyUserData, Function, Lua, Table}; +use ratatui::style::Stylize; +use yazi_macro::emit; +use yazi_shared::{Layer, event::Cmd}; + +use super::Utils; +use crate::{elements::Renderable, file::FileRef}; + +pub struct SpotLock { + pub url: yazi_shared::fs::Url, + pub cha: yazi_shared::fs::Cha, + pub mime: String, + + pub skip: usize, + pub data: Vec, +} + +impl TryFrom
for SpotLock { + type Error = mlua::Error; + + fn try_from(t: Table) -> Result { + let file: FileRef = t.raw_get("file")?; + Ok(Self { + url: file.url_owned(), + cha: file.cha, + mime: t.raw_get("mime")?, + + skip: t.raw_get("skip")?, + data: Default::default(), + }) + } +} + +impl SpotLock { + pub fn select(&mut self, idx: Option) { + if let Some(t) = self.table_mut() { + t.select(idx); + } + } + + pub fn selected(&self) -> Option { self.table()?.selected() } + + pub fn table(&self) -> Option<&crate::elements::Table> { + self.data.iter().rev().find_map(|r| match r { + Renderable::Table(t) => Some(t), + _ => None, + }) + } + + pub fn table_mut(&mut self) -> Option<&mut crate::elements::Table> { + self.data.iter_mut().rev().find_map(|r| match r { + Renderable::Table(t) => Some(t), + _ => None, + }) + } +} + +impl Utils { + pub(super) fn spot_table(lua: &Lua) -> mlua::Result { + lua.create_function(|_, (t, table): (mlua::Table, AnyUserData)| { + let mut lock = SpotLock::try_from(t)?; + let mut table = crate::elements::Table::try_from(table)?; + + let area = table.area; + table.area = area.inner(ratatui::widgets::Padding::uniform(1)); + + lock.data = vec![ + Renderable::Clear(crate::elements::Clear { area }), + Renderable::Border(crate::elements::Border { + area, + position: ratatui::widgets::Borders::ALL, + type_: ratatui::widgets::BorderType::Rounded, + style: ratatui::style::Style::default().blue(), + titles: vec![( + ratatui::widgets::block::Position::Top, + ratatui::text::Line::raw(lock.url.name().to_string_lossy().into_owned()).centered(), + )], + }), + Renderable::Table(table), + ]; + emit!(Call(Cmd::new("update_spotted").with_any("lock", lock), Layer::Manager)); + + Ok(()) + }) + } + + pub(super) fn spot_widgets(lua: &Lua) -> mlua::Result { + lua.create_function(|_, (t, widgets): (Table, Vec)| { + let mut lock = SpotLock::try_from(t)?; + lock.data = widgets.into_iter().map(Renderable::try_from).collect::>()?; + + emit!(Call(Cmd::new("update_spotted").with_any("lock", lock), Layer::Manager)); + Ok(()) + }) + } +} diff --git a/yazi-plugin/src/utils/utils.rs b/yazi-plugin/src/utils/utils.rs index c3f4251e..c41ded1a 100644 --- a/yazi-plugin/src/utils/utils.rs +++ b/yazi-plugin/src/utils/utils.rs @@ -19,6 +19,7 @@ pub fn compose(lua: &Lua, isolate: bool) -> mlua::Result
{ b"input_emit" => Utils::input_emit(lua)?, // Image + b"image_info" => Utils::image_info(lua)?, b"image_show" => Utils::image_show(lua)?, b"image_precache" => Utils::image_precache(lua)?, @@ -35,6 +36,10 @@ pub fn compose(lua: &Lua, isolate: bool) -> mlua::Result
{ b"preview_code" => Utils::preview_code(lua)?, b"preview_widgets" => Utils::preview_widgets(lua)?, + // Spot + b"spot_table" => Utils::spot_table(lua)?, + b"spot_widgets" => Utils::spot_widgets(lua)?, + // Sync b"sync" => Utils::sync(lua, isolate)?, diff --git a/yazi-proxy/src/manager.rs b/yazi-proxy/src/manager.rs index 1a4337f2..c5e1aad5 100644 --- a/yazi-proxy/src/manager.rs +++ b/yazi-proxy/src/manager.rs @@ -6,6 +6,11 @@ use crate::options::OpenDoOpt; pub struct ManagerProxy; impl ManagerProxy { + #[inline] + pub fn spot(skip: Option) { + emit!(Call(Cmd::new("spot").with_opt("skip", skip), Layer::Manager)); + } + #[inline] pub fn peek(force: bool) { emit!(Call(Cmd::new("peek").with_bool("force", force), Layer::Manager)); diff --git a/yazi-proxy/src/tab.rs b/yazi-proxy/src/tab.rs index 18a4a7da..b47b05ce 100644 --- a/yazi-proxy/src/tab.rs +++ b/yazi-proxy/src/tab.rs @@ -16,6 +16,11 @@ impl TabProxy { emit!(Call(Cmd::args("reveal", &[target]), Layer::Manager)); } + #[inline] + pub fn arrow(step: isize) { + emit!(Call(Cmd::args("arrow", &[step]), Layer::Manager)); + } + #[inline] pub fn search_do(opt: SearchOpt) { emit!(Call( diff --git a/yazi-scheduler/src/prework/prework.rs b/yazi-scheduler/src/prework/prework.rs index 2d93b9cc..5cb74924 100644 --- a/yazi-scheduler/src/prework/prework.rs +++ b/yazi-scheduler/src/prework/prework.rs @@ -31,7 +31,7 @@ impl Prework { match op { PreworkOp::Fetch(task) => { let urls: Vec<_> = task.targets.iter().map(|f| f.url_owned()).collect(); - let result = isolate::fetch(&task.plugin.name, task.targets).await; + let result = isolate::fetch(task.plugin.name, task.targets).await; if let Err(e) = result { self.fail( task.id, @@ -62,7 +62,7 @@ impl Prework { } PreworkOp::Load(task) => { let url = task.target.url_owned(); - let result = isolate::preload(&task.plugin.name, task.target).await; + let result = isolate::preload(task.plugin.name, task.target).await; if let Err(e) = result { self.fail( task.id, diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 075e517b..e2c6dd3b 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -223,7 +223,7 @@ impl Scheduler { self.plugin.macro_(PluginOpEntry { id, name, args }).ok(); } - pub fn fetch_paged(&self, fetcher: &Fetcher, targets: Vec) { + pub fn fetch_paged(&self, fetcher: &'static Fetcher, targets: Vec) { let id = self.ongoing.lock().add( TaskKind::Preload, format!("Run fetcher `{}` with {} target(s)", fetcher.run.name, targets.len()), @@ -236,7 +236,7 @@ impl Scheduler { }); } - pub fn preload_paged(&self, preloader: &Preloader, target: &yazi_shared::fs::File) { + pub fn preload_paged(&self, preloader: &'static Preloader, target: &yazi_shared::fs::File) { let id = self.ongoing.lock().add(TaskKind::Preload, format!("Run preloader `{}`", preloader.run.name)); diff --git a/yazi-shared/src/event/cmd.rs b/yazi-shared/src/event/cmd.rs index 181962c5..307da56a 100644 --- a/yazi-shared/src/event/cmd.rs +++ b/yazi-shared/src/event/cmd.rs @@ -34,6 +34,14 @@ impl Cmd { self } + #[inline] + pub fn with_opt(mut self, name: impl ToString, value: Option) -> Self { + if let Some(v) = value { + self.args.insert(name.to_string(), Data::String(v.to_string())); + } + self + } + #[inline] pub fn with_bool(mut self, name: impl ToString, state: bool) -> Self { self.args.insert(name.to_string(), Data::Boolean(state)); diff --git a/yazi-shared/src/layer.rs b/yazi-shared/src/layer.rs index 44df60a9..06b0689a 100644 --- a/yazi-shared/src/layer.rs +++ b/yazi-shared/src/layer.rs @@ -8,6 +8,7 @@ pub enum Layer { App, Manager, Tasks, + Spot, Pick, Input, Confirm, @@ -22,6 +23,7 @@ impl Display for Layer { Self::App => "app", Self::Manager => "manager", Self::Tasks => "tasks", + Self::Spot => "spot", Self::Pick => "pick", Self::Input => "input", Self::Confirm => "confirm", @@ -40,6 +42,7 @@ impl FromStr for Layer { "app" => Self::App, "manager" => Self::Manager, "tasks" => Self::Tasks, + "spot" => Self::Spot, "pick" => Self::Pick, "input" => Self::Input, "confirm" => Self::Confirm,