diff --git a/Cargo.lock b/Cargo.lock index c544e00d..434126b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,12 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "async-channel" version = "1.9.0" @@ -911,6 +917,12 @@ dependencies = [ "tiff", ] +[[package]] +name = "imagesize" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" + [[package]] name = "indexmap" version = "2.1.0" @@ -1158,7 +1170,9 @@ checksum = "7c81f8ac20188feb5461a73eabb22a34dd09d6d58513535eb587e46bff6ba250" dependencies = [ "bstr", "erased-serde", + "futures-util", "mlua-sys", + "mlua_derive", "num-traits", "once_cell", "rustc-hash", @@ -1179,6 +1193,21 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "mlua_derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f359220f24e6452dd82a3f50d7242d4aab822b5594798048e953d7a9e0314c6" +dependencies = [ + "itertools", + "once_cell", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 2.0.40", +] + [[package]] name = "nom" version = "7.1.3" @@ -1981,6 +2010,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.8" @@ -2575,6 +2617,7 @@ dependencies = [ "base64", "color_quant", "image", + "imagesize", "ratatui", "tokio", "tracing", @@ -2587,6 +2630,7 @@ name = "yazi-config" version = "0.1.5" dependencies = [ "anyhow", + "arc-swap", "clap", "clap_complete", "clap_complete_fig", @@ -2626,11 +2670,12 @@ dependencies = [ "syntect", "tokio", "tokio-stream", + "tokio-util", "tracing", "unicode-width", "yazi-adaptor", "yazi-config", - "yazi-prebuild", + "yazi-plugin", "yazi-scheduler", "yazi-shared", ] @@ -2646,6 +2691,7 @@ dependencies = [ "fdlimit", "futures", "libc", + "mlua", "ratatui", "signal-hook-tokio", "tokio", @@ -2657,6 +2703,7 @@ dependencies = [ "yazi-config", "yazi-core", "yazi-plugin", + "yazi-scheduler", "yazi-shared", ] @@ -2666,12 +2713,22 @@ version = "0.1.5" dependencies = [ "ansi-to-tui", "anyhow", + "futures", + "libc", + "md-5", "mlua", + "parking_lot", "ratatui", + "serde", + "serde_json", + "syntect", + "tokio", + "tokio-util", "tracing", "unicode-width", + "yazi-adaptor", "yazi-config", - "yazi-core", + "yazi-prebuild", "yazi-shared", ] @@ -2690,16 +2747,15 @@ dependencies = [ "base64", "crossterm", "futures", - "libc", "parking_lot", "regex", - "serde", - "serde_json", "tokio", + "tokio-stream", "tracing", "trash", "yazi-adaptor", "yazi-config", + "yazi-plugin", "yazi-shared", ] @@ -2716,6 +2772,7 @@ dependencies = [ "percent-encoding", "ratatui", "regex", + "serde", "tokio", ] diff --git a/cspell.json b/cspell.json index 9c481edc..1a34a0b9 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"language":"en","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"],"version":"0.2"} +{"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"],"language":"en","version":"0.2"} diff --git a/yazi-adaptor/Cargo.toml b/yazi-adaptor/Cargo.toml index 46d9057d..9ff5bc47 100644 --- a/yazi-adaptor/Cargo.toml +++ b/yazi-adaptor/Cargo.toml @@ -17,6 +17,7 @@ anyhow = "^1" base64 = "^0" color_quant = "^1" image = "^0" +imagesize = "^0" ratatui = "^0" tokio = { version = "^1", features = [ "parking_lot", "io-util", "process" ] } diff --git a/yazi-adaptor/src/adaptor.rs b/yazi-adaptor/src/adaptor.rs index 4d16fa30..2b9f0a66 100644 --- a/yazi-adaptor/src/adaptor.rs +++ b/yazi-adaptor/src/adaptor.rs @@ -1,20 +1,15 @@ -use std::{env, path::{Path, PathBuf}, sync::atomic::{AtomicBool, Ordering}}; +use std::{env, path::Path, sync::atomic::{AtomicBool, Ordering}}; use anyhow::{anyhow, Result}; use ratatui::prelude::Rect; -use tokio::{fs, sync::mpsc::UnboundedSender}; use tracing::warn; -use yazi_config::PREVIEW; -use yazi_shared::{env_exists, RoCell}; +use yazi_shared::env_exists; use super::{Iterm2, Kitty, KittyOld}; use crate::{ueberzug::Ueberzug, Sixel, TMUX}; static IMAGE_SHOWN: AtomicBool = AtomicBool::new(false); -#[allow(clippy::type_complexity)] -static UEBERZUG: RoCell>>> = RoCell::new(); - #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Adaptor { Kitty, @@ -164,16 +159,9 @@ impl ToString for Adaptor { } impl Adaptor { - pub(super) fn start(self) { - UEBERZUG.init(if self.needs_ueberzug() { Ueberzug::start(self).ok() } else { None }); - } - - pub async fn image_show(self, mut path: &Path, rect: Rect) -> Result<()> { - let cache = PREVIEW.cache(path, 0); - if fs::symlink_metadata(&cache).await.is_ok() { - path = cache.as_path(); - } + pub(super) fn start(self) { Ueberzug::start(self); } + pub async fn image_show(self, path: &Path, rect: Rect) -> Result<(u32, u32)> { self.image_hide(rect).ok(); IMAGE_SHOWN.store(true, Ordering::Relaxed); @@ -182,9 +170,7 @@ impl Adaptor { Self::KittyOld => KittyOld::image_show(path, rect).await, Self::Iterm2 => Iterm2::image_show(path, rect).await, Self::Sixel => Sixel::image_show(path, rect).await, - _ => Ok(if let Some(tx) = &*UEBERZUG { - tx.send(Some((path.to_path_buf(), rect)))?; - }), + _ => Ueberzug::image_show(path, rect).await, } } @@ -198,9 +184,7 @@ impl Adaptor { Self::Iterm2 => Iterm2::image_hide(rect), Self::KittyOld => KittyOld::image_hide(), Self::Sixel => Sixel::image_hide(rect), - _ => Ok(if let Some(tx) = &*UEBERZUG { - tx.send(None)?; - }), + _ => Ueberzug::image_hide(rect), } } diff --git a/yazi-adaptor/src/image.rs b/yazi-adaptor/src/image.rs index 746cb471..d5366353 100644 --- a/yazi-adaptor/src/image.rs +++ b/yazi-adaptor/src/image.rs @@ -2,51 +2,13 @@ use std::{fs::File, io::BufReader, path::{Path, PathBuf}}; use anyhow::Result; use image::{imageops::FilterType, io::Limits, DynamicImage, ImageFormat}; +use ratatui::layout::Rect; use yazi_config::{PREVIEW, TASKS}; use yazi_shared::term::Term; pub struct Image; impl Image { - fn set_limits(mut r: image::io::Reader>) -> image::io::Reader> { - let mut limits = Limits::no_limits(); - if TASKS.image_alloc > 0 { - limits.max_alloc = Some(TASKS.image_alloc as u64); - } - if TASKS.image_bound[0] > 0 { - limits.max_image_width = Some(TASKS.image_bound[0] as u32); - } - if TASKS.image_bound[1] > 0 { - limits.max_image_height = Some(TASKS.image_bound[1] as u32); - } - r.limits(limits); - r - } - - pub(super) async fn downscale(path: &Path, size: (u16, u16)) -> Result { - let (w, h) = Term::ratio() - .map(|(w, h)| { - let (w, h) = ((size.0 as f64 * w) as u32, (size.1 as f64 * h) as u32); - (w.min(PREVIEW.max_width), h.min(PREVIEW.max_height)) - }) - .unwrap_or((PREVIEW.max_width, PREVIEW.max_height)); - - let path = path.to_owned(); - let img = tokio::task::spawn_blocking(move || { - Self::set_limits(image::io::Reader::open(path)?.with_guessed_format()?).decode() - }) - .await??; - - tokio::task::spawn_blocking(move || { - Ok(if img.width() > w || img.height() > h { - img.resize(w, h, FilterType::Triangle) - } else { - img - }) - }) - .await? - } - pub async fn precache(path: &Path, cache: PathBuf) -> Result<()> { let path = path.to_owned(); let mut img = tokio::task::spawn_blocking(move || { @@ -69,21 +31,45 @@ impl Image { .await? } - pub async fn precache_vec(bin: Vec, cache: PathBuf) -> Result<()> { - let mut img = tokio::task::spawn_blocking(move || image::load_from_memory(&bin)).await??; + pub(super) async fn downscale(path: &Path, rect: Rect) -> Result { + let path = path.to_owned(); + let img = tokio::task::spawn_blocking(move || { + Self::set_limits(image::io::Reader::open(path)?.with_guessed_format()?).decode() + }) + .await??; + let (w, h) = Self::max_size(rect); tokio::task::spawn_blocking(move || { - let (w, h) = (PREVIEW.max_width, PREVIEW.max_height); - if img.width() > w || img.height() > h { - img = img.resize(w, h, FilterType::Triangle); - } - - Ok(match img { - DynamicImage::ImageRgb8(buf) => buf.save_with_format(cache, ImageFormat::Jpeg), - DynamicImage::ImageRgba8(buf) => buf.save_with_format(cache, ImageFormat::Jpeg), - buf => buf.into_rgb8().save_with_format(cache, ImageFormat::Jpeg), - }?) + Ok(if img.width() > w || img.height() > h { + img.resize(w, h, FilterType::Triangle) + } else { + img + }) }) .await? } + + pub(super) fn max_size(rect: Rect) -> (u32, u32) { + Term::ratio() + .map(|(r1, r2)| { + let (w, h) = ((rect.width as f64 * r1) as u32, (rect.height as f64 * r2) as u32); + (w.min(PREVIEW.max_width), h.min(PREVIEW.max_height)) + }) + .unwrap_or((PREVIEW.max_width, PREVIEW.max_height)) + } + + fn set_limits(mut r: image::io::Reader>) -> image::io::Reader> { + let mut limits = Limits::no_limits(); + if TASKS.image_alloc > 0 { + limits.max_alloc = Some(TASKS.image_alloc as u64); + } + if TASKS.image_bound[0] > 0 { + limits.max_image_width = Some(TASKS.image_bound[0] as u32); + } + if TASKS.image_bound[1] > 0 { + limits.max_image_height = Some(TASKS.image_bound[1] as u32); + } + r.limits(limits); + r + } } diff --git a/yazi-adaptor/src/iterm2.rs b/yazi-adaptor/src/iterm2.rs index 4ca13bc4..bedfda49 100644 --- a/yazi-adaptor/src/iterm2.rs +++ b/yazi-adaptor/src/iterm2.rs @@ -12,12 +12,16 @@ use crate::{CLOSE, START}; pub(super) struct Iterm2; impl Iterm2 { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> { - let img = Image::downscale(path, (rect.width, rect.height)).await?; + pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { + let img = Image::downscale(path, rect).await?; + let size = (img.width(), img.height()); let b = Self::encode(img).await?; Self::image_hide(rect)?; - Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| Ok(stdout.write_all(&b)?)) + Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| { + stdout.write_all(&b)?; + Ok(size) + }) } pub(super) fn image_hide(rect: Rect) -> Result<()> { diff --git a/yazi-adaptor/src/kitty.rs b/yazi-adaptor/src/kitty.rs index 86a35880..eb1b1103 100644 --- a/yazi-adaptor/src/kitty.rs +++ b/yazi-adaptor/src/kitty.rs @@ -312,8 +312,9 @@ static DIACRITICS: [char; 297] = [ pub(super) struct Kitty; impl Kitty { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> { - let img = Image::downscale(path, (rect.width, rect.height)).await?; + pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { + let img = Image::downscale(path, rect).await?; + let size = (img.width(), img.height()); let b = Self::encode(img).await?; Self::image_hide(rect)?; @@ -335,7 +336,7 @@ impl Kitty { stdout.write_all(buf.as_bytes())?; } - Ok(()) + Ok(size) }) } diff --git a/yazi-adaptor/src/kitty_old.rs b/yazi-adaptor/src/kitty_old.rs index 1dffaaa8..358c477c 100644 --- a/yazi-adaptor/src/kitty_old.rs +++ b/yazi-adaptor/src/kitty_old.rs @@ -12,12 +12,16 @@ use crate::{CLOSE, ESCAPE, START}; pub(super) struct KittyOld; impl KittyOld { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> { - let img = Image::downscale(path, (rect.width, rect.height)).await?; + pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { + let img = Image::downscale(path, rect).await?; + let size = (img.width(), img.height()); let b = Self::encode(img).await?; Self::image_hide()?; - Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| Ok(stdout.write_all(&b)?)) + Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| { + stdout.write_all(&b)?; + Ok(size) + }) } #[inline] diff --git a/yazi-adaptor/src/sixel.rs b/yazi-adaptor/src/sixel.rs index ac899dcd..736eca9c 100644 --- a/yazi-adaptor/src/sixel.rs +++ b/yazi-adaptor/src/sixel.rs @@ -11,12 +11,16 @@ use crate::{Image, CLOSE, ESCAPE, START}; pub(super) struct Sixel; impl Sixel { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> { - let img = Image::downscale(path, (rect.width, rect.height)).await?; + pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { + let img = Image::downscale(path, rect).await?; + let size = (img.width(), img.height()); let b = Self::encode(img).await?; Self::image_hide(rect)?; - Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| Ok(stdout.write_all(&b)?)) + Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| { + stdout.write_all(&b)?; + Ok(size) + }) } pub(super) fn image_hide(rect: Rect) -> Result<()> { diff --git a/yazi-adaptor/src/ueberzug.rs b/yazi-adaptor/src/ueberzug.rs index dce59f17..4ddbf472 100644 --- a/yazi-adaptor/src/ueberzug.rs +++ b/yazi-adaptor/src/ueberzug.rs @@ -1,17 +1,26 @@ -use std::{path::PathBuf, process::Stdio}; +use std::{path::{Path, PathBuf}, process::Stdio}; -use anyhow::Result; +use anyhow::{bail, Result}; +use imagesize::ImageSize; use ratatui::prelude::Rect; use tokio::{io::AsyncWriteExt, process::{Child, Command}, sync::mpsc::{self, UnboundedSender}}; use tracing::debug; use yazi_config::PREVIEW; +use yazi_shared::RoCell; -use crate::Adaptor; +use crate::{Adaptor, Image}; + +#[allow(clippy::type_complexity)] +static DEMON: RoCell>>> = RoCell::new(); pub(super) struct Ueberzug; impl Ueberzug { - pub(super) fn start(adaptor: Adaptor) -> Result>> { + pub(super) fn start(adaptor: Adaptor) { + if !adaptor.needs_ueberzug() { + return DEMON.init(None); + } + let mut child = Self::create_demon(adaptor).ok(); let (tx, mut rx) = mpsc::unbounded_channel(); @@ -29,8 +38,35 @@ impl Ueberzug { } } }); + DEMON.init(Some(tx)) + } - Ok(tx) + pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { + if let Some(tx) = &*DEMON { + tx.send(Some((path.to_path_buf(), rect)))?; + } else { + bail!("uninitialized ueberzug"); + } + + let path = path.to_owned(); + let ImageSize { width: w, height: h } = + tokio::task::spawn_blocking(move || imagesize::size(path)).await??; + + let (max_w, max_h) = Image::max_size(rect); + if w <= max_w as usize && h <= max_h as usize { + return Ok((w as u32, h as u32)); + } + + let ratio = f64::min(max_w as f64 / w as f64, max_h as f64 / h as f64); + Ok(((w as f64 * ratio).round() as u32, (h as f64 * ratio).round() as u32)) + } + + pub(super) fn image_hide(_: Rect) -> Result<()> { + if let Some(tx) = &*DEMON { + Ok(tx.send(None)?) + } else { + bail!("uninitialized ueberzug"); + } } fn create_demon(adaptor: Adaptor) -> Result { diff --git a/yazi-config/Cargo.toml b/yazi-config/Cargo.toml index 28640a24..f7b465a4 100644 --- a/yazi-config/Cargo.toml +++ b/yazi-config/Cargo.toml @@ -13,6 +13,7 @@ yazi-shared = { path = "../yazi-shared", version = "0.1.5" } # External dependencies anyhow = "^1" +arc-swap = "^1" clap = { version = "^4", features = [ "derive" ] } crossterm = "^0" dirs = "^5" diff --git a/yazi-config/build.rs b/yazi-config/build.rs index 553c001b..5b8e7ef3 100644 --- a/yazi-config/build.rs +++ b/yazi-config/build.rs @@ -1,5 +1,5 @@ -#[path = "src/boot/cli.rs"] -mod cli; +#[path = "src/boot/args.rs"] +mod args; use std::{env, error::Error, fs}; @@ -14,7 +14,7 @@ fn main() -> Result<(), Box> { return Ok(()); } - let cmd = &mut cli::Args::command(); + let cmd = &mut args::Args::command(); let bin = "yazi"; let out = "completions"; diff --git a/yazi-config/preset/keymap.toml b/yazi-config/preset/keymap.toml index 93063d4b..1dc00d37 100644 --- a/yazi-config/preset/keymap.toml +++ b/yazi-config/preset/keymap.toml @@ -33,10 +33,10 @@ keymap = [ { on = [ "H" ], exec = "back", desc = "Go back to the previous directory" }, { on = [ "L" ], exec = "forward", desc = "Go forward to the next directory" }, - { on = [ "" ], exec = "peek -5", desc = "Peek up 5 units in the preview" }, - { on = [ "" ], exec = "peek 5", desc = "Peek down 5 units in the preview" }, - { on = [ "" ], exec = "peek -5", desc = "Peek up 5 units in the preview" }, - { on = [ "" ], exec = "peek 5", desc = "Peek down 5 units in the preview" }, + { on = [ "" ], exec = "seek -5", desc = "Peek up 5 units in the preview" }, + { on = [ "" ], exec = "seek 5", desc = "Peek down 5 units in the preview" }, + { on = [ "" ], exec = "seek -5", desc = "Peek up 5 units in the preview" }, + { on = [ "" ], exec = "seek 5", desc = "Peek down 5 units in the preview" }, { on = [ "" ], exec = "arrow -1", desc = "Move cursor up" }, { on = [ "" ], exec = "arrow 1", desc = "Move cursor down" }, diff --git a/yazi-config/preset/theme.toml b/yazi-config/preset/theme.toml index ae21d95f..3ccd7665 100644 --- a/yazi-config/preset/theme.toml +++ b/yazi-config/preset/theme.toml @@ -27,10 +27,6 @@ tab_width = 1 border_symbol = "│" border_style = { fg = "gray" } -# Offset -folder_offset = [ 1, 0, 1, 0 ] -preview_offset = [ 1, 1, 1, 1 ] - # Highlighting syntect_theme = "" diff --git a/yazi-config/preset/yazi.toml b/yazi-config/preset/yazi.toml index 4240f05b..4db32843 100644 --- a/yazi-config/preset/yazi.toml +++ b/yazi-config/preset/yazi.toml @@ -61,6 +61,7 @@ rules = [ { mime = "application/x-bzip2", use = [ "extract", "reveal" ] }, { mime = "application/x-7z-compressed", use = [ "extract", "reveal" ] }, { mime = "application/x-rar", use = [ "extract", "reveal" ] }, + { mime = "application/xz", use = [ "extract", "reveal" ] }, { mime = "*", use = [ "open", "reveal" ] }, ] @@ -73,8 +74,44 @@ image_alloc = 536870912 # 512MB image_bound = [ 0, 0 ] suppress_preload = false -[plugins] -preload = [] +[plugin] + +preloaders = [ + { name = "*", cond = "!mime", exec = "mime.lua", multi = true }, + # Image + { mime = "image/vnd.djvu", exec = "noop.lua" }, + { mime = "image/*", exec = "image.lua" }, + # Video + { mime = "video/*", exec = "video.lua" }, + # PDF + { mime = "application/pdf", exec = "pdf.lua" }, +] +previewers = [ + { name = "*/", exec = "folder.lua", sync = true }, + # Code + { mime = "text/*", exec = "code.lua" }, + { mime = "*/xml", exec = "code.lua" }, + { mime = "*/javascript", exec = "code.lua" }, + { mime = "*/x-wine-extension-ini", exec = "code.lua" }, + # JSON + { mime = "application/json", exec = "json.lua" }, + # Image + { mime = "image/vnd.djvu", exec = "noop.lua" }, + { mime = "image/*", exec = "image.lua" }, + # Video + { mime = "video/*", exec = "video.lua" }, + # PDF + { mime = "application/pdf", exec = "pdf.lua" }, + # Archive + { mime = "application/zip", exec = "archive.lua" }, + { mime = "application/gzip", exec = "archive.lua" }, + { mime = "application/x-tar", exec = "archive.lua" }, + { mime = "application/x-bzip", exec = "archive.lua" }, + { mime = "application/x-bzip2", exec = "archive.lua" }, + { mime = "application/x-7z-compressed", exec = "archive.lua" }, + { mime = "application/x-rar", exec = "archive.lua" }, + { mime = "application/xz", exec = "archive.lua" }, +] [input] # cd diff --git a/yazi-config/src/boot/cli.rs b/yazi-config/src/boot/args.rs similarity index 95% rename from yazi-config/src/boot/cli.rs rename to yazi-config/src/boot/args.rs index 111e1345..08c4a11a 100644 --- a/yazi-config/src/boot/cli.rs +++ b/yazi-config/src/boot/args.rs @@ -4,7 +4,7 @@ use clap::{command, Parser}; #[derive(Debug, Parser)] #[command(name = "yazi")] -pub(super) struct Args { +pub struct Args { /// Set the current working entry #[arg(index = 1)] pub entry: Option, diff --git a/yazi-config/src/boot/boot.rs b/yazi-config/src/boot/boot.rs index 60c64e0b..eb3ad213 100644 --- a/yazi-config/src/boot/boot.rs +++ b/yazi-config/src/boot/boot.rs @@ -1,24 +1,23 @@ -use std::{ffi::OsString, fs, path::PathBuf, process}; +use std::{ffi::OsString, fs, path::{Path, PathBuf}, process}; use clap::Parser; use yazi_shared::fs::{current_cwd, expand_path}; -use super::cli::Args; -use crate::{Xdg, PREVIEW}; +use super::Args; +use crate::{Xdg, ARGS}; #[derive(Debug)] pub struct Boot { pub cwd: PathBuf, pub file: Option, - pub state_dir: PathBuf, - - pub cwd_file: Option, - pub chooser_file: Option, + pub config_dir: PathBuf, + pub plugin_dir: PathBuf, + pub state_dir: PathBuf, } impl Boot { - fn parse_entry(entry: Option) -> (PathBuf, Option) { + fn parse_entry(entry: Option<&Path>) -> (PathBuf, Option) { let entry = match entry { Some(p) => expand_path(p), None => return (current_cwd().unwrap(), None), @@ -35,7 +34,28 @@ impl Boot { impl Default for Boot { fn default() -> Self { - let args = Args::parse(); + let (cwd, file) = Self::parse_entry(ARGS.entry.as_deref()); + let boot = Self { + cwd, + file, + + config_dir: Xdg::config_dir().unwrap(), + plugin_dir: Xdg::plugin_dir().unwrap(), + state_dir: Xdg::state_dir().unwrap(), + }; + + if !boot.state_dir.is_dir() { + fs::create_dir_all(&boot.state_dir).unwrap(); + } + + boot + } +} + +impl Default for Args { + fn default() -> Self { + let args = Self::parse(); + if args.version { println!( "yazi {} ({} {})", @@ -46,37 +66,6 @@ impl Default for Boot { process::exit(0); } - let (cwd, file) = Self::parse_entry(args.entry); - let boot = Self { - cwd, - file, - - state_dir: Xdg::state_dir().unwrap(), - - cwd_file: args.cwd_file, - chooser_file: args.chooser_file, - }; - - if !boot.state_dir.is_dir() { - fs::create_dir_all(&boot.state_dir).unwrap(); - } - if !PREVIEW.cache_dir.is_dir() { - fs::create_dir(&PREVIEW.cache_dir).unwrap(); - } - - if args.clear_cache { - if PREVIEW.cache_dir == Xdg::cache_dir() { - println!("Clearing cache directory: \n{:?}", PREVIEW.cache_dir); - fs::remove_dir_all(&PREVIEW.cache_dir).unwrap(); - } else { - println!( - "You've changed the default cache directory, for your data's safety, please clear it manually: \n{:?}", - PREVIEW.cache_dir - ); - } - process::exit(0); - } - - boot + args } } diff --git a/yazi-config/src/boot/mod.rs b/yazi-config/src/boot/mod.rs index 865dc405..d29db39c 100644 --- a/yazi-config/src/boot/mod.rs +++ b/yazi-config/src/boot/mod.rs @@ -1,4 +1,5 @@ +mod args; mod boot; -mod cli; +pub use args::*; pub use boot::*; diff --git a/yazi-config/src/keymap/exec.rs b/yazi-config/src/keymap/exec.rs index 7c257c54..b0efd99e 100644 --- a/yazi-config/src/keymap/exec.rs +++ b/yazi-config/src/keymap/exec.rs @@ -34,7 +34,7 @@ where type Value = Vec; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a exec string, e.g. `tab_switch 0`") + formatter.write_str("a `exec` string or array of strings within [keymap]") } fn visit_seq(self, mut seq: A) -> Result @@ -45,6 +45,9 @@ where while let Some(value) = &seq.next_element::()? { execs.push(parse(value).map_err(de::Error::custom)?); } + if execs.is_empty() { + return Err(de::Error::custom("`exec` within [keymap] cannot be empty")); + } Ok(execs) } diff --git a/yazi-config/src/keymap/mod.rs b/yazi-config/src/keymap/mod.rs index c531882a..76a1c2c4 100644 --- a/yazi-config/src/keymap/mod.rs +++ b/yazi-config/src/keymap/mod.rs @@ -4,7 +4,7 @@ mod key; mod keymap; pub use control::*; -#[allow(unused_imports)] pub use exec::*; +#[allow(unused_imports)] pub use key::*; pub use keymap::*; diff --git a/yazi-config/src/layout.rs b/yazi-config/src/layout.rs new file mode 100644 index 00000000..c63809d5 --- /dev/null +++ b/yazi-config/src/layout.rs @@ -0,0 +1,12 @@ +use ratatui::layout::Rect; + +#[derive(Default)] +pub struct Layout { + pub header: Rect, + + pub parent: Rect, + pub current: Rect, + pub preview: Rect, + + pub status: Rect, +} diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index 0edb2e45..e71004f8 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -4,11 +4,12 @@ use yazi_shared::RoCell; mod boot; pub mod keymap; +mod layout; mod log; pub mod manager; pub mod open; mod pattern; -pub mod plugins; +pub mod plugin; pub mod popup; mod preset; pub mod preview; @@ -17,10 +18,15 @@ pub mod theme; mod validation; mod xdg; +pub use layout::*; pub(crate) use pattern::*; pub(crate) use preset::*; pub(crate) use xdg::*; +pub static ARGS: RoCell = RoCell::new(); +pub static BOOT: RoCell = RoCell::new(); +pub static LAYOUT: RoCell> = RoCell::new(); + static MERGED_KEYMAP: RoCell = RoCell::new(); static MERGED_THEME: RoCell = RoCell::new(); static MERGED_YAZI: RoCell = RoCell::new(); @@ -29,16 +35,18 @@ pub static KEYMAP: RoCell = RoCell::new(); pub static LOG: RoCell = RoCell::new(); pub static MANAGER: RoCell = RoCell::new(); pub static OPEN: RoCell = RoCell::new(); -pub static PLUGINS: RoCell = RoCell::new(); +pub static PLUGIN: RoCell = RoCell::new(); pub static PREVIEW: RoCell = RoCell::new(); pub static TASKS: RoCell = RoCell::new(); pub static THEME: RoCell = RoCell::new(); pub static INPUT: RoCell = RoCell::new(); pub static SELECT: RoCell = RoCell::new(); -pub static BOOT: RoCell = RoCell::new(); - pub fn init() { + ARGS.with(Default::default); + BOOT.with(Default::default); + LAYOUT.with(Default::default); + MERGED_KEYMAP.with(Preset::keymap); MERGED_THEME.with(Preset::theme); MERGED_YAZI.with(Preset::yazi); @@ -47,12 +55,10 @@ pub fn init() { LOG.with(Default::default); MANAGER.with(Default::default); OPEN.with(Default::default); - PLUGINS.with(Default::default); + PLUGIN.with(Default::default); PREVIEW.with(Default::default); TASKS.with(Default::default); THEME.with(Default::default); INPUT.with(Default::default); SELECT.with(Default::default); - - BOOT.with(Default::default); } diff --git a/yazi-config/src/manager/layout.rs b/yazi-config/src/manager/layout.rs deleted file mode 100644 index 61cbb346..00000000 --- a/yazi-config/src/manager/layout.rs +++ /dev/null @@ -1,83 +0,0 @@ -use anyhow::bail; -use crossterm::terminal::WindowSize; -use ratatui::{prelude::Rect, widgets::{Block, Padding}}; -use serde::{Deserialize, Serialize}; -use yazi_shared::term::Term; - -use crate::{PREVIEW, THEME}; - -#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] -#[serde(try_from = "Vec")] -pub struct ManagerLayout { - pub parent: u16, - pub current: u16, - pub preview: u16, - pub all: u16, -} - -impl TryFrom> for ManagerLayout { - type Error = anyhow::Error; - - fn try_from(ratio: Vec) -> Result { - if ratio.len() != 3 { - bail!("invalid layout ratio: {:?}", ratio); - } - if ratio.iter().all(|&r| r == 0) { - bail!("at least one layout ratio must be non-zero: {:?}", ratio); - } - - Ok(Self { - parent: ratio[0], - current: ratio[1], - preview: ratio[2], - all: ratio[0] + ratio[1] + ratio[2], - }) - } -} - -impl ManagerLayout { - pub fn preview_rect(&self) -> Rect { - let WindowSize { columns, rows, .. } = Term::size(); - let (top, right, bottom, left) = THEME.manager.preview_offset; - - let w = (columns * self.preview) as f64 / self.all as f64; - let w = if w.fract() > 0.5 { w.ceil() as u16 } else { w.floor() as u16 }; - - Rect { - x: left.saturating_add(columns - w), - y: top, - width: w.saturating_sub(left + right), - height: rows.saturating_sub(top + bottom), - } - } - - #[inline] - pub fn preview_height(&self) -> usize { self.preview_rect().height as usize } - - pub fn image_rect(&self) -> Rect { - let mut rect = self.preview_rect(); - if PREVIEW.max_width == 0 || PREVIEW.max_height == 0 { - return rect; - } - if let Some((w, h)) = Term::ratio() { - rect.width = rect.width.min((PREVIEW.max_width as f64 / w).ceil() as u16); - rect.height = rect.height.min((PREVIEW.max_height as f64 / h).ceil() as u16); - } - rect - } - - pub fn folder_rect(&self) -> Rect { - let WindowSize { columns, rows, .. } = Term::size(); - - let offset = THEME.manager.folder_offset; - Block::default().padding(Padding::new(offset.3, offset.1, offset.0, offset.2)).inner(Rect { - x: columns * self.parent / self.all, - y: 0, - width: columns * self.current / self.all, - height: rows, - }) - } - - #[inline] - pub fn folder_height(&self) -> usize { self.folder_rect().height as usize } -} diff --git a/yazi-config/src/manager/manager.rs b/yazi-config/src/manager/manager.rs index 6f8e8ead..be20951c 100644 --- a/yazi-config/src/manager/manager.rs +++ b/yazi-config/src/manager/manager.rs @@ -1,12 +1,13 @@ use serde::{Deserialize, Serialize}; use validator::Validate; -use super::{ManagerLayout, SortBy}; +use super::{ManagerRatio, SortBy}; use crate::{validation::check_validation, MERGED_YAZI}; #[derive(Debug, Deserialize, Serialize, Validate)] pub struct Manager { - pub layout: ManagerLayout, + // FIXME: rename this to "ratio" + pub layout: ManagerRatio, // Sorting pub sort_by: SortBy, diff --git a/yazi-config/src/manager/mod.rs b/yazi-config/src/manager/mod.rs index 08394cbf..2ddd6599 100644 --- a/yazi-config/src/manager/mod.rs +++ b/yazi-config/src/manager/mod.rs @@ -1,7 +1,7 @@ -mod layout; mod manager; +mod ratio; mod sorting; -pub use layout::*; pub use manager::*; +pub use ratio::*; pub use sorting::*; diff --git a/yazi-config/src/manager/ratio.rs b/yazi-config/src/manager/ratio.rs new file mode 100644 index 00000000..c264cb92 --- /dev/null +++ b/yazi-config/src/manager/ratio.rs @@ -0,0 +1,31 @@ +use anyhow::bail; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(try_from = "Vec")] +pub struct ManagerRatio { + pub parent: u16, + pub current: u16, + pub preview: u16, + pub all: u16, +} + +impl TryFrom> for ManagerRatio { + type Error = anyhow::Error; + + fn try_from(ratio: Vec) -> Result { + if ratio.len() != 3 { + bail!("invalid layout ratio: {:?}", ratio); + } + if ratio.iter().all(|&r| r == 0) { + bail!("at least one layout ratio must be non-zero: {:?}", ratio); + } + + Ok(Self { + parent: ratio[0], + current: ratio[1], + preview: ratio[2], + all: ratio[0] + ratio[1] + ratio[2], + }) + } +} diff --git a/yazi-config/src/open/open.rs b/yazi-config/src/open/open.rs index 30d04237..5339314d 100644 --- a/yazi-config/src/open/open.rs +++ b/yazi-config/src/open/open.rs @@ -23,7 +23,7 @@ impl Open { P: AsRef, M: AsRef, { - let is_folder = Some(mime.as_ref() == MIME_DIR); + let is_folder = mime.as_ref() == MIME_DIR; self.rules.iter().find_map(|rule| { if rule.mime.as_ref().is_some_and(|m| m.matches(&mime)) || rule.name.as_ref().is_some_and(|n| n.match_path(&path, is_folder)) diff --git a/yazi-config/src/pattern.rs b/yazi-config/src/pattern.rs index 205707e0..10ee044e 100644 --- a/yazi-config/src/pattern.rs +++ b/yazi-config/src/pattern.rs @@ -23,14 +23,14 @@ impl Pattern { } #[inline] - pub fn match_path(&self, path: impl AsRef, is_folder: Option) -> bool { + pub fn match_path(&self, path: impl AsRef, is_folder: bool) -> bool { let path = path.as_ref(); let s = if self.full_path { path.to_str() } else { path.file_name().and_then(|n| n.to_str()).or_else(|| path.to_str()) }; - is_folder.map_or(true, |f| f == self.is_folder) && s.is_some_and(|s| self.matches(s)) + is_folder == self.is_folder && s.is_some_and(|s| self.matches(s)) } } diff --git a/yazi-config/src/plugin/exec.rs b/yazi-config/src/plugin/exec.rs new file mode 100644 index 00000000..8e7f81bb --- /dev/null +++ b/yazi-config/src/plugin/exec.rs @@ -0,0 +1,39 @@ +use std::fmt; + +use anyhow::Result; +use serde::{de::{self, Visitor}, Deserializer}; +use yazi_shared::event::Exec; + +pub(super) fn exec_deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + struct ExecVisitor; + + impl<'de> Visitor<'de> for ExecVisitor { + type Value = Exec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a `exec` string or array of strings") + } + + fn visit_seq(self, _: A) -> Result + where + A: de::SeqAccess<'de>, + { + Err(de::Error::custom("`exec` within [plugin] must be a string")) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + if value.is_empty() { + return Err(de::Error::custom("`exec` within [plugin] cannot be empty")); + } + Ok(Exec { cmd: value.to_owned(), ..Default::default() }) + } + } + + deserializer.deserialize_any(ExecVisitor) +} diff --git a/yazi-config/src/plugin/mod.rs b/yazi-config/src/plugin/mod.rs new file mode 100644 index 00000000..af03fb09 --- /dev/null +++ b/yazi-config/src/plugin/mod.rs @@ -0,0 +1,7 @@ +mod exec; +mod plugin; + +pub use exec::*; +pub use plugin::*; + +pub const MAX_PRELOADERS: u8 = 32; diff --git a/yazi-config/src/plugin/plugin.rs b/yazi-config/src/plugin/plugin.rs new file mode 100644 index 00000000..83eb47ef --- /dev/null +++ b/yazi-config/src/plugin/plugin.rs @@ -0,0 +1,78 @@ +use std::path::Path; + +use serde::Deserialize; +use yazi_shared::{event::Exec, Condition, MIME_DIR}; + +use crate::{pattern::Pattern, plugin::MAX_PRELOADERS, MERGED_YAZI}; + +#[derive(Deserialize)] +pub struct Plugin { + pub preloaders: Vec, + pub previewers: Vec, +} + +#[derive(Deserialize)] +pub struct PluginRule { + #[serde(default)] + pub id: u8, + pub cond: Option, + pub name: Option, + pub mime: Option, + #[serde(deserialize_with = "super::exec_deserialize")] + pub exec: Exec, + #[serde(default)] + pub sync: bool, + #[serde(default)] + pub multi: bool, +} + +impl Default for Plugin { + fn default() -> Self { + #[derive(Deserialize)] + struct Outer { + plugin: Plugin, + } + + let mut plugin = toml::from_str::(&MERGED_YAZI).unwrap().plugin; + if plugin.preloaders.len() > MAX_PRELOADERS as usize { + panic!("Too many preloaders"); + } + + for (i, preloader) in plugin.preloaders.iter_mut().enumerate() { + if preloader.sync { + panic!("Preloaders cannot be synchronous"); + } + preloader.id = i as u8; + } + + plugin + } +} + +impl Plugin { + pub fn preloaders( + &self, + path: &Path, + mime: Option<&str>, + f: impl Fn(&str) -> bool + Copy, + ) -> Vec<&PluginRule> { + let is_folder = mime == Some(MIME_DIR); + self + .preloaders + .iter() + .filter(|&rule| { + rule.cond.as_ref().and_then(|c| c.eval(f)) != Some(false) + && (rule.name.as_ref().is_some_and(|n| n.match_path(path, is_folder)) + || rule.mime.as_ref().zip(mime).map_or(false, |(m, s)| m.matches(s))) + }) + .collect() + } + + pub fn previewer(&self, path: &Path, mime: &str) -> Option<&PluginRule> { + let is_folder = mime == MIME_DIR; + self.previewers.iter().find(|&rule| { + rule.mime.as_ref().is_some_and(|m| m.matches(mime)) + || rule.name.as_ref().is_some_and(|n| n.match_path(path, is_folder)) + }) + } +} diff --git a/yazi-config/src/plugins/mod.rs b/yazi-config/src/plugins/mod.rs deleted file mode 100644 index 1e08e1ac..00000000 --- a/yazi-config/src/plugins/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod plugins; - -pub use plugins::*; diff --git a/yazi-config/src/plugins/plugins.rs b/yazi-config/src/plugins/plugins.rs deleted file mode 100644 index b2b95a78..00000000 --- a/yazi-config/src/plugins/plugins.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::path::PathBuf; - -use serde::Deserialize; -use validator::Validate; -use yazi_shared::fs::expand_path; - -use crate::MERGED_YAZI; - -#[derive(Debug, Deserialize, Validate)] -pub struct Plugins { - pub preload: Vec, -} - -impl Default for Plugins { - fn default() -> Self { - #[derive(Deserialize)] - struct Outer { - plugins: Plugins, - } - - let mut plugins = toml::from_str::(&MERGED_YAZI).unwrap().plugins; - - plugins.preload.iter_mut().for_each(|p| { - *p = expand_path(&p); - }); - - plugins - } -} diff --git a/yazi-config/src/preset.rs b/yazi-config/src/preset.rs index 37e3bd64..3ad63f79 100644 --- a/yazi-config/src/preset.rs +++ b/yazi-config/src/preset.rs @@ -2,7 +2,7 @@ use std::fs; use toml::Table; -use crate::xdg::Xdg; +use crate::BOOT; pub(crate) struct Preset; @@ -29,7 +29,7 @@ impl Preset { } fn merge_str(user: &str, base: &str) -> String { - let path = Xdg::config_dir().unwrap().join(user); + let path = BOOT.config_dir.join(user); let mut user = fs::read_to_string(path).unwrap_or_default().parse::().unwrap(); let base = base.parse::
().unwrap(); diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index d22aa078..37d68e26 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -1,12 +1,11 @@ -use std::{path::{Path, PathBuf}, time::{self, SystemTime}}; +use std::{fs, path::PathBuf, process, time::{self, SystemTime}}; -use md5::{Digest, Md5}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use yazi_shared::fs::expand_path; -use crate::{xdg::Xdg, MERGED_YAZI}; +use crate::{xdg::Xdg, ARGS, MERGED_YAZI}; -#[derive(Debug)] +#[derive(Debug, Serialize)] pub struct Preview { pub tab_size: u8, pub max_width: u32, @@ -37,10 +36,26 @@ impl Default for Preview { } let preview = toml::from_str::(&MERGED_YAZI).unwrap().preview; - let cache_dir = preview.cache_dir.filter(|p| !p.is_empty()).map_or_else(Xdg::cache_dir, expand_path); + if !cache_dir.is_dir() { + fs::create_dir(&cache_dir).unwrap(); + } + + if ARGS.clear_cache { + if cache_dir == Xdg::cache_dir() { + println!("Clearing cache directory: \n{:?}", cache_dir); + fs::remove_dir_all(&cache_dir).unwrap(); + } else { + println!( + "You've changed the default cache directory, for your data's safety, please clear it manually: \n{:?}", + cache_dir + ); + } + process::exit(0); + } + Preview { tab_size: preview.tab_size, max_width: preview.max_width, @@ -55,13 +70,6 @@ impl Default for Preview { } impl Preview { - #[inline] - pub fn cache(&self, path: &Path, skip: usize) -> PathBuf { - self - .cache_dir - .join(format!("{:x}", Md5::new_with_prefix(format!("{:?}///{}", path, skip)).finalize())) - } - #[inline] pub fn tmpfile(&self, prefix: &str) -> PathBuf { let nanos = SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap().as_nanos(); diff --git a/yazi-config/src/theme/filetype.rs b/yazi-config/src/theme/filetype.rs index e3f95da6..f5fdb16d 100644 --- a/yazi-config/src/theme/filetype.rs +++ b/yazi-config/src/theme/filetype.rs @@ -1,6 +1,7 @@ use std::path::Path; use serde::{Deserialize, Deserializer}; +use yazi_shared::MIME_DIR; use super::{Color, Style, StyleShadow}; use crate::Pattern; @@ -12,14 +13,10 @@ pub struct Filetype { } impl Filetype { - pub fn matches(&self, path: &Path, mime: Option>, is_dir: bool) -> bool { - if self.name.as_ref().is_some_and(|e| e.match_path(path, Some(is_dir))) { - return true; - } - if let Some(mime) = mime { - return self.mime.as_ref().is_some_and(|m| m.matches(mime)); - } - false + pub fn matches(&self, path: &Path, mime: Option<&str>) -> bool { + let is_dir = mime == Some(MIME_DIR); + self.name.as_ref().is_some_and(|n| n.match_path(path, is_dir)) + || self.mime.as_ref().zip(mime).map_or(false, |(m, s)| m.matches(s)) } } diff --git a/yazi-config/src/theme/theme.rs b/yazi-config/src/theme/theme.rs index ae5cfa75..d58796eb 100644 --- a/yazi-config/src/theme/theme.rs +++ b/yazi-config/src/theme/theme.rs @@ -34,10 +34,6 @@ pub struct Manager { pub border_symbol: String, pub border_style: Style, - // Offset - pub(crate) folder_offset: (u16, u16, u16, u16), - pub(crate) preview_offset: (u16, u16, u16, u16), - // Highlighting pub syntect_theme: PathBuf, } diff --git a/yazi-config/src/xdg.rs b/yazi-config/src/xdg.rs index d17e3409..ec44c7b6 100644 --- a/yazi-config/src/xdg.rs +++ b/yazi-config/src/xdg.rs @@ -24,6 +24,9 @@ impl Xdg { } } + #[inline] + pub(super) fn plugin_dir() -> Option { Self::config_dir().map(|p| p.join("plugins")) } + pub(super) fn state_dir() -> Option { #[cfg(windows)] { diff --git a/yazi-core/Cargo.toml b/yazi-core/Cargo.toml index 8b083ccf..81ad2873 100644 --- a/yazi-core/Cargo.toml +++ b/yazi-core/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/sxyazi/yazi" [dependencies] yazi-adaptor = { path = "../yazi-adaptor", version = "0.1.5" } yazi-config = { path = "../yazi-config", version = "0.1.5" } +yazi-plugin = { path = "../yazi-plugin", version = "0.1.5" } yazi-scheduler = { path = "../yazi-scheduler", version = "0.1.5" } yazi-shared = { path = "../yazi-shared", version = "0.1.5" } @@ -30,8 +31,8 @@ serde = "^1" syntect = { version = "^5", default-features = false, features = [ "parsing", "default-themes", "plist-load", "regex-onig" ] } tokio = { version = "^1", features = [ "parking_lot", "macros", "rt-multi-thread", "sync", "time", "fs", "process", "io-std", "io-util" ] } tokio-stream = "^0" +tokio-util = "^0" unicode-width = "^0" -yazi-prebuild = "^0" # Logging tracing = { version = "^0", features = [ "max_level_debug", "release_max_level_warn" ] } diff --git a/yazi-core/src/completion/mod.rs b/yazi-core/src/completion/mod.rs index daf831ad..de2ab003 100644 --- a/yazi-core/src/completion/mod.rs +++ b/yazi-core/src/completion/mod.rs @@ -1,4 +1,4 @@ mod commands; mod completion; -pub(super) use completion::*; +pub use completion::*; diff --git a/yazi-core/src/files/files.rs b/yazi-core/src/folder/files.rs similarity index 100% rename from yazi-core/src/files/files.rs rename to yazi-core/src/folder/files.rs diff --git a/yazi-core/src/tab/folder.rs b/yazi-core/src/folder/folder.rs similarity index 87% rename from yazi-core/src/tab/folder.rs rename to yazi-core/src/folder/folder.rs index 832fb63f..230bc9f1 100644 --- a/yazi-core/src/tab/folder.rs +++ b/yazi-core/src/folder/folder.rs @@ -1,8 +1,8 @@ use ratatui::layout::Rect; -use yazi_config::MANAGER; -use yazi_shared::{emit, fs::{File, FilesOp}, fs::Url}; +use yazi_config::LAYOUT; +use yazi_shared::{emit, fs::{File, FilesOp, Url}}; -use crate::{files::Files, Step}; +use crate::{folder::Files, Step}; #[derive(Default)] pub struct Folder { @@ -50,7 +50,7 @@ impl Folder { } pub fn set_page(&mut self, force: bool) { - let limit = MANAGER.layout.folder_height(); + let limit = LAYOUT.load().current.height as usize; if limit == 0 { return; } @@ -81,8 +81,8 @@ impl Folder { let old = (self.cursor, self.offset); let len = self.files.len(); - let limit = MANAGER.layout.folder_height(); - self.cursor = step.add(self.cursor, || limit).min(len.saturating_sub(1)); + let limit = LAYOUT.load().current.height as usize; + self.cursor = step.add(self.cursor, limit).min(len.saturating_sub(1)); self.offset = if self.cursor >= (self.offset + limit).min(len).saturating_sub(5) { len.saturating_sub(limit).min(self.offset + self.cursor - old.0) } else { @@ -97,7 +97,7 @@ impl Folder { let old = (self.cursor, self.offset); let max = self.files.len().saturating_sub(1); - self.cursor = step.add(self.cursor, || MANAGER.layout.folder_height()).min(max); + self.cursor = step.add(self.cursor, LAYOUT.load().current.height as usize).min(max); self.offset = if self.cursor < self.offset + 5 { self.offset.saturating_sub(old.0 - self.cursor) } else { @@ -133,7 +133,7 @@ impl Folder { pub fn paginate(&self, page: usize) -> &[File] { let len = self.files.len(); - let limit = MANAGER.layout.folder_height(); + let limit = LAYOUT.load().current.height as usize; let start = (page * limit).min(len.saturating_sub(1)); let end = (start + limit).min(len); @@ -143,7 +143,7 @@ impl Folder { pub fn rect_current(&self, url: &Url) -> Option { let y = self.files.position(url)? - self.offset; - let mut rect = MANAGER.layout.folder_rect(); + let mut rect = LAYOUT.load().current; rect.y = rect.y.saturating_sub(1) + y as u16; rect.height = 1; Some(rect) diff --git a/yazi-core/src/files/mod.rs b/yazi-core/src/folder/mod.rs similarity index 66% rename from yazi-core/src/files/mod.rs rename to yazi-core/src/folder/mod.rs index f6b8945d..f40a0ab7 100644 --- a/yazi-core/src/files/mod.rs +++ b/yazi-core/src/folder/mod.rs @@ -1,5 +1,7 @@ mod files; +mod folder; mod sorter; pub use files::*; +pub use folder::*; pub use sorter::*; diff --git a/yazi-core/src/files/sorter.rs b/yazi-core/src/folder/sorter.rs similarity index 94% rename from yazi-core/src/files/sorter.rs rename to yazi-core/src/folder/sorter.rs index 75c0c5fe..4dea6083 100644 --- a/yazi-core/src/files/sorter.rs +++ b/yazi-core/src/folder/sorter.rs @@ -68,16 +68,16 @@ impl FilesSorter { let mut entities = Vec::with_capacity(items.len()); for (i, file) in items.iter().enumerate() { indices.push(i); - entities.push((file.url.to_string_lossy(), file)); + entities.push(file.url.as_os_str().as_encoded_bytes()); } indices.sort_unstable_by(|&a, &b| { - let promote = self.promote(entities[a].1, entities[b].1); + let promote = self.promote(&items[a], &items[b]); if promote != Ordering::Equal { return promote; } - let ordering = natsort(&entities[a].0, &entities[b].0, !self.sensitive); + let ordering = natsort(entities[a], entities[b], !self.sensitive); if self.reverse { ordering.reverse() } else { ordering } }); diff --git a/yazi-core/src/input/shell.rs b/yazi-core/src/input/shell.rs index 16cf0230..9ff89fee 100644 --- a/yazi-core/src/input/shell.rs +++ b/yazi-core/src/input/shell.rs @@ -1,8 +1,8 @@ use anyhow::{bail, Result}; use syntect::{easy::HighlightLines, util::as_24_bit_terminal_escaped}; +use yazi_plugin::external::Highlighter; use super::Input; -use crate::Highlighter; impl Input { pub fn value_pretty(&self) -> Result { diff --git a/yazi-core/src/lib.rs b/yazi-core/src/lib.rs index 5552e2ac..c2cefc44 100644 --- a/yazi-core/src/lib.rs +++ b/yazi-core/src/lib.rs @@ -8,13 +8,10 @@ mod clipboard; pub mod completion; -mod context; -pub mod files; +pub mod folder; pub mod help; -mod highlighter; pub mod input; pub mod manager; -pub mod preview; pub mod select; mod step; pub mod tab; @@ -22,8 +19,6 @@ pub mod tasks; pub mod which; pub use clipboard::*; -pub use context::*; -pub use highlighter::*; pub use step::*; pub fn init() { diff --git a/yazi-core/src/manager/commands/hover.rs b/yazi-core/src/manager/commands/hover.rs index 33188093..6a93ee4e 100644 --- a/yazi-core/src/manager/commands/hover.rs +++ b/yazi-core/src/manager/commands/hover.rs @@ -27,7 +27,7 @@ impl Manager { let mut b = self.current_mut().repos(opt.url); // Re-peek - b |= self.peek(0); + b |= self.peek(()); // Refresh watcher let mut to_watch = BTreeSet::new(); diff --git a/yazi-core/src/manager/commands/mod.rs b/yazi-core/src/manager/commands/mod.rs index a1fd71fd..0fc34796 100644 --- a/yazi-core/src/manager/commands/mod.rs +++ b/yazi-core/src/manager/commands/mod.rs @@ -9,9 +9,11 @@ mod quit; mod refresh; mod remove; mod rename; +mod seek; mod suspend; mod tab_close; mod tab_create; mod tab_swap; mod tab_switch; +mod update_mimetype; mod yank; diff --git a/yazi-core/src/manager/commands/open.rs b/yazi-core/src/manager/commands/open.rs index 029c6eb6..9275bf7f 100644 --- a/yazi-core/src/manager/commands/open.rs +++ b/yazi-core/src/manager/commands/open.rs @@ -1,7 +1,7 @@ use std::ffi::OsString; use yazi_config::{popup::SelectCfg, OPEN}; -use yazi_scheduler::external; +use yazi_plugin::external; use yazi_shared::{event::Exec, MIME_DIR}; use crate::{manager::Manager, select::Select, tasks::Tasks}; diff --git a/yazi-core/src/manager/commands/peek.rs b/yazi-core/src/manager/commands/peek.rs index 500ad8d0..3fefb558 100644 --- a/yazi-core/src/manager/commands/peek.rs +++ b/yazi-core/src/manager/commands/peek.rs @@ -1,11 +1,10 @@ -use yazi_config::MANAGER; -use yazi_shared::{emit, event::Exec, fs::Url, Layer, MIME_DIR}; +use yazi_shared::{event::Exec, fs::Url, MIME_DIR}; use crate::manager::Manager; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Opt { - step: isize, + skip: Option, only_if: Option, upper_bound: bool, } @@ -13,28 +12,17 @@ pub struct Opt { impl From<&Exec> for Opt { fn from(e: &Exec) -> Self { Self { - step: e.args.first().and_then(|s| s.parse().ok()).unwrap_or(0), + skip: e.args.first().and_then(|s| s.parse().ok()), only_if: e.named.get("only-if").map(Url::from), upper_bound: e.named.contains_key("upper-bound"), } } } -impl From for Opt { - fn from(step: isize) -> Self { Self { step, only_if: None, upper_bound: false } } +impl From<()> for Opt { + fn from(_: ()) -> Self { Default::default() } } impl Manager { - #[inline] - pub fn _peek_upper_bound(bound: usize, only_if: &Url) { - emit!(Call( - Exec::call("peek", vec![bound.to_string()]) - .with("only-if", only_if.to_string()) - .with_bool("upper-bound", true) - .vec(), - Layer::Manager - )); - } - pub fn peek(&mut self, opt: impl Into) -> bool { let Some(hovered) = self.hovered() else { return self.active_mut().preview.reset(); @@ -45,42 +33,30 @@ impl Manager { return false; } - if hovered.is_dir() { - return self.peek_folder(opt, hovered.url.clone()); - } - - let Some(mime) = self.mimetype.get(&hovered.url).cloned() else { + let mime = if hovered.is_dir() { + MIME_DIR.to_owned() + } else if let Some(s) = self.mimetype.get(&hovered.url) { + s.to_owned() + } else { return self.active_mut().preview.reset(); }; - let (url, cha) = (hovered.url.clone(), hovered.cha); - if opt.upper_bound { - self.active_mut().preview.arrow(0, &mime, Some(opt.step as usize)); - } else if self.active().preview.same_url(&url) { - self.active_mut().preview.arrow(opt.step, &mime, None); - } else { - self.active_mut().preview.arrow(0, &mime, Some(0)); + let hovered = hovered.clone(); + if !self.active().preview.same_url(&hovered.url) { + self.active_mut().preview.skip = 0; self.active_mut().preview.reset(); } - self.active_mut().preview.go(&url, cha, &mime); + if let Some(skip) = opt.skip { + let preview = &mut self.active_mut().preview; + if opt.upper_bound { + preview.skip = preview.skip.min(skip); + } else { + preview.skip = skip; + } + } + + self.active_mut().preview.go(hovered, mime); false } - - fn peek_folder(&mut self, opt: Opt, url: Url) -> bool { - let folder = self.active().history.get(&url); - let (skip, bound) = folder - .map(|f| (f.offset, f.files.len().saturating_sub(MANAGER.layout.folder_height()))) - .unwrap_or_default(); - - let in_chunks = folder.is_none(); - if self.active().preview.same_url(&url) { - self.active_mut().preview.arrow(opt.step, MIME_DIR, Some(bound)); - self.active_mut().preview.sync_skip() - } else { - self.active_mut().preview.arrow(skip as isize, MIME_DIR, Some(skip)); - self.active_mut().preview.go_folder(url, in_chunks); - false - } - } } diff --git a/yazi-core/src/manager/commands/rename.rs b/yazi-core/src/manager/commands/rename.rs index b820754f..17e39a3d 100644 --- a/yazi-core/src/manager/commands/rename.rs +++ b/yazi-core/src/manager/commands/rename.rs @@ -3,10 +3,11 @@ use std::{collections::BTreeMap, ffi::OsStr, io::{stdout, BufWriter, Write}, pat use anyhow::{anyhow, bail, Result}; use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}}; use yazi_config::{popup::InputCfg, OPEN, PREVIEW}; -use yazi_scheduler::{external::{self, ShellOpt}, BLOCKER}; +use yazi_plugin::external::{self, ShellOpt}; +use yazi_scheduler::{Scheduler, BLOCKER}; use yazi_shared::{emit, event::Exec, fs::{max_common_root, File, FilesOp, Url}, term::Term, Defer}; -use crate::{input::Input, manager::Manager, Ctx}; +use crate::{input::Input, manager::Manager}; pub struct Opt { force: bool, @@ -87,10 +88,10 @@ impl Manager { let _guard = BLOCKER.acquire().await.unwrap(); let _defer = Defer::new(|| { - Ctx::resume(); + Scheduler::app_resume(); tokio::spawn(fs::remove_file(tmp.clone())) }); - Ctx::stop().await; + Scheduler::app_stop().await; let mut child = external::shell(ShellOpt { cmd: (*opener.exec).into(), diff --git a/yazi-core/src/manager/commands/seek.rs b/yazi-core/src/manager/commands/seek.rs new file mode 100644 index 00000000..8687e8f5 --- /dev/null +++ b/yazi-core/src/manager/commands/seek.rs @@ -0,0 +1,40 @@ +use yazi_config::PLUGIN; +use yazi_plugin::isolate; +use yazi_shared::{event::Exec, MIME_DIR}; + +use crate::manager::Manager; + +#[derive(Debug)] +pub struct Opt { + units: i16, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { + Self { units: e.args.first().and_then(|s| s.parse().ok()).unwrap_or(0) } + } +} + +impl Manager { + pub fn seek(&mut self, opt: impl Into) -> bool { + let Some(hovered) = self.hovered() else { + return self.active_mut().preview.reset(); + }; + + let mime = if hovered.is_dir() { + MIME_DIR + } else if let Some(s) = self.mimetype.get(&hovered.url) { + s + } else { + return self.active_mut().preview.reset(); + }; + + let Some(previewer) = PLUGIN.previewer(&hovered.url, mime) else { + return self.active_mut().preview.reset(); + }; + + let opt = opt.into() as Opt; + isolate::seek_sync(&previewer.exec, hovered.clone(), opt.units); + false + } +} diff --git a/yazi-core/src/manager/commands/suspend.rs b/yazi-core/src/manager/commands/suspend.rs index f3db2bf1..4c6def2b 100644 --- a/yazi-core/src/manager/commands/suspend.rs +++ b/yazi-core/src/manager/commands/suspend.rs @@ -1,6 +1,7 @@ +use yazi_scheduler::Scheduler; use yazi_shared::event::Exec; -use crate::{manager::Manager, Ctx}; +use crate::manager::Manager; pub struct Opt; impl From<&Exec> for Opt { @@ -11,7 +12,7 @@ impl Manager { pub fn suspend(&mut self, _: impl Into) -> bool { #[cfg(unix)] tokio::spawn(async move { - Ctx::stop().await; + Scheduler::app_stop().await; unsafe { libc::raise(libc::SIGTSTP) }; }); false diff --git a/yazi-core/src/manager/commands/update_mimetype.rs b/yazi-core/src/manager/commands/update_mimetype.rs new file mode 100644 index 00000000..1ebf0f61 --- /dev/null +++ b/yazi-core/src/manager/commands/update_mimetype.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; + +use yazi_plugin::ValueSendable; +use yazi_shared::{event::Exec, fs::Url}; + +use crate::{manager::Manager, tasks::Tasks}; + +pub struct Opt { + data: ValueSendable, +} + +impl TryFrom<&Exec> for Opt { + type Error = (); + + fn try_from(e: &Exec) -> Result { Ok(Self { data: e.take_data().ok_or(())? }) } +} + +impl Manager { + pub fn update_mimetype(&mut self, opt: impl TryInto, tasks: &Tasks) -> bool { + let Ok(opt) = opt.try_into() else { + return false; + }; + + let updates: HashMap<_, _> = opt + .data + .into_table_string() + .into_iter() + .map(|(url, mime)| (Url::from(url), mime)) + .filter(|(url, mime)| self.mimetype.get(url) != Some(mime)) + .collect(); + + if updates.is_empty() { + return false; + } + + let paged = self.current().paginate(self.current().page); + tasks.preload_affected(paged, &updates); + + self.mimetype.extend(updates); + self.peek(()); + true + } +} diff --git a/yazi-core/src/manager/manager.rs b/yazi-core/src/manager/manager.rs index 4cc0c7a5..040f0d6b 100644 --- a/yazi-core/src/manager/manager.rs +++ b/yazi-core/src/manager/manager.rs @@ -1,9 +1,9 @@ -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{HashMap, HashSet}; -use yazi_shared::{fs::{File, FilesOp}, fs::Url}; +use yazi_shared::fs::{File, FilesOp, Url}; use super::{Tabs, Watcher}; -use crate::{tab::{Folder, Tab}, tasks::Tasks}; +use crate::{folder::Folder, tab::Tab}; pub struct Manager { pub tabs: Tabs, @@ -65,20 +65,6 @@ impl Manager { false } } - - pub fn update_mimetype(&mut self, mut mimes: BTreeMap, tasks: &Tasks) -> bool { - mimes.retain(|f, m| self.mimetype.get(f) != Some(m)); - if mimes.is_empty() { - return false; - } - - tasks.precache_image(&mimes); - tasks.precache_video(&mimes); - tasks.precache_pdf(&mimes); - - self.mimetype.extend(mimes); - true - } } impl Manager { diff --git a/yazi-core/src/manager/tabs.rs b/yazi-core/src/manager/tabs.rs index 0f8a882d..4b49480d 100644 --- a/yazi-core/src/manager/tabs.rs +++ b/yazi-core/src/manager/tabs.rs @@ -31,7 +31,8 @@ impl Tabs { #[inline] pub(super) fn set_idx(&mut self, idx: usize) { self.idx = idx; - self.active_mut().preview.reset_image(); + // TODO: plugin system + // self.active_mut().preview.reset_image(); Manager::_refresh(); } } diff --git a/yazi-core/src/manager/watcher.rs b/yazi-core/src/manager/watcher.rs index ac62a0c9..7cd1baf6 100644 --- a/yazi-core/src/manager/watcher.rs +++ b/yazi-core/src/manager/watcher.rs @@ -5,10 +5,9 @@ use notify::{event::{MetadataKind, ModifyKind}, EventKind, RecommendedWatcher, R use parking_lot::RwLock; use tokio::{fs, pin, sync::mpsc::{self, UnboundedReceiver}}; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; -use yazi_scheduler::external; use yazi_shared::{emit, fs::{File, FilesOp, Url}}; -use crate::files::Files; +use crate::folder::Files; pub struct Watcher { watcher: RecommendedWatcher, @@ -164,23 +163,24 @@ impl Watcher { } async fn files_changed(urls: &[Url], watched: &IndexMap>) { - let Ok(mut mimes) = external::file(urls).await else { - return; - }; + // TODO: plugin system + // let Ok(mut mimes) = external::file(urls).await else { + // return; + // }; - let linked: Vec<_> = watched.iter().filter_map(|(k, v)| v.as_ref().map(|v| (k, v))).fold( - Vec::new(), - |mut aac, (k, v)| { - mimes - .iter() - .filter(|(u, _)| u.parent().map(|p| p == **v) == Some(true)) - .for_each(|(u, m)| aac.push((k.join(u.file_name().unwrap()), m.clone()))); - aac - }, - ); + // let linked: Vec<_> = watched.iter().filter_map(|(k, v)| + // v.as_ref().map(|v| (k, v))).fold( Vec::new(), + // |mut aac, (k, v)| { + // mimes + // .iter() + // .filter(|(u, _)| u.parent().map(|p| p == **v) == Some(true)) + // .for_each(|(u, m)| aac.push((k.join(u.file_name().unwrap()), + // m.clone()))); aac + // }, + // ); - mimes.extend(linked); - emit!(Mimetype(mimes)); + // mimes.extend(linked); + // emit!(Mimetype(mimes)); } async fn dir_changed(url: &Url, watched: &IndexMap>) { diff --git a/yazi-core/src/preview/mod.rs b/yazi-core/src/preview/mod.rs deleted file mode 100644 index 2d5ba586..00000000 --- a/yazi-core/src/preview/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod preview; -mod provider; - -pub use preview::*; -use provider::*; - -pub static COLLISION: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); diff --git a/yazi-core/src/preview/preview.rs b/yazi-core/src/preview/preview.rs deleted file mode 100644 index 81ec6e01..00000000 --- a/yazi-core/src/preview/preview.rs +++ /dev/null @@ -1,140 +0,0 @@ -use std::{mem, time::Duration}; - -use tokio::{pin, task::JoinHandle}; -use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; -use yazi_adaptor::ADAPTOR; -use yazi_config::MANAGER; -use yazi_shared::{emit, event::{PreviewData, PreviewLock}, fs::{Cha, FilesOp, Url}, MimeKind, PeekError}; - -use super::Provider; -use crate::{files::Files, manager::Manager, Highlighter}; - -#[derive(Default)] -pub struct Preview { - pub lock: Option, - skip: usize, - - handle: Option>, -} - -impl Preview { - pub fn go(&mut self, url: &Url, cha: Cha, mime: &str) { - if self.content_unchanged(url, &cha) { - return; - } - - self.abort(); - let (url, kind, skip) = (url.clone(), MimeKind::new(mime), self.skip); - - self.handle = Some(tokio::spawn(async move { - match Provider::auto(kind, &url, skip).await { - Ok(data) => { - emit!(Preview(PreviewLock { url, cha: Some(cha), skip, data })); - } - Err(PeekError::Exceed(max)) => { - Manager::_peek_upper_bound(max, &url); - } - _ => {} - } - })); - } - - pub fn go_folder(&mut self, url: Url, in_chunks: bool) { - self.abort(); - self.lock = Some(PreviewLock { - url: url.clone(), - cha: None, - skip: self.skip, - data: PreviewData::Folder, - }); - - self.handle = Some(tokio::spawn(async move { - let Ok(rx) = Files::from_dir(&url).await else { - emit!(Files(FilesOp::IOErr(url.clone()))); - return; - }; - - if !in_chunks { - emit!(Files(FilesOp::Full(url.clone(), UnboundedReceiverStream::new(rx).collect().await))); - return; - } - - let stream = - UnboundedReceiverStream::new(rx).chunks_timeout(10000, Duration::from_millis(500)); - pin!(stream); - - let ticket = FilesOp::prepare(&url); - while let Some(chunk) = stream.next().await { - emit!(Files(FilesOp::Part(url.clone(), ticket, chunk))); - } - })); - } - - pub fn arrow(&mut self, step: isize, mime: &str, upper: Option) { - let size = Provider::step_size(MimeKind::new(mime), step.unsigned_abs()); - self.skip = if step < 0 { self.skip.saturating_sub(size) } else { self.skip + size }; - - if let Some(upper) = upper { - self.skip = self.skip.min(upper); - } - } - - #[inline] - pub fn abort(&mut self) { - self.handle.take().map(|h| h.abort()); - Highlighter::abort(); - ADAPTOR.image_hide(MANAGER.layout.image_rect()).ok(); - } - - #[inline] - pub fn reset(&mut self) -> bool { - self.abort(); - self.lock.take().map(|l| l.is_image()) == Some(false) - } - - pub fn reset_image(&mut self) -> bool { - if matches!(self.lock, Some(ref l) if l.is_image()) { - self.reset(); - true - } else { - false - } - } - - pub fn same_url(&self, url: &Url) -> bool { - matches!(self.lock, Some(ref lock) if lock.url == *url) - } - - pub fn sync_skip(&mut self) -> bool { - if let Some(lock) = &mut self.lock { - mem::replace(&mut lock.skip, self.skip) != self.skip - } else { - false - } - } - - fn content_unchanged(&self, url: &Url, cha: &Cha) -> bool { - let Some(lock) = &self.lock else { - return false; - }; - let Some(cha_) = &lock.cha else { - return false; - }; - - *url == lock.url - && self.skip == lock.skip - && cha.len == cha_.len - && cha.modified == cha_.modified - && cha.kind == cha_.kind - && { - #[cfg(unix)] - { - cha.permissions == cha_.permissions - } - #[cfg(windows)] - { - true - } - } - } -} diff --git a/yazi-core/src/preview/provider.rs b/yazi-core/src/preview/provider.rs deleted file mode 100644 index 09e0df68..00000000 --- a/yazi-core/src/preview/provider.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::path::Path; - -use tokio::fs; -use yazi_adaptor::ADAPTOR; -use yazi_config::{MANAGER, PREVIEW}; -use yazi_scheduler::external; -use yazi_shared::{event::PreviewData, MimeKind, PeekError}; - -use crate::Highlighter; - -pub(super) struct Provider; - -impl Provider { - pub(super) async fn auto( - kind: MimeKind, - path: &Path, - skip: usize, - ) -> Result { - match kind { - MimeKind::Empty => Err("Empty file".into()), - MimeKind::Archive => Provider::archive(path, skip).await.map(PreviewData::Text), - MimeKind::Image => Provider::image(path).await, - MimeKind::Video => Provider::video(path, skip).await, - MimeKind::JSON => Provider::json(path, skip).await.map(PreviewData::Text), - MimeKind::PDF => Provider::pdf(path, skip).await, - MimeKind::Text => Provider::highlight(path, skip).await.map(PreviewData::Text), - MimeKind::Others => Err("Unsupported mimetype".into()), - } - } - - pub(super) fn step_size(kind: MimeKind, step: usize) -> usize { - match kind { - MimeKind::Empty => 0, - MimeKind::Archive => step * MANAGER.layout.preview_height() / 10, - MimeKind::Image => 0, - MimeKind::Video => step, - MimeKind::JSON => step * MANAGER.layout.preview_height() / 10, - MimeKind::PDF => 1, - MimeKind::Text => step * MANAGER.layout.preview_height() / 10, - MimeKind::Others => step * MANAGER.layout.preview_height() / 10, - } - } - - pub(super) async fn image(path: &Path) -> Result { - ADAPTOR.image_show(path, MANAGER.layout.image_rect()).await?; - Ok(PreviewData::Image) - } - - pub(super) async fn video(path: &Path, skip: usize) -> Result { - let cache = PREVIEW.cache(path, skip); - if fs::symlink_metadata(&cache).await.is_err() { - external::ffmpegthumbnailer(path, &cache, skip).await?; - } - - Self::image(&cache).await - } - - pub(super) async fn pdf(path: &Path, skip: usize) -> Result { - let cache = PREVIEW.cache(path, skip); - if fs::symlink_metadata(&cache).await.is_err() { - external::pdftoppm(path, &cache, skip).await?; - } - - Self::image(&cache).await - } - - pub(super) async fn json(path: &Path, skip: usize) -> Result { - let result = external::jq(path, skip, MANAGER.layout.preview_height()).await; - if let Err(PeekError::Unexpected(_)) = result { - return Self::highlight(path, skip).await; - } - result - } - - pub(super) async fn archive(path: &Path, skip: usize) -> Result { - Ok( - external::lsar(path, skip, MANAGER.layout.preview_height()) - .await? - .into_iter() - .map(|f| f.name) - .collect::>() - .join("\n"), - ) - } - - pub(super) async fn highlight(path: &Path, skip: usize) -> Result { - let limit = MANAGER.layout.preview_height(); - let result = Highlighter::new(path.to_owned()).highlight(skip, limit).await?; - Ok(result.replace('\t', &" ".repeat(PREVIEW.tab_size as usize))) - } -} diff --git a/yazi-core/src/step.rs b/yazi-core/src/step.rs index 153d637e..1379d415 100644 --- a/yazi-core/src/step.rs +++ b/yazi-core/src/step.rs @@ -36,11 +36,11 @@ impl Step { impl Step { #[inline] - pub fn add usize>(self, pos: usize, f: F) -> usize { + pub fn add(self, pos: usize, limit: usize) -> usize { let fixed = match self { Self::Fixed(n) => n, Self::Percent(0) => 0, - Self::Percent(n) => n as isize * f() as isize / 100, + Self::Percent(n) => n as isize * limit as isize / 100, }; if fixed > 0 { pos + fixed as usize } else { pos.saturating_sub(fixed.unsigned_abs()) } } diff --git a/yazi-core/src/tab/commands/cd.rs b/yazi-core/src/tab/commands/cd.rs index c4e3f40b..dc173f69 100644 --- a/yazi-core/src/tab/commands/cd.rs +++ b/yazi-core/src/tab/commands/cd.rs @@ -35,7 +35,7 @@ impl Tab { pub fn cd(&mut self, opt: impl Into) -> bool { let opt = opt.into() as Opt; if opt.interactive { - return self.cd_interactive(opt); + return self.cd_interactive(); } if self.current.cwd == opt.target { @@ -68,11 +68,9 @@ impl Tab { true } - fn cd_interactive(&mut self, opt: impl Into) -> bool { - let opt = opt.into() as Opt; - + fn cd_interactive(&mut self) -> bool { tokio::spawn(async move { - let rx = Input::_show(InputCfg::cd().with_value(opt.target.to_string_lossy())); + let rx = Input::_show(InputCfg::cd()); let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50)); pin!(rx); diff --git a/yazi-core/src/tab/commands/jump.rs b/yazi-core/src/tab/commands/jump.rs index 0700daf0..8a2913d9 100644 --- a/yazi-core/src/tab/commands/jump.rs +++ b/yazi-core/src/tab/commands/jump.rs @@ -1,7 +1,8 @@ -use yazi_scheduler::{external::{self, FzfOpt, ZoxideOpt}, BLOCKER}; +use yazi_plugin::external::{self, FzfOpt, ZoxideOpt}; +use yazi_scheduler::{Scheduler, BLOCKER}; use yazi_shared::{event::Exec, fs::ends_with_slash, Defer}; -use crate::{tab::Tab, Ctx}; +use crate::tab::Tab; pub struct Opt { type_: OptType, @@ -36,8 +37,8 @@ impl Tab { let cwd = self.current.cwd.clone(); tokio::spawn(async move { let _guard = BLOCKER.acquire().await.unwrap(); - let _defer = Defer::new(Ctx::resume); - Ctx::stop().await; + let _defer = Defer::new(Scheduler::app_resume); + Scheduler::app_stop().await; let result = if opt.type_ == OptType::Fzf { external::fzf(FzfOpt { cwd }).await diff --git a/yazi-core/src/tab/commands/mod.rs b/yazi-core/src/tab/commands/mod.rs index fa8d1c39..86834755 100644 --- a/yazi-core/src/tab/commands/mod.rs +++ b/yazi-core/src/tab/commands/mod.rs @@ -9,6 +9,7 @@ mod hidden; mod jump; mod leave; mod linemode; +mod preview; mod reveal; mod search; mod select; diff --git a/yazi-core/src/tab/commands/preview.rs b/yazi-core/src/tab/commands/preview.rs new file mode 100644 index 00000000..a23ae3c2 --- /dev/null +++ b/yazi-core/src/tab/commands/preview.rs @@ -0,0 +1,36 @@ +use anyhow::anyhow; +use yazi_plugin::utils::PreviewLock; +use yazi_shared::event::Exec; + +use crate::tab::Tab; + +pub struct Opt { + lock: PreviewLock, +} + +impl TryFrom<&Exec> for Opt { + type Error = anyhow::Error; + + fn try_from(e: &Exec) -> Result { + Ok(Self { lock: e.take_data().ok_or_else(|| anyhow!("invalid data"))? }) + } +} + +impl Tab { + pub fn preview(&mut self, opt: impl TryInto) -> bool { + let Some(hovered) = self.current.hovered().map(|h| &h.url) else { + return self.preview.reset(); + }; + + let Ok(opt) = opt.try_into() else { + return false; + }; + + if hovered != &opt.lock.url { + return false; + } + + self.preview.lock = Some(opt.lock); + true + } +} diff --git a/yazi-core/src/tab/commands/search.rs b/yazi-core/src/tab/commands/search.rs index 69e2176f..fa7fe25d 100644 --- a/yazi-core/src/tab/commands/search.rs +++ b/yazi-core/src/tab/commands/search.rs @@ -4,7 +4,7 @@ use anyhow::bail; use tokio::pin; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; use yazi_config::popup::InputCfg; -use yazi_scheduler::external; +use yazi_plugin::external; use yazi_shared::{emit, event::Exec, fs::FilesOp}; use crate::{input::Input, manager::Manager, tab::Tab}; @@ -78,7 +78,8 @@ impl Tab { handle.abort(); } if self.current.cwd.is_search() { - self.preview.reset_image(); + // TODO: plugin system + // self.preview.reset_image(); let rep = self.history_new(&self.current.cwd.to_regular()); drop(mem::replace(&mut self.current, rep)); diff --git a/yazi-core/src/tab/config.rs b/yazi-core/src/tab/config.rs index 5f6f11d2..9ac497e2 100644 --- a/yazi-core/src/tab/config.rs +++ b/yazi-core/src/tab/config.rs @@ -1,6 +1,6 @@ use yazi_config::{manager::SortBy, MANAGER}; -use crate::files::FilesSorter; +use crate::folder::FilesSorter; #[derive(Clone, PartialEq)] pub struct Config { diff --git a/yazi-core/src/tab/finder.rs b/yazi-core/src/tab/finder.rs index 8ed6101c..f0462469 100644 --- a/yazi-core/src/tab/finder.rs +++ b/yazi-core/src/tab/finder.rs @@ -4,7 +4,7 @@ use anyhow::Result; use regex::bytes::{Regex, RegexBuilder}; use yazi_shared::fs::Url; -use crate::files::Files; +use crate::folder::Files; #[derive(PartialEq, Eq)] pub enum FinderCase { diff --git a/yazi-core/src/tab/mod.rs b/yazi-core/src/tab/mod.rs index ebcd998e..b002a2fa 100644 --- a/yazi-core/src/tab/mod.rs +++ b/yazi-core/src/tab/mod.rs @@ -2,13 +2,13 @@ mod backstack; mod commands; mod config; mod finder; -mod folder; mod mode; +mod preview; mod tab; pub use backstack::*; pub use config::*; pub use finder::*; -pub use folder::*; pub use mode::*; +pub use preview::*; pub use tab::*; diff --git a/yazi-core/src/tab/preview.rs b/yazi-core/src/tab/preview.rs new file mode 100644 index 00000000..3658be90 --- /dev/null +++ b/yazi-core/src/tab/preview.rs @@ -0,0 +1,72 @@ +use tokio_util::sync::CancellationToken; +use yazi_adaptor::ADAPTOR; +use yazi_config::{LAYOUT, PLUGIN}; +use yazi_plugin::{external::Highlighter, utils::PreviewLock}; +use yazi_shared::fs::{Cha, File, Url}; + +#[derive(Default)] +pub struct Preview { + pub lock: Option, + pub skip: usize, + + previewer_ct: Option, +} + +impl Preview { + pub fn go(&mut self, file: File, mime: String) { + if self.content_unchanged(&file.url, &file.cha) { + return; + } + + self.abort(); + let Some(previewer) = PLUGIN.previewer(&file.url, &mime) else { + return; + }; + + if previewer.sync { + yazi_plugin::isolate::peek_sync(&previewer.exec, file, self.skip); + } else { + self.previewer_ct = Some(yazi_plugin::isolate::peek(&previewer.exec, file, self.skip)); + } + } + + #[inline] + pub fn abort(&mut self) { + self.previewer_ct.take().map(|ct| ct.cancel()); + Highlighter::abort(); + } + + #[inline] + pub fn reset(&mut self) -> bool { + self.abort(); + ADAPTOR.image_hide(LAYOUT.load().preview).ok(); + self.lock.take().is_some() + } + + #[inline] + pub fn same_url(&self, url: &Url) -> bool { + matches!(self.lock, Some(ref lock) if lock.url == *url) + } + + fn content_unchanged(&self, url: &Url, cha: &Cha) -> bool { + let Some(lock) = &self.lock else { + return false; + }; + + *url == lock.url + && self.skip == lock.skip + && cha.len == lock.cha.len + && cha.modified == lock.cha.modified + && cha.kind == lock.cha.kind + && { + #[cfg(unix)] + { + cha.permissions == lock.cha.permissions + } + #[cfg(windows)] + { + true + } + } + } +} diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index 72adf988..238829c6 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -2,10 +2,10 @@ use std::{borrow::Cow, collections::BTreeMap}; use anyhow::Result; use tokio::task::JoinHandle; -use yazi_shared::{event::PreviewLock, fs::File, fs::Url}; +use yazi_shared::fs::{File, Url}; -use super::{Backstack, Config, Finder, Folder, Mode}; -use crate::preview::Preview; +use super::{Backstack, Config, Finder, Mode, Preview}; +use crate::folder::Folder; pub struct Tab { pub mode: Mode, @@ -46,21 +46,6 @@ impl From<&Url> for Tab { fn from(url: &Url) -> Self { Self::from(url.clone()) } } -impl Tab { - pub fn update_preview(&mut self, lock: PreviewLock) -> bool { - let Some(hovered) = self.current.hovered().map(|h| &h.url) else { - return self.preview.reset(); - }; - - if lock.url != *hovered { - return false; - } - - self.preview.lock = Some(lock); - true - } -} - impl Tab { // --- Mode #[inline] diff --git a/yazi-core/src/tasks/commands/inspect.rs b/yazi-core/src/tasks/commands/inspect.rs index e93f5479..df72508e 100644 --- a/yazi-core/src/tasks/commands/inspect.rs +++ b/yazi-core/src/tasks/commands/inspect.rs @@ -2,10 +2,10 @@ use std::io::{stdout, Write}; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use tokio::{io::{stdin, AsyncReadExt}, select, sync::mpsc, time}; -use yazi_scheduler::BLOCKER; +use yazi_scheduler::{Scheduler, BLOCKER}; use yazi_shared::{event::Exec, term::Term, Defer}; -use crate::{tasks::Tasks, Ctx}; +use crate::tasks::Tasks; pub struct Opt; @@ -32,10 +32,10 @@ impl Tasks { task.logs.clone() }; - Ctx::stop().await; + Scheduler::app_stop().await; let _defer = Defer::new(|| { disable_raw_mode().ok(); - Ctx::resume(); + Scheduler::app_resume(); }); Term::clear(&mut stdout()).ok(); diff --git a/yazi-core/src/tasks/commands/open.rs b/yazi-core/src/tasks/commands/open.rs index 513945f4..bd0ac9a8 100644 --- a/yazi-core/src/tasks/commands/open.rs +++ b/yazi-core/src/tasks/commands/open.rs @@ -1,7 +1,7 @@ use std::ffi::OsString; use anyhow::anyhow; -use yazi_config::{open::Opener, BOOT}; +use yazi_config::{open::Opener, ARGS}; use yazi_shared::{emit, event::Exec, Layer}; use crate::tasks::Tasks; @@ -29,7 +29,7 @@ impl Tasks { return false; }; - if let Some(p) = &BOOT.chooser_file { + if let Some(p) = &ARGS.chooser_file { let paths = opt.targets.into_iter().fold(OsString::new(), |mut s, (p, _)| { s.push(p); s.push("\n"); diff --git a/yazi-core/src/tasks/tasks.rs b/yazi-core/src/tasks/tasks.rs index d993254c..8e669a26 100644 --- a/yazi-core/src/tasks/tasks.rs +++ b/yazi-core/src/tasks/tasks.rs @@ -1,13 +1,13 @@ -use std::{collections::{BTreeMap, HashMap, HashSet}, ffi::OsStr, path::Path, sync::Arc, time::Duration}; +use std::{collections::{BTreeMap, HashMap, HashSet}, ffi::OsStr, mem, path::Path, sync::Arc, time::Duration}; use tokio::time::sleep; use tracing::debug; -use yazi_config::{manager::SortBy, open::Opener, popup::InputCfg, OPEN}; +use yazi_config::{manager::SortBy, open::Opener, plugin::{PluginRule, MAX_PRELOADERS}, popup::InputCfg, OPEN, PLUGIN}; use yazi_scheduler::{Scheduler, TaskSummary}; -use yazi_shared::{fs::{File, Url}, term::Term, MimeKind}; +use yazi_shared::{fs::{File, Url}, term::Term, MIME_DIR}; use super::{TasksProgress, TASKS_PADDING, TASKS_PERCENT}; -use crate::{files::Files, input::Input}; +use crate::{folder::Files, input::Input}; pub struct Tasks { pub(super) scheduler: Arc, @@ -149,76 +149,94 @@ impl Tasks { false } - #[inline] - pub fn precache_size(&self, targets: &Files) -> bool { + pub fn plugin_micro(&self, name: &str) -> bool { + self.scheduler.plugin_micro(name.to_owned()); + false + } + + pub fn plugin_macro(&self, name: &str) -> bool { + self.scheduler.plugin_macro(name.to_owned()); + false + } + + pub fn preload_paged(&self, paged: &[File], mimetype: &HashMap) { + let mut single_tasks = Vec::with_capacity(paged.len()); + let mut multi_tasks: [Vec<_>; MAX_PRELOADERS as usize] = Default::default(); + + let loaded = self.scheduler.preload.rule_loaded.read(); + for f in paged { + let mime = if f.is_dir() { Some(MIME_DIR) } else { mimetype.get(&f.url).map(|s| &**s) }; + let factors = |s: &str| match s { + "mime" => mime.is_some(), + _ => false, + }; + + for rule in PLUGIN.preloaders(&f.url, mime, factors) { + if loaded.get(&f.url).is_some_and(|x| x & 1 << rule.id != 0) { + continue; + } + if rule.multi { + multi_tasks[rule.id as usize].push(f); + } else { + single_tasks.push((rule, f)); + } + } + } + + drop(loaded); + let mut loaded = self.scheduler.preload.rule_loaded.write(); + + let mut go = |rule: &PluginRule, targets: Vec<&File>| { + for &f in &targets { + *loaded.entry(f.url.clone()).or_default() |= 1 << rule.id; + } + self.scheduler.preload_paged(rule, targets); + }; + + for i in 0..PLUGIN.preloaders.len() { + if !multi_tasks[i].is_empty() { + go(&PLUGIN.preloaders[i], mem::take(&mut multi_tasks[i])); + } + } + for (rule, target) in single_tasks { + go(rule, vec![target]); + } + } + + pub fn preload_affected(&self, paged: &[File], mimetype: &HashMap) { + { + let mut loaded = self.scheduler.preload.rule_loaded.write(); + for u in mimetype.keys() { + loaded.remove(u); + } + } + + self.preload_paged(paged, mimetype); + } + + pub fn preload_sorted(&self, targets: &Files) { if targets.sorter().by != SortBy::Size { - return false; + return; } - let targets: Vec<_> = targets - .iter() - .filter(|f| f.is_dir() && !targets.sizes.contains_key(&f.url)) - .map(|f| &f.url) - .collect(); - - if !targets.is_empty() { - self.scheduler.precache_size(targets); + let targets: Vec<_> = { + let loading = self.scheduler.preload.size_loading.read(); + targets + .iter() + .filter(|f| f.is_dir() && !targets.sizes.contains_key(&f.url) && !loading.contains(&f.url)) + .map(|f| &f.url) + .collect() + }; + if targets.is_empty() { + return; } - false - } - - #[inline] - pub fn precache_mime(&self, targets: &[File], mimetype: &HashMap) -> bool { - let targets: Vec<_> = targets - .iter() - .filter(|f| !f.is_dir() && !mimetype.contains_key(&f.url)) - .map(|f| f.url()) - .collect(); - - if !targets.is_empty() { - self.scheduler.precache_mime(targets); + let mut loading = self.scheduler.preload.size_loading.write(); + for &target in &targets { + loading.insert(target.clone()); } - false - } - pub fn precache_image(&self, mimetype: &BTreeMap) -> bool { - let targets: Vec<_> = mimetype - .iter() - .filter(|(_, m)| MimeKind::new(m) == MimeKind::Image) - .map(|(u, _)| u.clone()) - .collect(); - - if !targets.is_empty() { - self.scheduler.precache_image(targets); - } - false - } - - pub fn precache_video(&self, mimetype: &BTreeMap) -> bool { - let targets: Vec<_> = mimetype - .iter() - .filter(|(_, m)| MimeKind::new(m) == MimeKind::Video) - .map(|(u, _)| u.clone()) - .collect(); - - if !targets.is_empty() { - self.scheduler.precache_video(targets); - } - false - } - - pub fn precache_pdf(&self, mimetype: &BTreeMap) -> bool { - let targets: Vec<_> = mimetype - .iter() - .filter(|(_, m)| MimeKind::new(m) == MimeKind::PDF) - .map(|(u, _)| u.clone()) - .collect(); - - if !targets.is_empty() { - self.scheduler.precache_pdf(targets); - } - false + self.scheduler.preload_size(targets); } } diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index 55ab098c..e33abb02 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -9,11 +9,12 @@ homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" [dependencies] -yazi-adaptor = { path = "../yazi-adaptor", version = "0.1.5" } -yazi-config = { path = "../yazi-config", version = "0.1.5" } -yazi-core = { path = "../yazi-core", version = "0.1.5" } -yazi-plugin = { path = "../yazi-plugin", version = "0.1.5" } -yazi-shared = { path = "../yazi-shared", version = "0.1.5" } +yazi-adaptor = { path = "../yazi-adaptor", version = "0.1.5" } +yazi-config = { path = "../yazi-config", version = "0.1.5" } +yazi-core = { path = "../yazi-core", version = "0.1.5" } +yazi-plugin = { path = "../yazi-plugin", version = "0.1.5" } +yazi-scheduler = { path = "../yazi-scheduler", version = "0.1.5" } +yazi-shared = { path = "../yazi-shared", version = "0.1.5" } # External dependencies ansi-to-tui = "^3" @@ -35,6 +36,12 @@ tracing-subscriber = "^0" libc = "^0" signal-hook-tokio = { version = "^0", features = [ "futures-v0_3" ] } +[target.'cfg(any(target_arch="riscv64", target_arch="loongarch64"))'.dependencies] +mlua = { version = "^0", features = [ "lua52", "vendored" ] } + +[target.'cfg(not(any(target_arch="riscv64", target_arch="loongarch64")))'.dependencies] +mlua = { version = "^0", features = [ "luajit52", "vendored" ] } + [[bin]] name = "yazi" path = "src/main.rs" diff --git a/yazi-fm/src/app/app.rs b/yazi-fm/src/app/app.rs index c8107b03..763e31a2 100644 --- a/yazi-fm/src/app/app.rs +++ b/yazi-fm/src/app/app.rs @@ -3,11 +3,11 @@ use std::sync::atomic::Ordering; use anyhow::{Ok, Result}; use crossterm::event::KeyEvent; use ratatui::{backend::Backend, prelude::Rect}; -use yazi_config::{keymap::Key, BOOT}; -use yazi_core::{input::InputMode, preview::COLLISION, Ctx}; -use yazi_shared::{emit, event::{Event, Exec}, fs::FilesOp, term::Term, Layer}; +use yazi_config::{keymap::Key, ARGS}; +use yazi_core::input::InputMode; +use yazi_shared::{emit, event::{Event, Exec}, fs::FilesOp, term::Term, Layer, COLLISION}; -use crate::{Executor, Logs, Panic, Root, Signals}; +use crate::{lives::Lives, Ctx, Executor, Logs, Panic, Root, Signals}; pub(crate) struct App { pub(crate) cx: Ctx, @@ -19,11 +19,14 @@ impl App { pub(crate) async fn run() -> Result<()> { Panic::install(); let _log = Logs::init()?; - let term = Term::start()?; + let term = Term::start()?; let signals = Signals::start()?; + + Lives::register()?; let mut app = Self { cx: Ctx::make(), term: Some(term), signals }; + app.dispatch_render()?; while let Some(event) = app.signals.recv().await { match event { Event::Quit(no_cwd_file) => { @@ -42,7 +45,7 @@ impl App { } fn dispatch_quit(&mut self, no_cwd_file: bool) { - if let Some(p) = BOOT.cwd_file.as_ref().filter(|_| !no_cwd_file) { + if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no_cwd_file) { let cwd = self.cx.manager.cwd().as_os_str(); std::fs::write(p, cwd.as_encoded_bytes()).ok(); } @@ -72,7 +75,7 @@ impl App { let collision = COLLISION.swap(false, Ordering::Relaxed); let frame = term.draw(|f| { - yazi_plugin::scope(&self.cx, |_| { + Lives::scope(&self.cx, |_| { f.render_widget(Root::new(&self.cx), f.size()); }); @@ -83,8 +86,9 @@ impl App { if !COLLISION.load(Ordering::Relaxed) { if collision { // Reload preview if collision is resolved - self.cx.manager.active_mut().preview.reset_image(); - self.cx.manager.peek(0); + // TODO: plugin system + // self.cx.manager.active_mut().preview.reset_image(); + self.cx.manager.peek(()); } return Ok(()); } @@ -115,7 +119,7 @@ impl App { self.cx.manager.current_mut().set_page(true); self.cx.manager.active_mut().preview.reset(); - self.cx.manager.peek(0); + self.cx.manager.peek(()); emit!(Render); } @@ -140,23 +144,12 @@ impl App { emit!(Render); } if calc { - tasks.precache_size(&manager.current().files); + tasks.preload_sorted(&manager.current().files); } } Event::Pages(page) => { let targets = self.cx.manager.current().paginate(page); - tasks.precache_mime(targets, &self.cx.manager.mimetype); - } - Event::Mimetype(mimes) => { - if manager.update_mimetype(mimes, tasks) { - emit!(Render); - manager.peek(0); - } - } - Event::Preview(lock) => { - if manager.active_mut().update_preview(lock) { - emit!(Render); - } + tasks.preload_paged(targets, &self.cx.manager.mimetype); } _ => unreachable!(), } diff --git a/yazi-fm/src/app/commands/mod.rs b/yazi-fm/src/app/commands/mod.rs index eeec0cfb..4fddf53d 100644 --- a/yazi-fm/src/app/commands/mod.rs +++ b/yazi-fm/src/app/commands/mod.rs @@ -1 +1,2 @@ +mod plugin; mod stop; diff --git a/yazi-fm/src/app/commands/plugin.rs b/yazi-fm/src/app/commands/plugin.rs new file mode 100644 index 00000000..23350a6b --- /dev/null +++ b/yazi-fm/src/app/commands/plugin.rs @@ -0,0 +1,62 @@ +use mlua::{ExternalError, ExternalResult, IntoLua, Table, TableExt, Value, Variadic}; +use tracing::error; +use yazi_plugin::{LOADED, LUA}; +use yazi_shared::{emit, event::Exec, Layer}; + +use crate::{app::App, lives::Lives}; + +impl App { + pub(crate) fn plugin(&mut self, opt: impl TryInto) -> bool { + let Ok(opt) = opt.try_into() else { + return false; + }; + + if !opt.sync { + return self.cx.tasks.plugin_micro(&opt.name); + } + + tokio::spawn(async move { + if LOADED.ensure(&opt.name).await.is_ok() { + emit!(Call(Exec::call("plugin_do", vec![opt.name]).with_data(opt.data).vec(), Layer::App)); + } + }); + false + } + + pub(crate) fn plugin_do(&mut self, opt: impl TryInto) -> bool { + let Ok(opt) = opt.try_into() else { + return false; + }; + + let args = Variadic::from_iter(opt.data.args.into_iter().filter_map(|v| v.into_lua(&LUA).ok())); + let mut ret: mlua::Result = Err("uninitialized plugin".into_lua_err()); + + Lives::scope(&self.cx, |_| { + let mut plugin: Option
= None; + if let Some(b) = LOADED.read().get(&opt.name) { + match LUA.load(b).call(()) { + Ok(t) => plugin = Some(t), + Err(e) => ret = Err(e), + } + } + if let Some(plugin) = plugin { + ret = + if let Some(cb) = opt.data.cb { cb(plugin) } else { plugin.call_method("entry", args) }; + } + }); + + if let Err(e) = ret { + error!("{e}"); + return false; + } + + let Some(tx) = opt.data.tx else { + return false; + }; + + if let Ok(v) = ret.and_then(|v| v.try_into().into_lua_err()) { + tx.send(v).ok(); + } + false + } +} diff --git a/yazi-fm/src/app/commands/stop.rs b/yazi-fm/src/app/commands/stop.rs index 591335cc..3b19f3cf 100644 --- a/yazi-fm/src/app/commands/stop.rs +++ b/yazi-fm/src/app/commands/stop.rs @@ -24,7 +24,8 @@ impl App { return false; }; - self.cx.manager.active_mut().preview.reset_image(); + // TODO: plugin system + // self.cx.manager.active_mut().preview.reset_image(); if opt.state { self.signals.stop_term(true); self.term = None; diff --git a/yazi-fm/src/completion/completion.rs b/yazi-fm/src/completion/completion.rs index f10bd83b..909eb6e1 100644 --- a/yazi-fm/src/completion/completion.rs +++ b/yazi-fm/src/completion/completion.rs @@ -2,9 +2,8 @@ use std::path::MAIN_SEPARATOR; use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, List, ListItem, Widget}}; use yazi_config::{popup::{Offset, Position}, THEME}; -use yazi_core::Ctx; -use crate::widgets; +use crate::{widgets, Ctx}; pub(crate) struct Completion<'a> { cx: &'a Ctx, diff --git a/yazi-fm/src/components/header.rs b/yazi-fm/src/components/header.rs new file mode 100644 index 00000000..a9d22e2a --- /dev/null +++ b/yazi-fm/src/components/header.rs @@ -0,0 +1,20 @@ +use mlua::{Table, TableExt}; +use ratatui::{prelude::Buffer, widgets::Widget}; +use tracing::error; +use yazi_plugin::{bindings::Cast, elements::{render_widgets, Rect}, LUA}; + +pub(crate) struct Header; + +impl Widget for Header { + fn render(self, area: ratatui::layout::Rect, buf: &mut Buffer) { + let mut f = || { + let area = Rect::cast(&LUA, area)?; + let comp: Table = LUA.globals().get("Header")?; + render_widgets(comp.call_method("render", area)?, buf); + Ok::<_, anyhow::Error>(()) + }; + if let Err(e) = f() { + error!("{:?}", e); + } + } +} diff --git a/yazi-fm/src/components/manager.rs b/yazi-fm/src/components/manager.rs new file mode 100644 index 00000000..b613196a --- /dev/null +++ b/yazi-fm/src/components/manager.rs @@ -0,0 +1,20 @@ +use mlua::{Table, TableExt}; +use ratatui::{prelude::Buffer, widgets::Widget}; +use tracing::error; +use yazi_plugin::{bindings::Cast, elements::{render_widgets, Rect}, LUA}; + +pub(crate) struct Manager; + +impl Widget for Manager { + fn render(self, area: ratatui::layout::Rect, buf: &mut Buffer) { + let mut f = || { + let area = Rect::cast(&LUA, area)?; + let comp: Table = LUA.globals().get("Manager")?; + render_widgets(comp.call_method("render", area)?, buf); + Ok::<_, anyhow::Error>(()) + }; + if let Err(e) = f() { + error!("{:?}", e); + } + } +} diff --git a/yazi-fm/src/components/mod.rs b/yazi-fm/src/components/mod.rs new file mode 100644 index 00000000..95b2d4be --- /dev/null +++ b/yazi-fm/src/components/mod.rs @@ -0,0 +1,11 @@ +#![allow(clippy::module_inception)] + +mod header; +mod manager; +mod preview; +mod status; + +pub(super) use header::*; +pub(super) use manager::*; +pub(super) use preview::*; +pub(super) use status::*; diff --git a/yazi-fm/src/components/preview.rs b/yazi-fm/src/components/preview.rs new file mode 100644 index 00000000..735c6582 --- /dev/null +++ b/yazi-fm/src/components/preview.rs @@ -0,0 +1,25 @@ +use ratatui::{prelude::Buffer, widgets::Widget}; + +use crate::Ctx; + +pub(crate) struct Preview<'a> { + cx: &'a Ctx, +} + +impl<'a> Preview<'a> { + #[inline] + pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } } +} + +impl Widget for Preview<'_> { + fn render(self, _: ratatui::layout::Rect, buf: &mut Buffer) { + let preview = &self.cx.manager.active().preview; + let Some(lock) = &preview.lock else { + return; + }; + + for w in &lock.data { + w.clone_render(buf); + } + } +} diff --git a/yazi-fm/src/components/status.rs b/yazi-fm/src/components/status.rs new file mode 100644 index 00000000..4452c3e6 --- /dev/null +++ b/yazi-fm/src/components/status.rs @@ -0,0 +1,20 @@ +use mlua::{Table, TableExt}; +use ratatui::widgets::Widget; +use tracing::error; +use yazi_plugin::{bindings::Cast, elements::{render_widgets, Rect}, LUA}; + +pub(crate) struct Status; + +impl Widget for Status { + fn render(self, area: ratatui::layout::Rect, buf: &mut ratatui::prelude::Buffer) { + let mut f = || { + let area = Rect::cast(&LUA, area)?; + let comp: Table = LUA.globals().get("Status")?; + render_widgets(comp.call_method("render", area)?, buf); + Ok::<_, anyhow::Error>(()) + }; + if let Err(e) = f() { + error!("{:?}", e); + } + } +} diff --git a/yazi-core/src/context.rs b/yazi-fm/src/context.rs similarity index 81% rename from yazi-core/src/context.rs rename to yazi-fm/src/context.rs index 373666f6..39319be7 100644 --- a/yazi-core/src/context.rs +++ b/yazi-fm/src/context.rs @@ -1,7 +1,6 @@ use ratatui::prelude::Rect; use yazi_config::popup::{Origin, Position}; - -use crate::{completion::Completion, help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which}; +use yazi_core::{completion::Completion, help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which}; pub struct Ctx { pub manager: Manager, @@ -26,12 +25,6 @@ impl Ctx { } } - #[inline] - pub async fn stop() { yazi_scheduler::Scheduler::app_stop().await } - - #[inline] - pub fn resume() { yazi_scheduler::Scheduler::app_resume() } - pub fn area(&self, position: &Position) -> Rect { if position.origin != Origin::Hovered { return position.rect(); diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 8628aec6..dbabe75a 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -84,6 +84,8 @@ impl<'a> Executor<'a> { }; } + on!(plugin); + on!(plugin_do); on!(stop); false @@ -108,13 +110,16 @@ impl<'a> Executor<'a> { }; } - on!(MANAGER, peek); + on!(MANAGER, update_mimetype, &self.app.cx.tasks); on!(MANAGER, hover); + on!(MANAGER, peek); + on!(MANAGER, seek); on!(MANAGER, refresh); on!(MANAGER, quit, &self.app.cx.tasks); on!(MANAGER, close, &self.app.cx.tasks); on!(MANAGER, suspend); on!(ACTIVE, escape); + on!(ACTIVE, preview); // Navigation on!(ACTIVE, arrow); diff --git a/yazi-fm/src/help/bindings.rs b/yazi-fm/src/help/bindings.rs index d990c143..fbd5dd51 100644 --- a/yazi-fm/src/help/bindings.rs +++ b/yazi-fm/src/help/bindings.rs @@ -1,6 +1,7 @@ use ratatui::{layout::{self, Constraint}, prelude::{Buffer, Direction, Rect}, widgets::{List, ListItem, Widget}}; use yazi_config::THEME; -use yazi_core::Ctx; + +use crate::Ctx; pub(super) struct Bindings<'a> { cx: &'a Ctx, diff --git a/yazi-fm/src/help/layout.rs b/yazi-fm/src/help/layout.rs index 7aa4ab1a..a34d054f 100644 --- a/yazi-fm/src/help/layout.rs +++ b/yazi-fm/src/help/layout.rs @@ -1,9 +1,8 @@ use ratatui::{buffer::Buffer, layout::{self, Rect}, prelude::{Constraint, Direction}, widgets::{Paragraph, Widget}}; use yazi_config::THEME; -use yazi_core::Ctx; use super::Bindings; -use crate::widgets; +use crate::{Ctx, widgets}; pub(crate) struct Layout<'a> { cx: &'a Ctx, diff --git a/yazi-fm/src/input/input.rs b/yazi-fm/src/input/input.rs index c1858fa7..7e551bdb 100644 --- a/yazi-fm/src/input/input.rs +++ b/yazi-fm/src/input/input.rs @@ -3,10 +3,10 @@ use std::ops::Range; use ansi_to_tui::IntoText; use ratatui::{buffer::Buffer, layout::Rect, text::{Line, Text}, widgets::{Block, BorderType, Borders, Paragraph, Widget}}; use yazi_config::THEME; -use yazi_core::{input::InputMode, Ctx}; +use yazi_core::input::InputMode; use yazi_shared::term::Term; -use crate::widgets; +use crate::{Ctx, widgets}; pub(crate) struct Input<'a> { cx: &'a Ctx, diff --git a/yazi-fm/src/lives/active.rs b/yazi-fm/src/lives/active.rs new file mode 100644 index 00000000..c36414b5 --- /dev/null +++ b/yazi-fm/src/lives/active.rs @@ -0,0 +1,78 @@ +use mlua::{AnyUserData, Lua, MetaMethod, UserDataFields, UserDataMethods, Value}; +use yazi_config::LAYOUT; + +use super::Folder; + +pub struct Active<'a, 'b> { + scope: &'b mlua::Scope<'a, 'a>, + + inner: &'a yazi_core::tab::Tab, +} + +impl<'a, 'b> Active<'a, 'b> { + pub(super) fn register(lua: &Lua) -> mlua::Result<()> { + lua.register_userdata_type::(|reg| { + reg.add_field_method_get("is_select", |_, me| Ok(me.is_select())); + reg.add_field_method_get("is_unset", |_, me| Ok(me.is_unset())); + reg.add_field_method_get("is_visual", |_, me| Ok(me.is_visual())); + reg.add_method("pending", |_, me, (idx, state): (usize, bool)| Ok(me.pending(idx, state))); + + reg.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.to_string())); + })?; + + lua.register_userdata_type::(|reg| { + reg.add_field_method_get("sort_by", |_, me| Ok(me.sort_by.to_string())); + reg.add_field_method_get("sort_sensitive", |_, me| Ok(me.sort_sensitive)); + reg.add_field_method_get("sort_reverse", |_, me| Ok(me.sort_reverse)); + reg.add_field_method_get("sort_dir_first", |_, me| Ok(me.sort_dir_first)); + + reg.add_field_method_get("linemode", |_, me| Ok(me.linemode.to_owned())); + reg.add_field_method_get("show_hidden", |_, me| Ok(me.show_hidden)); + })?; + + lua.register_userdata_type::(|reg| { + reg.add_field_method_get("skip", |_, me| Ok(me.skip)); + reg.add_field_function_get("folder", |_, me| me.named_user_value::("folder")); + })?; + + Ok(()) + } +} + +impl<'a, 'b> Active<'a, 'b> { + pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, inner: &'a yazi_core::tab::Tab) -> Self { + Self { scope, inner } + } + + pub(crate) fn make(&self) -> mlua::Result> { + let ud = self.scope.create_any_userdata_ref(self.inner)?; + ud.set_named_user_value("mode", self.scope.create_any_userdata_ref(&self.inner.mode)?)?; + ud.set_named_user_value("conf", self.scope.create_any_userdata_ref(&self.inner.conf)?)?; + ud.set_named_user_value( + "parent", + self.inner.parent.as_ref().and_then(|p| Folder::new(self.scope, p).make(None).ok()), + )?; + ud.set_named_user_value("current", Folder::new(self.scope, &self.inner.current).make(None)?)?; + ud.set_named_user_value("preview", self.preview(self.inner)?)?; + + Ok(ud) + } + + fn preview(&self, tab: &'a yazi_core::tab::Tab) -> mlua::Result> { + let inner = &tab.preview; + let window = || inner.lock.as_ref().map(|l| (l.skip, LAYOUT.load().preview.height as usize)); + + let ud = self.scope.create_any_userdata_ref(inner)?; + ud.set_named_user_value( + "folder", + tab + .current + .hovered() + .filter(|&f| f.is_dir()) + .and_then(|f| tab.history(&f.url)) + .and_then(|f| Folder::new(self.scope, f).make(window()).ok()), + )?; + + Ok(ud) + } +} diff --git a/yazi-fm/src/lives/folder.rs b/yazi-fm/src/lives/folder.rs new file mode 100644 index 00000000..bbf7a364 --- /dev/null +++ b/yazi-fm/src/lives/folder.rs @@ -0,0 +1,195 @@ +use mlua::{AnyUserData, IntoLua, Lua, MetaMethod, UserDataFields, UserDataMethods, Value}; +use yazi_config::{LAYOUT, THEME}; +use yazi_plugin::{bindings::{Cast, File, Range, Url}, elements::Style}; +use yazi_shared::MIME_DIR; + +use super::{CtxRef, FolderRef}; + +pub struct Folder<'a, 'b> { + scope: &'b mlua::Scope<'a, 'a>, + + inner: &'a yazi_core::folder::Folder, +} + +impl<'a, 'b> Folder<'a, 'b> { + pub(super) fn register(lua: &Lua) -> mlua::Result<()> { + lua.register_userdata_type::(|reg| { + reg.add_field_method_get("cwd", |lua, me| Url::cast(lua, me.cwd.clone())); + reg.add_field_method_get("offset", |_, me| Ok(me.offset)); + reg.add_field_method_get("cursor", |_, me| Ok(me.cursor)); + + reg.add_field_function_get("window", |_, me| me.named_user_value::("window")); + reg.add_field_function_get("files", |_, me| me.named_user_value::("files")); + reg.add_field_function_get("hovered", |_, me| me.named_user_value::("hovered")); + })?; + + lua.register_userdata_type::(|reg| { + reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len())); + + reg.add_meta_function(MetaMethod::Pairs, |lua, me: AnyUserData| { + let iter = lua.create_function(|lua, (me, i): (AnyUserData, usize)| { + let files = me.borrow::()?; + let i = i + 1; + Ok(if i > files.len() { + mlua::Variadic::new() + } else { + mlua::Variadic::from_iter([ + i.into_lua(lua)?, + File::cast(lua, files[i - 1].clone())?.into_lua(lua)?, + ]) + }) + })?; + Ok((iter, me, 0)) + }); + })?; + + File::register(lua, |reg| { + reg.add_function("size", |_, me: AnyUserData| { + let file = me.borrow::()?; + if !file.is_dir() { + return Ok(Some(file.len)); + } + + let folder = me.named_user_value::("folder")?; + Ok(folder.files.sizes.get(&file.url).copied()) + }); + reg.add_function("mime", |lua, me: AnyUserData| { + let cx = lua.named_registry_value::("cx")?; + let file = me.borrow::()?; + Ok(cx.manager.mimetype.get(&file.url).cloned()) + }); + reg.add_function("prefix", |lua, me: AnyUserData| { + let folder = me.named_user_value::("folder")?; + if !folder.cwd.is_search() { + return Ok(None); + } + + let file = me.borrow::()?; + let mut p = file.url.strip_prefix(&folder.cwd).unwrap_or(&file.url).components(); + p.next_back(); + Some(lua.create_string(p.as_path().as_os_str().as_encoded_bytes())).transpose() + }); + reg.add_method("icon", |_, me, ()| { + Ok( + THEME + .icons + .iter() + .find(|&x| x.name.match_path(&me.url, me.is_dir())) + .map(|x| x.display.to_string()), + ) + }); + reg.add_function("style", |lua, me: AnyUserData| { + let cx = lua.named_registry_value::("cx")?; + let file = me.borrow::()?; + + let mime = if file.is_dir() { + Some(MIME_DIR) + } else { + cx.manager.mimetype.get(&file.url).map(|x| &**x) + }; + + Ok( + THEME + .filetypes + .iter() + .find(|&x| x.matches(&file.url, mime)) + .map(|x| Style::from(x.style)), + ) + }); + reg.add_function("is_hovered", |_, me: AnyUserData| { + let folder = me.named_user_value::("folder")?; + let file = me.borrow::()?; + Ok(matches!(folder.hovered(), Some(f) if f.url == file.url)) + }); + reg.add_function("is_yanked", |lua, me: AnyUserData| { + let cx = lua.named_registry_value::("cx")?; + let file = me.borrow::()?; + Ok(if !cx.manager.yanked.1.contains(&file.url) { + 0u8 + } else if cx.manager.yanked.0 { + 2u8 + } else { + 1u8 + }) + }); + reg.add_function("is_selected", |lua, me: AnyUserData| { + let cx = lua.named_registry_value::("cx")?; + let folder = me.named_user_value::("folder")?; + let file = me.borrow::()?; + + let selected = folder.files.is_selected(&file.url); + Ok(if !cx.manager.active().mode.is_visual() { + selected + } else { + let idx: usize = me.named_user_value("idx")?; + cx.manager.active().mode.pending(folder.offset + idx, selected) + }) + }); + reg.add_function("found", |lua, me: AnyUserData| { + let cx = lua.named_registry_value::("cx")?; + let Some(finder) = &cx.manager.active().finder else { + return Ok(None); + }; + + let file = me.borrow::()?; + if let Some(idx) = finder.matched_idx(&file.url) { + return Some( + lua.create_sequence_from([idx.into_lua(lua)?, finder.matched().len().into_lua(lua)?]), + ) + .transpose(); + } + Ok(None) + }); + reg.add_function("highlights", |lua, me: AnyUserData| { + let cx = lua.named_registry_value::("cx")?; + let Some(finder) = &cx.manager.active().finder else { + return Ok(None); + }; + + let file = me.borrow::()?; + let Some(h) = file.name().and_then(|n| finder.highlighted(n)) else { + return Ok(None); + }; + + Ok(Some(h.into_iter().map(Range::from).collect::>())) + }); + })?; + + Ok(()) + } +} + +impl<'a, 'b> Folder<'a, 'b> { + pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, inner: &'a yazi_core::folder::Folder) -> Self { + Self { scope, inner } + } + + pub(crate) fn make(&self, window: Option<(usize, usize)>) -> mlua::Result> { + let window = + window.unwrap_or_else(|| (self.inner.offset, LAYOUT.load().current.height as usize)); + + let ud = self.scope.create_any_userdata_ref(self.inner)?; + ud.set_named_user_value( + "window", + self + .inner + .files + .iter() + .skip(window.0) + .take(window.1) + .filter_map(|f| self.file(f).ok()) + .collect::>(), + )?; + ud.set_named_user_value("files", self.scope.create_any_userdata_ref(&self.inner.files)?)?; + ud.set_named_user_value("hovered", self.inner.hovered().and_then(|h| self.file(h).ok()))?; + + Ok(ud) + } + + fn file(&self, inner: &'a yazi_shared::fs::File) -> mlua::Result> { + let ud = self.scope.create_any_userdata_ref(inner)?; + ud.set_named_user_value("folder", self.scope.create_any_userdata_ref(self.inner)?)?; + + Ok(ud) + } +} diff --git a/yazi-fm/src/lives/lives.rs b/yazi-fm/src/lives/lives.rs new file mode 100644 index 00000000..ba903853 --- /dev/null +++ b/yazi-fm/src/lives/lives.rs @@ -0,0 +1,56 @@ +use std::sync::Arc; + +use mlua::{Scope, Table}; +use tracing::error; +use yazi_config::LAYOUT; +use yazi_plugin::{elements::RectRef, LUA}; + +use crate::Ctx; + +pub(crate) struct Lives; + +impl Lives { + pub(crate) fn register() -> mlua::Result<()> { + yazi_plugin::bindings::Cha::register(&LUA)?; + yazi_plugin::bindings::Url::register(&LUA)?; + + super::Active::register(&LUA)?; + super::Folder::register(&LUA)?; + super::Tabs::register(&LUA)?; + super::Tasks::register(&LUA)?; + + Ok(()) + } + + pub(crate) fn scope<'a>(cx: &'a Ctx, f: impl FnOnce(&Scope<'a, 'a>)) { + let result = LUA.scope(|scope| { + LUA.set_named_registry_value("cx", scope.create_any_userdata_ref(cx)?)?; + + let global = LUA.globals(); + global.set( + "cx", + LUA.create_table_from([ + ("active", super::Active::new(scope, cx.manager.active()).make()?), + ("tabs", super::Tabs::new(scope, &cx.manager.tabs).make()?), + ("tasks", super::Tasks::new(scope, &cx.tasks).make()?), + ])?, + )?; + + f(scope); + + LAYOUT.store(Arc::new(yazi_config::Layout { + header: *global.get::<_, Table>("Header")?.get::<_, RectRef>("area")?, + parent: *global.get::<_, Table>("Parent")?.get::<_, RectRef>("area")?, + current: *global.get::<_, Table>("Current")?.get::<_, RectRef>("area")?, + preview: *global.get::<_, Table>("Preview")?.get::<_, RectRef>("area")?, + status: *global.get::<_, Table>("Status")?.get::<_, RectRef>("area")?, + })); + + Ok(()) + }); + + if let Err(e) = result { + error!("{e}"); + } + } +} diff --git a/yazi-fm/src/lives/mod.rs b/yazi-fm/src/lives/mod.rs new file mode 100644 index 00000000..2dcc8e18 --- /dev/null +++ b/yazi-fm/src/lives/mod.rs @@ -0,0 +1,16 @@ +#![allow(clippy::module_inception)] + +mod active; +mod folder; +mod lives; +mod tabs; +mod tasks; + +pub(super) use active::*; +pub(super) use folder::*; +pub(super) use lives::*; +pub(super) use tabs::*; +pub(super) use tasks::*; + +type CtxRef<'lua> = mlua::UserDataRef<'lua, crate::Ctx>; +type FolderRef<'lua> = mlua::UserDataRef<'lua, yazi_core::folder::Folder>; diff --git a/yazi-plugin/src/bindings/tabs.rs b/yazi-fm/src/lives/tabs.rs similarity index 58% rename from yazi-plugin/src/bindings/tabs.rs rename to yazi-fm/src/lives/tabs.rs index 8d31e8b5..c3461ce8 100644 --- a/yazi-plugin/src/bindings/tabs.rs +++ b/yazi-fm/src/lives/tabs.rs @@ -1,6 +1,4 @@ -use mlua::{AnyUserData, MetaMethod, UserDataFields, UserDataMethods, Value}; - -use crate::LUA; +use mlua::{AnyUserData, Lua, MetaMethod, UserDataFields, UserDataMethods, Value}; pub struct Tabs<'a, 'b> { scope: &'b mlua::Scope<'a, 'a>, @@ -9,8 +7,8 @@ pub struct Tabs<'a, 'b> { } impl<'a, 'b> Tabs<'a, 'b> { - pub(crate) fn init() -> mlua::Result<()> { - LUA.register_userdata_type::(|reg| { + pub(super) fn register(lua: &Lua) -> mlua::Result<()> { + lua.register_userdata_type::(|reg| { reg.add_field_method_get("idx", |_, me| Ok(me.idx)); reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len())); reg.add_meta_function(MetaMethod::Index, |_, (me, index): (AnyUserData, usize)| { @@ -19,17 +17,15 @@ impl<'a, 'b> Tabs<'a, 'b> { }); })?; - LUA.register_userdata_type::(|reg| { - reg.add_method("name", |_, me, ()| { - Ok( - me.current - .cwd - .file_name() - .map(|n| n.to_string_lossy()) - .or_else(|| Some(me.current.cwd.to_string_lossy())) - .unwrap_or_default() - .into_owned(), - ) + lua.register_userdata_type::(|reg| { + reg.add_method("name", |lua, me, ()| { + Some(lua.create_string( + me.current.cwd.file_name().map_or_else( + || me.current.cwd.as_os_str().as_encoded_bytes(), + |n| n.as_encoded_bytes(), + ), + )) + .transpose() }); reg.add_field_function_get("mode", |_, me| me.named_user_value::("mode")); @@ -41,7 +37,9 @@ impl<'a, 'b> Tabs<'a, 'b> { Ok(()) } +} +impl<'a, 'b> Tabs<'a, 'b> { pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, inner: &'a yazi_core::manager::Tabs) -> Self { Self { scope, inner } } @@ -62,35 +60,16 @@ impl<'a, 'b> Tabs<'a, 'b> { ud.set_named_user_value("parent", inner.parent.as_ref().and_then(|p| self.folder(p).ok()))?; ud.set_named_user_value("current", self.folder(&inner.current)?)?; - ud.set_named_user_value("preview", self.preview(inner)?)?; Ok(ud) } - pub(crate) fn folder(&self, inner: &'a yazi_core::tab::Folder) -> mlua::Result> { + pub(crate) fn folder( + &self, + inner: &'a yazi_core::folder::Folder, + ) -> mlua::Result> { let ud = self.scope.create_any_userdata_ref(inner)?; - ud.set_named_user_value("files", self.files(&inner.files)?)?; - - Ok(ud) - } - - fn files(&self, inner: &'a yazi_core::files::Files) -> mlua::Result> { - self.scope.create_any_userdata_ref(inner) - } - - fn preview(&self, tab: &'a yazi_core::tab::Tab) -> mlua::Result> { - let inner = &tab.preview; - - let ud = self.scope.create_any_userdata_ref(inner)?; - ud.set_named_user_value( - "folder", - inner - .lock - .as_ref() - .filter(|l| l.is_folder()) - .and_then(|l| tab.history(&l.url)) - .and_then(|f| self.folder(f).ok()), - )?; + ud.set_named_user_value("files", self.scope.create_any_userdata_ref(&inner.files)?)?; Ok(ud) } diff --git a/yazi-plugin/src/bindings/tasks.rs b/yazi-fm/src/lives/tasks.rs similarity index 72% rename from yazi-plugin/src/bindings/tasks.rs rename to yazi-fm/src/lives/tasks.rs index 07947182..44ed5108 100644 --- a/yazi-plugin/src/bindings/tasks.rs +++ b/yazi-fm/src/lives/tasks.rs @@ -1,6 +1,4 @@ -use mlua::{AnyUserData, LuaSerdeExt, UserDataFields}; - -use crate::LUA; +use mlua::{AnyUserData, Lua, LuaSerdeExt, UserDataFields}; pub struct Tasks<'a, 'b> { scope: &'b mlua::Scope<'a, 'a>, @@ -9,8 +7,8 @@ pub struct Tasks<'a, 'b> { } impl<'a, 'b> Tasks<'a, 'b> { - pub(crate) fn init() -> mlua::Result<()> { - LUA.register_userdata_type::(|reg| { + pub(super) fn register(lua: &Lua) -> mlua::Result<()> { + lua.register_userdata_type::(|reg| { reg.add_field_method_get("progress", |lua, me| lua.to_value(&me.progress)) })?; diff --git a/yazi-fm/src/main.rs b/yazi-fm/src/main.rs index 8af3c9f4..0ecd9e90 100644 --- a/yazi-fm/src/main.rs +++ b/yazi-fm/src/main.rs @@ -2,9 +2,12 @@ mod app; mod completion; +mod components; +mod context; mod executor; mod help; mod input; +mod lives; mod logs; mod panic; mod root; @@ -14,6 +17,7 @@ mod tasks; mod which; mod widgets; +use context::*; use executor::*; use logs::*; use panic::*; @@ -29,6 +33,8 @@ async fn main() -> anyhow::Result<()> { yazi_core::init(); + yazi_scheduler::init(); + yazi_plugin::init(); yazi_adaptor::init(); diff --git a/yazi-fm/src/root.rs b/yazi-fm/src/root.rs index 4dde5b7b..e1c34a93 100644 --- a/yazi-fm/src/root.rs +++ b/yazi-fm/src/root.rs @@ -1,9 +1,7 @@ use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, widgets::Widget}; -use yazi_core::Ctx; -use yazi_plugin::components; use super::{completion, input, select, tasks, which}; -use crate::help; +use crate::{components, help, Ctx}; pub(super) struct Root<'a> { cx: &'a Ctx, @@ -20,9 +18,10 @@ impl<'a> Widget for Root<'a> { .constraints([Constraint::Length(1), Constraint::Min(0), Constraint::Length(1)]) .split(area); - components::Header::new(self.cx).render(chunks[0], buf); - components::Manager::new(self.cx).render(chunks[1], buf); - components::Status::new(self.cx).render(chunks[2], buf); + components::Header.render(chunks[0], buf); + components::Manager.render(chunks[1], buf); + components::Status.render(chunks[2], buf); + components::Preview::new(self.cx).render(chunks[2], buf); if self.cx.tasks.visible { tasks::Layout::new(self.cx).render(area, buf); diff --git a/yazi-fm/src/select/select.rs b/yazi-fm/src/select/select.rs index a7e94d2b..19983d0f 100644 --- a/yazi-fm/src/select/select.rs +++ b/yazi-fm/src/select/select.rs @@ -1,8 +1,7 @@ use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, List, ListItem, Widget}}; use yazi_config::THEME; -use yazi_core::Ctx; -use crate::widgets; +use crate::{Ctx, widgets}; pub(crate) struct Select<'a> { cx: &'a Ctx, diff --git a/yazi-fm/src/signals.rs b/yazi-fm/src/signals.rs index 1ce4c9e1..450eeda4 100644 --- a/yazi-fm/src/signals.rs +++ b/yazi-fm/src/signals.rs @@ -50,7 +50,7 @@ impl Signals { #[cfg(unix)] fn spawn_system_task(&self) -> Result> { use libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGTERM}; - use yazi_core::Ctx; + use yazi_scheduler::Scheduler; let tx = self.tx.clone(); let mut signals = signal_hook_tokio::Signals::new([ @@ -68,7 +68,7 @@ impl Signals { break; } } - SIGCONT => Ctx::resume(), + SIGCONT => Scheduler::app_resume(), _ => {} } } diff --git a/yazi-fm/src/tasks/layout.rs b/yazi-fm/src/tasks/layout.rs index 7c07ab73..82e68ee7 100644 --- a/yazi-fm/src/tasks/layout.rs +++ b/yazi-fm/src/tasks/layout.rs @@ -1,8 +1,8 @@ use ratatui::{buffer::Buffer, layout::{self, Alignment, Constraint, Direction, Rect}, text::Line, widgets::{Block, BorderType, Borders, List, ListItem, Padding, Widget}}; use yazi_config::THEME; -use yazi_core::{tasks::TASKS_PERCENT, Ctx}; +use yazi_core::tasks::TASKS_PERCENT; -use crate::widgets; +use crate::{Ctx, widgets}; pub(crate) struct Layout<'a> { cx: &'a Ctx, diff --git a/yazi-fm/src/which/layout.rs b/yazi-fm/src/which/layout.rs index 745570f7..14a20687 100644 --- a/yazi-fm/src/which/layout.rs +++ b/yazi-fm/src/which/layout.rs @@ -1,9 +1,8 @@ use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, widgets::{Block, Widget}}; use yazi_config::THEME; -use yazi_core::Ctx; use super::Side; -use crate::widgets; +use crate::{Ctx, widgets}; pub(crate) struct Which<'a> { cx: &'a Ctx, diff --git a/yazi-fm/src/widgets/clear.rs b/yazi-fm/src/widgets/clear.rs index a7a8e947..aa939e7a 100644 --- a/yazi-fm/src/widgets/clear.rs +++ b/yazi-fm/src/widgets/clear.rs @@ -2,8 +2,8 @@ use std::sync::atomic::Ordering; use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; use yazi_adaptor::ADAPTOR; -use yazi_config::MANAGER; -use yazi_core::preview::COLLISION; +use yazi_config::LAYOUT; +use yazi_shared::COLLISION; pub(crate) struct Clear; @@ -28,7 +28,7 @@ impl Widget for Clear { fn render(self, area: Rect, buf: &mut Buffer) { ratatui::widgets::Clear.render(area, buf); - let Some(r) = overlap(&area, &MANAGER.layout.image_rect()) else { + let Some(r) = overlap(&area, &LAYOUT.load().preview) else { return; }; diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index 27b6f0c3..e00de264 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -9,19 +9,29 @@ homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" [dependencies] -yazi-config = { path = "../yazi-config", version = "0.1.5" } -yazi-core = { path = "../yazi-core", version = "0.1.5" } -yazi-shared = { path = "../yazi-shared", version = "0.1.5" } +yazi-adaptor = { path = "../yazi-adaptor", version = "0.1.5" } +yazi-config = { path = "../yazi-config", version = "0.1.5" } +yazi-shared = { path = "../yazi-shared", version = "0.1.5" } # External dependencies ansi-to-tui = "^3" anyhow = "^1" +futures = "^0" +libc = "^0" +md-5 = "^0" +parking_lot = "^0" ratatui = "^0" +serde = "^1" +serde_json = "^1" +syntect = { version = "^5", default-features = false, features = [ "parsing", "default-themes", "plist-load", "regex-onig" ] } +tokio = { version = "^1", features = [ "parking_lot", "rt-multi-thread" ] } +tokio-util = "^0" tracing = { version = "^0", features = [ "max_level_debug", "release_max_level_warn" ] } unicode-width = "^0" +yazi-prebuild = "^0" -[target.'cfg(any(target_arch = "riscv64", target_arch="loongarch64"))'.dependencies] -mlua = { version = "^0", features = [ "lua52", "vendored", "serialize" ] } +[target.'cfg(any(target_arch="riscv64", target_arch="loongarch64"))'.dependencies] +mlua = { version = "^0", features = [ "lua52", "vendored", "serialize", "macros", "async" ] } -[target.'cfg(not(any(target_arch = "riscv64", target_arch="loongarch64")))'.dependencies] -mlua = { version = "^0", features = [ "luajit52", "vendored", "serialize" ] } +[target.'cfg(not(any(target_arch="riscv64", target_arch="loongarch64")))'.dependencies] +mlua = { version = "^0", features = [ "luajit52", "vendored", "serialize", "macros", "async" ] } diff --git a/yazi-plugin/preset/components/current.lua b/yazi-plugin/preset/components/current.lua new file mode 100644 index 00000000..24fa14d4 --- /dev/null +++ b/yazi-plugin/preset/components/current.lua @@ -0,0 +1,31 @@ +Current = { + area = ui.Rect.default, +} + +function Current:render(area) + self.area = area + + local markers = {} + local items = {} + for i, f in ipairs(Folder:by_kind(Folder.CURRENT).window) do + local name = Folder:highlighted_name(f) + + -- Highlight hovered file + local item = ui.ListItem(ui.Line { Folder:icon(f), table.unpack(name) }) + if f:is_hovered() then + item = item:style(THEME.manager.hovered) + else + item = item:style(f:style()) + end + items[#items + 1] = item + + -- Mark yanked/selected files + local yanked = f:is_yanked() + if yanked ~= 0 then + markers[#markers + 1] = { i, yanked } + elseif f:is_selected() then + markers[#markers + 1] = { i, 3 } + end + end + return ya.flat { ui.List(area, items), Folder:linemode(area), Folder:markers(area, markers) } +end diff --git a/yazi-plugin/preset/components/folder.lua b/yazi-plugin/preset/components/folder.lua index 3cf687ad..9c74278f 100644 --- a/yazi-plugin/preset/components/folder.lua +++ b/yazi-plugin/preset/components/folder.lua @@ -55,11 +55,11 @@ function Folder:linemode(area) local spans = { ui.Span(" ") } if mode == "size" then local size = f:size() - spans[#spans + 1] = ui.Span(size and utils.readable_size(size) or "") + spans[#spans + 1] = ui.Span(size and ya.readable_size(size) or "") elseif mode == "mtime" then - spans[#spans + 1] = ui.Span(os.date("%y-%m-%d %H:%M", f.modified)) + spans[#spans + 1] = ui.Span(os.date("%y-%m-%d %H:%M", f.cha.modified)) elseif mode == "permissions" then - spans[#spans + 1] = ui.Span(f:permissions() or "") + spans[#spans + 1] = ui.Span(f.cha:permissions() or "") end spans[#spans + 1] = ui.Span(" ") @@ -109,80 +109,3 @@ function Folder:markers(area, markers) append(last) return elements end - -function Folder:parent(area) - local folder = self:by_kind(self.PARENT) - if folder == nil then - return {} - end - - local items = {} - for _, f in ipairs(folder.window) do - local item = ui.ListItem(ui.Line { self:icon(f), ui.Span(f.name) }) - if f:is_hovered() then - item = item:style(THEME.manager.hovered) - else - item = item:style(f:style()) - end - - items[#items + 1] = item - end - - return { ui.List(area, items) } -end - -function Folder:current(area) - local markers = {} - local items = {} - for i, f in ipairs(self:by_kind(self.CURRENT).window) do - local name = self:highlighted_name(f) - - -- Highlight hovered file - local item = ui.ListItem(ui.Line { self:icon(f), table.unpack(name) }) - if f:is_hovered() then - item = item:style(THEME.manager.hovered) - else - item = item:style(f:style()) - end - items[#items + 1] = item - - -- Mark yanked/selected files - local yanked = f:is_yanked() - if yanked ~= 0 then - markers[#markers + 1] = { i, yanked } - elseif f:is_selected() then - markers[#markers + 1] = { i, 3 } - end - end - return utils.flat { ui.List(area, items), self:linemode(area), self:markers(area, markers) } -end - -function Folder:preview(area) - local folder = self:by_kind(self.PREVIEW) - if folder == nil then - return {} - end - - local items = {} - for _, f in ipairs(folder.window) do - local item = ui.ListItem(ui.Line { self:icon(f), ui.Span(f.name) }) - if f:is_hovered() then - item = item:style(THEME.manager.preview_hovered) - else - item = item:style(f:style()) - end - items[#items + 1] = item - end - - return { ui.List(area, items) } -end - -function Folder:render(area, args) - if args.kind == self.PARENT then - return self:parent(area) - elseif args.kind == self.CURRENT then - return self:current(area) - elseif args.kind == self.PREVIEW then - return self:preview(area) - end -end diff --git a/yazi-plugin/preset/components/header.lua b/yazi-plugin/preset/components/header.lua index 08342425..c1f08267 100644 --- a/yazi-plugin/preset/components/header.lua +++ b/yazi-plugin/preset/components/header.lua @@ -1,13 +1,15 @@ -Header = {} +Header = { + area = ui.Rect.default, +} function Header:cwd() local cwd = cx.active.current.cwd local span if not cwd.is_search then - span = ui.Span(utils.readable_path(tostring(cwd))) + span = ui.Span(ya.readable_path(tostring(cwd))) else - span = ui.Span(string.format("%s (search: %s)", utils.readable_path(tostring(cwd)), cwd.frag)) + span = ui.Span(string.format("%s (search: %s)", ya.readable_path(tostring(cwd)), cwd.frag)) end return span:style(THEME.manager.cwd) end @@ -17,7 +19,7 @@ function Header:tabs() for i = 1, #cx.tabs do local text = i if THEME.manager.tab_width > 2 then - text = utils.truncate(text .. " " .. cx.tabs[i]:name(), THEME.manager.tab_width) + text = ya.truncate(text .. " " .. cx.tabs[i]:name(), THEME.manager.tab_width) end if i == cx.tabs.idx + 1 then spans[#spans + 1] = ui.Span(" " .. text .. " "):style(THEME.manager.tab_active) @@ -29,6 +31,8 @@ function Header:tabs() end function Header:render(area) + self.area = area + local chunks = ui.Layout() :direction(ui.Direction.HORIZONTAL) :constraints({ ui.Constraint.Percentage(50), ui.Constraint.Percentage(50) }) diff --git a/yazi-plugin/preset/components/manager.lua b/yazi-plugin/preset/components/manager.lua index c8e11ec1..2bb298b6 100644 --- a/yazi-plugin/preset/components/manager.lua +++ b/yazi-plugin/preset/components/manager.lua @@ -1,6 +1,10 @@ -Manager = {} +Manager = { + area = ui.Rect.default, +} function Manager:render(area) + self.area = area + local chunks = ui.Layout() :direction(ui.Direction.HORIZONTAL) :constraints({ @@ -10,16 +14,16 @@ function Manager:render(area) }) :split(area) - return utils.flat { + return ya.flat { -- Borders ui.Bar(chunks[1], ui.Position.RIGHT):symbol(THEME.manager.border_symbol):style(THEME.manager.border_style), ui.Bar(chunks[3], ui.Position.LEFT):symbol(THEME.manager.border_symbol):style(THEME.manager.border_style), -- Parent - Folder:render(chunks[1]:padding(ui.Padding.x(1)), { kind = Folder.PARENT }), + Parent:render(chunks[1]:padding(ui.Padding.x(1))), -- Current - Folder:render(chunks[2], { kind = Folder.CURRENT }), + Current:render(chunks[2]), -- Preview - ui.Base(chunks[3]:padding(ui.Padding.x(1)), ui.Base.PREVIEW), + Preview:render(chunks[3]:padding(ui.Padding.x(1))), } end diff --git a/yazi-plugin/preset/components/parent.lua b/yazi-plugin/preset/components/parent.lua new file mode 100644 index 00000000..6c29bd81 --- /dev/null +++ b/yazi-plugin/preset/components/parent.lua @@ -0,0 +1,26 @@ +Parent = { + area = ui.Rect.default, +} + +function Parent:render(area) + self.area = area + + local folder = Folder:by_kind(Folder.PARENT) + if folder == nil then + return {} + end + + local items = {} + for _, f in ipairs(folder.window) do + local item = ui.ListItem(ui.Line { Folder:icon(f), ui.Span(f.name) }) + if f:is_hovered() then + item = item:style(THEME.manager.hovered) + else + item = item:style(f:style()) + end + + items[#items + 1] = item + end + + return { ui.List(area, items) } +end diff --git a/yazi-plugin/preset/components/preview.lua b/yazi-plugin/preset/components/preview.lua new file mode 100644 index 00000000..dd6b73cc --- /dev/null +++ b/yazi-plugin/preset/components/preview.lua @@ -0,0 +1,8 @@ +Preview = { + area = ui.Rect.default, +} + +function Preview:render(area) + self.area = area + return {} +end diff --git a/yazi-plugin/preset/components/status.lua b/yazi-plugin/preset/components/status.lua index de98ac6d..0992d6fd 100644 --- a/yazi-plugin/preset/components/status.lua +++ b/yazi-plugin/preset/components/status.lua @@ -1,4 +1,6 @@ -Status = {} +Status = { + area = ui.Rect.default, +} function Status.style() if cx.active.mode.is_select then @@ -31,7 +33,7 @@ function Status:size() local style = self.style() return ui.Line { - ui.Span(" " .. utils.readable_size(h:size() or h.length) .. " "):fg(style.bg):bg(THEME.status.separator_style.bg), + ui.Span(" " .. ya.readable_size(h:size() or h.cha.length) .. " "):fg(style.bg):bg(THEME.status.separator_style.bg), ui.Span(THEME.status.separator_close):fg(THEME.status.separator_style.fg), } end @@ -51,7 +53,7 @@ function Status:permissions() return ui.Span("") end - local perm = h:permissions() + local perm = h.cha:permissions() if perm == nil then return ui.Span("") end @@ -139,6 +141,8 @@ function Status:progress(area, offset) end function Status:render(area) + self.area = area + local left = ui.Line { self:mode(), self:size(), self:name() } local right = ui.Line { self:permissions(), self:percentage(), self:position() } local progress = self:progress(area, right:width()) diff --git a/yazi-plugin/preset/inspect/inspect.lua b/yazi-plugin/preset/inspect/inspect.lua index 2c80726a..be86016e 100644 --- a/yazi-plugin/preset/inspect/inspect.lua +++ b/yazi-plugin/preset/inspect/inspect.lua @@ -302,9 +302,9 @@ setmetatable(inspect, { __call = function(_, root, options) return inspect.inspect(root, options) end, }) -yazi = yazi or {} -yazi.inspect = inspect -yazi.print = function(...) +ya = ya or {} +ya.inspect = inspect +ya.print = function(...) local args = { ... } for i = 1, #args do print(inspect(args[i])) diff --git a/yazi-plugin/preset/plugins/archive.lua b/yazi-plugin/preset/plugins/archive.lua new file mode 100644 index 00000000..8811fec1 --- /dev/null +++ b/yazi-plugin/preset/plugins/archive.lua @@ -0,0 +1,18 @@ +local Archive = {} + +function Archive:peek() + local _, max = ya.preview_archive(self.area, self.file, self.skip) + if max then + ya.manager_emit("peek", { tostring(max) }) + end +end + +function Archive:seek(units) + local h = cx.active.current.hovered + if h and h.url == self.file.url then + local step = math.floor(units * self.area.h / 10) + ya.manager_emit("peek", { tostring(math.max(0, cx.active.preview.skip + step)) }) + end +end + +return Archive diff --git a/yazi-plugin/preset/plugins/code.lua b/yazi-plugin/preset/plugins/code.lua new file mode 100644 index 00000000..7b8ea169 --- /dev/null +++ b/yazi-plugin/preset/plugins/code.lua @@ -0,0 +1,18 @@ +local Code = {} + +function Code:peek() + local _, max = ya.preview_code(self.area, self.file, self.skip) + if max then + ya.manager_emit("peek", { tostring(max) }) + end +end + +function Code:seek(units) + local h = cx.active.current.hovered + if h and h.url == self.file.url then + local step = math.floor(units * self.area.h / 10) + ya.manager_emit("peek", { tostring(math.max(0, cx.active.preview.skip + step)) }) + end +end + +return Code diff --git a/yazi-plugin/preset/plugins/folder.lua b/yazi-plugin/preset/plugins/folder.lua new file mode 100644 index 00000000..f6769ddd --- /dev/null +++ b/yazi-plugin/preset/plugins/folder.lua @@ -0,0 +1,26 @@ +local Folder_ = {} + +function Folder_:peek() + local folder = Folder:by_kind(Folder.PREVIEW) + if folder == nil then + return {} + end + + local items = {} + for _, f in ipairs(folder.window) do + local item = ui.ListItem(ui.Line { Folder:icon(f), ui.Span(f.name) }) + if f:is_hovered() then + item = item:style(THEME.manager.preview_hovered) + else + item = item:style(f:style()) + end + items[#items + 1] = item + end + ya.preview_widgets(self.file, self.skip, { ui.List(self.area, items) }) +end + +function Folder_:seek(units) + -- TODO +end + +return Folder_ diff --git a/yazi-plugin/preset/plugins/image.lua b/yazi-plugin/preset/plugins/image.lua new file mode 100644 index 00000000..60a9b1ce --- /dev/null +++ b/yazi-plugin/preset/plugins/image.lua @@ -0,0 +1,22 @@ +local Image = {} + +function Image:cache() return ya.cache_file(self.file.url .. tostring(self.file.cha.modified)) end + +function Image:peek() + local cache = self:cache() + ya.image_show(fs.symlink_metadata(cache) and cache or self.file.url, self.area) + ya.preview_widgets(self.file, self.skip, {}) +end + +function Image:seek() end + +function Image:preload() + local cache = self:cache() + if fs.symlink_metadata(cache) then + return 1 + end + + return ya.image_precache(self.file.url, cache) and 1 or 2 +end + +return Image diff --git a/yazi-plugin/preset/plugins/json.lua b/yazi-plugin/preset/plugins/json.lua new file mode 100644 index 00000000..b30e7484 --- /dev/null +++ b/yazi-plugin/preset/plugins/json.lua @@ -0,0 +1,46 @@ +local Json = {} + +function Json:peek() + local limit = self.area.h + local child = Command.new("jq") + :args({ + "-C", + "--tab", + ".", + tostring(self.file.url), + }) + :stdout(Command.PIPED) + :stderr(Command.PIPED) + :spawn() + + local i, lines = 0, "" + repeat + local code, next = child:read_line() + if code ~= 0 then + break + end + + i = i + 1 + if i > self.skip then + lines = lines .. next + end + until i >= self.skip + limit + + child:start_kill() + if self.skip > 0 and i < self.skip + limit then + ya.manager_emit("peek", { tostring(math.max(0, i - limit)) }) + else + lines = lines:gsub("\t", string.rep(" ", PREVIEW.tab_size)) + ya.preview_widgets(self.file, self.skip, { ui.Paragraph.parse(self.area, lines) }) + end +end + +function Json:seek(units) + local h = cx.active.current.hovered + if h and h.url == self.file.url then + local step = math.floor(units * self.area.h / 10) + ya.manager_emit("peek", { tostring(math.max(0, cx.active.preview.skip + step)) }) + end +end + +return Json diff --git a/yazi-plugin/preset/plugins/mime.lua b/yazi-plugin/preset/plugins/mime.lua new file mode 100644 index 00000000..ed88a3ef --- /dev/null +++ b/yazi-plugin/preset/plugins/mime.lua @@ -0,0 +1,35 @@ +local Mime = {} + +function Mime:preload() + local command = Command.new("file"):arg("--mime-type"):stdout(Command.PIPED):stderr(Command.PIPED) + if ya.target_family() == "windows" then + command:arg("-b") + else + command:arg("-bL") + end + + local urls = {} + for _, file in ipairs(self.files) do + urls[#urls + 1] = tostring(file.url) + end + + local i, data = 1, {} + local output = command:args(urls):output() + for line in output.stdout:gmatch("[^\r\n]+") do + if i > #urls then + break + end + if ya.mime_valid(line) then + data[urls[i]] = line + end + i = i + 1 + end + + if #data then + ya.manager_emit("update_mimetype", {}, data) + return 3 + end + return 2 +end + +return Mime diff --git a/yazi-plugin/preset/plugins/pdf.lua b/yazi-plugin/preset/plugins/pdf.lua new file mode 100644 index 00000000..b9a21cea --- /dev/null +++ b/yazi-plugin/preset/plugins/pdf.lua @@ -0,0 +1,43 @@ +local Pdf = {} + +function Pdf:cache() return ya.cache_file(self.file.url .. self.skip .. tostring(self.file.cha.modified)) end + +function Pdf:peek() + if self:preload() == 1 then + ya.image_show(self:cache(), self.area) + ya.preview_widgets(self.file, self.skip, {}) + end +end + +function Pdf:seek(units) + local h = cx.active.current.hovered + if h and h.url == self.file.url then + local step = ya.clamp(-1, units, 1) + ya.manager_emit("peek", { tostring(math.max(0, cx.active.preview.skip + step)) }) + end +end + +function Pdf:preload() + local cache = self:cache() + if fs.symlink_metadata(cache) then + return 1 + end + + local output = Command.new("pdftoppm") + :args({ "-singlefile", "-jpeg", "-jpegopt", "quality=75", "-f", tostring(self.skip + 1), tostring(self.file.url) }) + :stdout(Command.PIPED) + :stderr(Command.PIPED) + :output() + + if not output.status:success() then + local pages = tonumber(output.stderr:match("the last page %((%d+)%)")) or 0 + if self.skip > 0 and pages > 0 then + ya.manager_emit("peek", { tostring(math.max(0, pages - 1)) }) + end + return 0 + end + + return fs.write(cache, output.stdout) and 1 or 2 +end + +return Pdf diff --git a/yazi-plugin/preset/plugins/video.lua b/yazi-plugin/preset/plugins/video.lua new file mode 100644 index 00000000..e9f6641e --- /dev/null +++ b/yazi-plugin/preset/plugins/video.lua @@ -0,0 +1,52 @@ +local Video = {} + +function Video:cache() return ya.cache_file(self.file.url .. self.skip .. tostring(self.file.cha.modified)) end + +function Video:peek() + if self:preload() == 1 then + ya.image_show(self:cache(), self.area) + ya.preview_widgets(self.file, self.skip, {}) + end +end + +function Video:seek(units) + local h = cx.active.current.hovered + if h and h.url == self.file.url then + ya.manager_emit("peek", { tostring(math.max(0, cx.active.preview.skip + units)) }) + end +end + +function Video:preload() + local percentage = 5 + self.skip + if percentage > 95 then + ya.manager_emit("peek", { "90" }) + return 2 + end + + local cache = self:cache() + if fs.symlink_metadata(cache) then + return 1 + end + + local status = Command.new("ffmpegthumbnailer") + :args({ + "-q", + "6", + "-c", + "jpeg", + "-i", + tostring(self.file.url), + "-o", + tostring(cache), + "-t", + tostring(percentage), + "-s", + tostring(PREVIEW.max_width), + }) + :spawn() + :wait() + + return status:success() and 1 or 2 +end + +return Video diff --git a/yazi-plugin/preset/ui.lua b/yazi-plugin/preset/ui.lua index b408de4b..0439e87c 100644 --- a/yazi-plugin/preset/ui.lua +++ b/yazi-plugin/preset/ui.lua @@ -1,4 +1,5 @@ ui = { + -- FIXME: merge those three into their own modules Alignment = { LEFT = 0, CENTER = 1, @@ -16,33 +17,6 @@ ui = { LEFT = 8, ALL = 15, }, - - Base = setmetatable({ - PREVIEW = 0, - }, { - __call = function(_, ...) return ui.Base.new(...) end, - }), - Border = setmetatable({ - PLAIN = 0, - ROUNDED = 1, - DOUBLE = 2, - THICK = 3, - QUADRANT_INSIDE = 4, - QUADRANT_OUTSIDE = 5, - }, { - __call = function(_, ...) return ui.Border.new(...) end, - }), - Padding = setmetatable({ - left = function(left) return ui.Padding.new(left, 0, 0, 0) end, - right = function(right) return ui.Padding.new(0, right, 0, 0) end, - top = function(top) return ui.Padding.new(0, 0, top, 0) end, - bottom = function(bottom) return ui.Padding.new(0, 0, 0, bottom) end, - x = function(x) return ui.Padding.new(x, x, 0, 0) end, - y = function(y) return ui.Padding.new(0, 0, y, y) end, - xy = function(xy) return ui.Padding.new(xy, xy, xy, xy) end, - }, { - __call = function(_, ...) return ui.Padding.new(...) end, - }), } function ui.highlight_ranges(s, ranges) diff --git a/yazi-plugin/preset/utils.lua b/yazi-plugin/preset/ya.lua similarity index 57% rename from yazi-plugin/preset/utils.lua rename to yazi-plugin/preset/ya.lua index 7f5121c0..f93568c9 100644 --- a/yazi-plugin/preset/utils.lua +++ b/yazi-plugin/preset/ya.lua @@ -1,8 +1,8 @@ table.unpack = table.unpack or unpack -utils = utils or {} +ya = ya or {} -function utils.clamp(min, x, max) +function ya.clamp(min, x, max) if x < min then return min elseif x > max then @@ -12,11 +12,11 @@ function utils.clamp(min, x, max) end end -function utils.flat(t) +function ya.flat(t) local r = {} for _, v in ipairs(t) do if type(v) == "table" then - for _, v2 in ipairs(utils.flat(v)) do + for _, v2 in ipairs(ya.flat(v)) do r[#r + 1] = v2 end else @@ -26,9 +26,23 @@ function utils.flat(t) return r end -function utils.basename(str) return string.gsub(str, "(.*[/\\])(.*)", "%2") end +function ya.sync(f) + ya.SYNC_CALLS = ya.SYNC_CALLS + 1 + if ya.SYNC_BLOCK == ya.SYNC_CALLS then + ya.SYNC_ENTRY = f + end -function utils.readable_size(size) + if ya.SYNC_ON then + return f + end + + local calls = ya.SYNC_CALLS + return function(...) return plugin_retrieve(ya.PLUGIN_NAME, calls, ...) end +end + +function ya.basename(str) return string.gsub(str, "(.*[/\\])(.*)", "%2") end + +function ya.readable_size(size) local units = { "B", "KB", "MB", "GB", "TB", "PB", "EB" } local i = 1 while size > 1024.0 and i < #units do @@ -38,7 +52,7 @@ function utils.readable_size(size) return string.format("%.1f %s", size, units[i]) end -function utils.readable_path(path) +function ya.readable_path(path) local home = os.getenv("HOME") or os.getenv("USERPROFILE") if home == nil then return path diff --git a/yazi-plugin/src/bindings/active.rs b/yazi-plugin/src/bindings/active.rs deleted file mode 100644 index 9075cf26..00000000 --- a/yazi-plugin/src/bindings/active.rs +++ /dev/null @@ -1,134 +0,0 @@ -use mlua::{AnyUserData, MetaMethod, UserDataFields, UserDataMethods, Value}; -use yazi_config::MANAGER; -use yazi_core::Ctx; - -use super::Url; -use crate::LUA; - -pub struct Active<'a, 'b> { - scope: &'b mlua::Scope<'a, 'a>, - - cx: &'a yazi_core::Ctx, - inner: &'a yazi_core::tab::Tab, -} - -impl<'a, 'b> Active<'a, 'b> { - pub(crate) fn init() -> mlua::Result<()> { - LUA.register_userdata_type::(|reg| { - reg.add_field_method_get("is_select", |_, me| Ok(me.is_select())); - reg.add_field_method_get("is_unset", |_, me| Ok(me.is_unset())); - reg.add_field_method_get("is_visual", |_, me| Ok(me.is_visual())); - reg.add_method("pending", |_, me, (idx, state): (usize, bool)| Ok(me.pending(idx, state))); - - reg.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.to_string())); - })?; - - LUA.register_userdata_type::(|reg| { - reg.add_field_method_get("sort_by", |_, me| Ok(me.sort_by.to_string())); - reg.add_field_method_get("sort_sensitive", |_, me| Ok(me.sort_sensitive)); - reg.add_field_method_get("sort_reverse", |_, me| Ok(me.sort_reverse)); - reg.add_field_method_get("sort_dir_first", |_, me| Ok(me.sort_dir_first)); - - reg.add_field_method_get("linemode", |_, me| Ok(me.linemode.to_owned())); - reg.add_field_method_get("show_hidden", |_, me| Ok(me.show_hidden)); - })?; - - LUA.register_userdata_type::(|reg| { - reg.add_field_method_get("cwd", |_, me| Ok(Url::from(&me.cwd))); - reg.add_field_method_get("offset", |_, me| Ok(me.offset)); - reg.add_field_method_get("cursor", |_, me| Ok(me.cursor)); - - reg.add_field_function_get("window", |_, me| me.named_user_value::("window")); - reg.add_field_function_get("files", |_, me| me.named_user_value::("files")); - reg.add_field_function_get("hovered", |_, me| me.named_user_value::("hovered")); - })?; - - LUA.register_userdata_type::(|reg| { - reg.add_field_function_get("folder", |_, me| me.named_user_value::("folder")); - })?; - - Ok(()) - } - - pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, cx: &'a Ctx) -> Self { - Self { scope, cx, inner: cx.manager.active() } - } - - pub(crate) fn make(&self) -> mlua::Result> { - let ud = self.scope.create_any_userdata_ref(self.inner)?; - ud.set_named_user_value("mode", self.scope.create_any_userdata_ref(&self.inner.mode)?)?; - ud.set_named_user_value("conf", self.scope.create_any_userdata_ref(&self.inner.conf)?)?; - ud.set_named_user_value( - "parent", - self.inner.parent.as_ref().and_then(|p| self.folder(p, None).ok()), - )?; - ud.set_named_user_value("current", self.folder(&self.inner.current, None)?)?; - ud.set_named_user_value("preview", self.preview(self.inner)?)?; - - Ok(ud) - } - - pub(crate) fn folder( - &self, - inner: &'a yazi_core::tab::Folder, - window: Option<(usize, usize)>, - ) -> mlua::Result> { - let window = window.unwrap_or_else(|| (inner.offset, MANAGER.layout.folder_height())); - - let ud = self.scope.create_any_userdata_ref(inner)?; - ud.set_named_user_value( - "window", - inner - .files - .iter() - .skip(window.0) - .take(window.1) - .enumerate() - .filter_map(|(i, f)| self.file(i, f, inner).ok()) - .collect::>(), - )?; - ud.set_named_user_value("files", self.files(&inner.files)?)?; - // TODO: remove this - ud.set_named_user_value( - "hovered", - inner.hovered().and_then(|h| self.file(999, h, inner).ok()), - )?; - - Ok(ud) - } - - fn files(&self, inner: &'a yazi_core::files::Files) -> mlua::Result> { - self.scope.create_any_userdata_ref(inner) - } - - fn file( - &self, - idx: usize, - inner: &'a yazi_shared::fs::File, - folder: &'a yazi_core::tab::Folder, - ) -> mlua::Result> { - let ud = self.scope.create_any_userdata_ref(inner)?; - ud.set_named_user_value("idx", idx)?; - ud.set_named_user_value("folder", self.scope.create_any_userdata_ref(folder)?)?; - ud.set_named_user_value("manager", self.scope.create_any_userdata_ref(&self.cx.manager)?)?; - - Ok(ud) - } - - fn preview(&self, tab: &'a yazi_core::tab::Tab) -> mlua::Result> { - let inner = &tab.preview; - - let ud = self.scope.create_any_userdata_ref(inner)?; - ud.set_named_user_value( - "folder", - inner - .lock - .as_ref() - .filter(|l| l.is_folder()) - .and_then(|l| tab.history(&l.url).map(|f| (l, f))) - .and_then(|(l, f)| self.folder(f, Some((l.skip, MANAGER.layout.preview_height()))).ok()), - )?; - - Ok(ud) - } -} diff --git a/yazi-plugin/src/bindings/bindings.rs b/yazi-plugin/src/bindings/bindings.rs index 76c3d7af..3b04e52f 100644 --- a/yazi-plugin/src/bindings/bindings.rs +++ b/yazi-plugin/src/bindings/bindings.rs @@ -1,8 +1,3 @@ -pub fn init() -> mlua::Result<()> { - super::active::Active::init()?; - super::files::Files::init()?; - super::tabs::Tabs::init()?; - super::tasks::Tasks::init()?; - - Ok(()) +pub trait Cast { + fn cast(lua: &mlua::Lua, data: T) -> mlua::Result; } diff --git a/yazi-plugin/src/bindings/cha.rs b/yazi-plugin/src/bindings/cha.rs new file mode 100644 index 00000000..6589466d --- /dev/null +++ b/yazi-plugin/src/bindings/cha.rs @@ -0,0 +1,51 @@ +use std::time::UNIX_EPOCH; + +use mlua::{AnyUserData, Lua, UserDataFields, UserDataMethods}; + +use super::Cast; + +pub struct Cha; + +impl Cha { + pub fn register(lua: &Lua) -> mlua::Result<()> { + lua.register_userdata_type::(|reg| { + reg.add_field_method_get("is_link", |_, me| Ok(me.is_link())); + reg.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden())); + + // Metadata + reg.add_field_method_get("is_dir", |_, me| Ok(me.is_dir())); + reg.add_field_method_get("is_symlink", |_, me| Ok(me.is_link())); + #[cfg(unix)] + { + reg.add_field_method_get("is_block_device", |_, me| Ok(me.is_block_device())); + reg.add_field_method_get("is_char_device", |_, me| Ok(me.is_char_device())); + reg.add_field_method_get("is_fifo", |_, me| Ok(me.is_fifo())); + reg.add_field_method_get("is_socket", |_, me| Ok(me.is_socket())); + } + reg.add_field_method_get("length", |_, me| Ok(me.len)); + reg.add_field_method_get("created", |_, me| { + Ok(me.created.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) + }); + reg.add_field_method_get("modified", |_, me| { + Ok(me.modified.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) + }); + reg.add_field_method_get("accessed", |_, me| { + Ok(me.accessed.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) + }); + reg.add_method("permissions", |_, me, ()| { + Ok( + #[cfg(unix)] + Some(yazi_shared::fs::permissions(me.permissions)), + #[cfg(windows)] + None::, + ) + }); + })?; + + Ok(()) + } +} + +impl> Cast for Cha { + fn cast(lua: &Lua, data: T) -> mlua::Result { lua.create_any_userdata(data.into()) } +} diff --git a/yazi-plugin/src/bindings/file.rs b/yazi-plugin/src/bindings/file.rs new file mode 100644 index 00000000..20dc6095 --- /dev/null +++ b/yazi-plugin/src/bindings/file.rs @@ -0,0 +1,33 @@ +use mlua::{AnyUserData, Lua, UserDataFields, UserDataRef, UserDataRegistry}; + +use super::{Cast, Cha, Url}; + +pub type FileRef<'lua> = UserDataRef<'lua, yazi_shared::fs::File>; + +pub struct File; + +impl File { + pub fn register( + lua: &Lua, + f: impl FnOnce(&mut UserDataRegistry), + ) -> mlua::Result<()> { + lua.register_userdata_type::(|reg| { + reg.add_field_method_get("url", |lua, me| Url::cast(lua, me.url.clone())); + reg.add_field_method_get("cha", |lua, me| Cha::cast(lua, me.cha)); + reg.add_field_method_get("link_to", |lua, me| { + me.link_to.as_ref().cloned().map(|u| Url::cast(lua, u)).transpose() + }); + + // Extension + reg.add_field_method_get("name", |lua, me| { + me.url.file_name().map(|n| lua.create_string(n.as_encoded_bytes())).transpose() + }); + + f(reg); + }) + } +} + +impl> Cast for File { + fn cast(lua: &Lua, data: T) -> mlua::Result { lua.create_any_userdata(data.into()) } +} diff --git a/yazi-plugin/src/bindings/files.rs b/yazi-plugin/src/bindings/files.rs deleted file mode 100644 index 18327ffd..00000000 --- a/yazi-plugin/src/bindings/files.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::time::UNIX_EPOCH; - -use mlua::{AnyUserData, IntoLua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef}; -use yazi_config::THEME; - -use super::{Range, Url}; -use crate::{layout::Style, LUA}; - -pub struct File(yazi_shared::fs::File); - -impl From<&yazi_shared::fs::File> for File { - fn from(value: &yazi_shared::fs::File) -> Self { Self(value.clone()) } -} - -impl UserData for File { - fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("url", |_, me| Ok(Url::from(&me.0.url))); - fields.add_field_method_get("link_to", |_, me| Ok(me.0.link_to().map(Url::from))); - fields.add_field_method_get("is_link", |_, me| Ok(me.0.is_link())); - fields.add_field_method_get("is_hidden", |_, me| Ok(me.0.is_hidden())); - } -} - -pub struct Files; - -impl Files { - pub(crate) fn init() -> mlua::Result<()> { - LUA.register_userdata_type::(|reg| { - reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len())); - - reg.add_meta_function(MetaMethod::Pairs, |lua, me: AnyUserData| { - let iter = lua.create_function(|lua, (me, i): (AnyUserData, usize)| { - let files = me.borrow::()?; - let i = i + 1; - Ok(if i > files.len() { - mlua::Variadic::new() - } else { - mlua::Variadic::from_iter([i.into_lua(lua)?, File::from(&files[i - 1]).into_lua(lua)?]) - }) - })?; - Ok((iter, me, 0)) - }); - - reg.add_function("slice", |_, (me, skip, take): (AnyUserData, usize, usize)| { - let files = me.borrow::()?; - Ok(files.iter().skip(skip).take(take).map(File::from).collect::>()) - }); - })?; - - LUA.register_userdata_type::(|reg| { - reg.add_field_method_get("url", |_, me| Ok(Url::from(&me.url))); - reg.add_field_method_get("link_to", |_, me| Ok(me.link_to().map(Url::from))); - reg.add_field_method_get("is_link", |_, me| Ok(me.is_link())); - reg.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden())); - - // Metadata - reg.add_field_method_get("is_dir", |_, me| Ok(me.is_dir())); - reg.add_field_method_get("is_symlink", |_, me| Ok(me.is_link())); - #[cfg(unix)] - { - reg.add_field_method_get("is_block_device", |_, me| Ok(me.is_block_device())); - reg.add_field_method_get("is_char_device", |_, me| Ok(me.is_char_device())); - reg.add_field_method_get("is_fifo", |_, me| Ok(me.is_fifo())); - reg.add_field_method_get("is_socket", |_, me| Ok(me.is_socket())); - } - reg.add_field_method_get("length", |_, me| Ok(me.len)); - reg.add_field_method_get("created", |_, me| { - Ok(me.created.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) - }); - reg.add_field_method_get("modified", |_, me| { - Ok(me.modified.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) - }); - reg.add_field_method_get("accessed", |_, me| { - Ok(me.accessed.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) - }); - reg.add_method("permissions", |_, me, ()| { - Ok( - #[cfg(unix)] - Some(yazi_shared::fs::permissions(me.permissions)), - #[cfg(windows)] - None::, - ) - }); - - // Extension - reg.add_field_method_get("name", |_, me| { - Ok(me.url.file_name().map(|n| n.to_string_lossy().to_string())) - }); - reg.add_function("size", |_, me: AnyUserData| { - let file = me.borrow::()?; - if !file.is_dir() { - return Ok(Some(file.len)); - } - - let folder = me.named_user_value::>("folder")?; - Ok(folder.files.sizes.get(&file.url).copied()) - }); - reg.add_function("mime", |_, me: AnyUserData| { - let manager = me.named_user_value::>("manager")?; - let file = me.borrow::()?; - Ok(manager.mimetype.get(&file.url).cloned()) - }); - reg.add_function("prefix", |_, me: AnyUserData| { - let folder = me.named_user_value::>("folder")?; - if !folder.cwd.is_search() { - return Ok(None); - } - - let file = me.borrow::()?; - let mut p = file.url.strip_prefix(&folder.cwd).unwrap_or(&file.url).components(); - p.next_back(); - Ok(Some(p.as_path().to_string_lossy().to_string())) - }); - reg.add_method("icon", |_, me, ()| { - Ok( - THEME - .icons - .iter() - .find(|&x| x.name.match_path(&me.url, Some(me.is_dir()))) - .map(|x| x.display.to_string()), - ) - }); - reg.add_function("style", |_, me: AnyUserData| { - let manager = me.named_user_value::>("manager")?; - let file = me.borrow::()?; - let mime = manager.mimetype.get(&file.url); - Ok( - THEME - .filetypes - .iter() - .find(|&x| x.matches(&file.url, mime, file.is_dir())) - .map(|x| Style::from(x.style)), - ) - }); - reg.add_function("is_hovered", |_, me: AnyUserData| { - let folder = me.named_user_value::>("folder")?; - let file = me.borrow::()?; - Ok(matches!(folder.hovered(), Some(f) if f.url == file.url)) - }); - reg.add_function("is_yanked", |_, me: AnyUserData| { - let manager = me.named_user_value::>("manager")?; - let file = me.borrow::()?; - Ok(if !manager.yanked.1.contains(&file.url) { - 0u8 - } else if manager.yanked.0 { - 2u8 - } else { - 1u8 - }) - }); - reg.add_function("is_selected", |_, me: AnyUserData| { - let manager = me.named_user_value::>("manager")?; - let folder = me.named_user_value::>("folder")?; - let file = me.borrow::()?; - - let selected = folder.files.is_selected(&file.url); - Ok(if !manager.active().mode.is_visual() { - selected - } else { - let idx: usize = me.named_user_value("idx")?; - manager.active().mode.pending(folder.offset + idx, selected) - }) - }); - reg.add_function("found", |lua, me: AnyUserData| { - let manager = me.named_user_value::>("manager")?; - let Some(finder) = &manager.active().finder else { - return Ok(None); - }; - - let file = me.borrow::()?; - if let Some(idx) = finder.matched_idx(&file.url) { - return Some( - lua.create_sequence_from([idx.into_lua(lua)?, finder.matched().len().into_lua(lua)?]), - ) - .transpose(); - } - Ok(None) - }); - reg.add_function("highlights", |_, me: AnyUserData| { - let manager = me.named_user_value::>("manager")?; - let Some(finder) = &manager.active().finder else { - return Ok(None); - }; - - let file = me.borrow::()?; - let Some(h) = file.name().and_then(|n| finder.highlighted(n)) else { - return Ok(None); - }; - - Ok(Some(h.into_iter().map(Range::from).collect::>())) - }); - })?; - - Ok(()) - } -} diff --git a/yazi-plugin/src/bindings/mod.rs b/yazi-plugin/src/bindings/mod.rs index a6ee7ff7..8ce302bd 100644 --- a/yazi-plugin/src/bindings/mod.rs +++ b/yazi-plugin/src/bindings/mod.rs @@ -1,16 +1,17 @@ #![allow(clippy::module_inception)] -mod active; mod bindings; -mod files; -mod shared; -mod tabs; -mod tasks; +mod cha; +mod file; +mod range; +mod url; -pub use active::*; pub use bindings::*; -#[allow(unused_imports)] -pub use files::*; -pub use shared::*; -pub use tabs::*; -pub use tasks::*; +pub use cha::*; +pub use file::*; +pub use range::*; +pub use url::*; + +pub trait Cast { + fn cast(lua: &mlua::Lua, data: T) -> mlua::Result; +} diff --git a/yazi-plugin/src/bindings/range.rs b/yazi-plugin/src/bindings/range.rs new file mode 100644 index 00000000..b1ee2a7a --- /dev/null +++ b/yazi-plugin/src/bindings/range.rs @@ -0,0 +1,17 @@ +use mlua::IntoLua; + +pub struct Range(std::ops::Range); + +impl From> for Range { + fn from(value: std::ops::Range) -> Self { Self(value) } +} + +impl<'lua, T> IntoLua<'lua> for Range +where + T: IntoLua<'lua>, +{ + fn into_lua(self, lua: &'lua mlua::Lua) -> mlua::Result { + let tbl = lua.create_sequence_from([self.0.start, self.0.end])?; + tbl.into_lua(lua) + } +} diff --git a/yazi-plugin/src/bindings/shared.rs b/yazi-plugin/src/bindings/shared.rs deleted file mode 100644 index 348c96ef..00000000 --- a/yazi-plugin/src/bindings/shared.rs +++ /dev/null @@ -1,43 +0,0 @@ -use mlua::{IntoLua, MetaMethod, UserData, UserDataRef}; - -// --- Range -pub struct Range(std::ops::Range); - -impl From> for Range { - fn from(value: std::ops::Range) -> Self { Self(value) } -} - -impl<'lua, T> IntoLua<'lua> for Range -where - T: IntoLua<'lua>, -{ - fn into_lua(self, lua: &'lua mlua::Lua) -> mlua::Result { - let tbl = lua.create_sequence_from([self.0.start, self.0.end])?; - tbl.into_lua(lua) - } -} - -// --- Url -pub struct Url(yazi_shared::fs::Url); - -impl From<&yazi_shared::fs::Url> for Url { - fn from(value: &yazi_shared::fs::Url) -> Self { Self(value.clone()) } -} - -impl UserData for Url { - fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("frag", |_, me| Ok(me.0.frag().map(ToOwned::to_owned))); - fields.add_field_method_get("is_regular", |_, me| Ok(me.0.is_regular())); - fields.add_field_method_get("is_search", |_, me| Ok(me.0.is_search())); - fields.add_field_method_get("is_archive", |_, me| Ok(me.0.is_archive())); - } - - fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_meta_function( - MetaMethod::Eq, - |_, (lhs, rhs): (UserDataRef, UserDataRef)| Ok(lhs.0 == rhs.0), - ); - - methods.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.0.display().to_string())); - } -} diff --git a/yazi-plugin/src/bindings/url.rs b/yazi-plugin/src/bindings/url.rs new file mode 100644 index 00000000..34ddd1e8 --- /dev/null +++ b/yazi-plugin/src/bindings/url.rs @@ -0,0 +1,38 @@ +use mlua::{AnyUserData, Lua, MetaMethod, UserDataFields, UserDataMethods, UserDataRef}; + +use super::Cast; + +pub type UrlRef<'lua> = UserDataRef<'lua, yazi_shared::fs::Url>; + +pub struct Url; + +impl Url { + pub fn register(lua: &Lua) -> mlua::Result<()> { + lua.register_userdata_type::(|reg| { + reg.add_field_method_get("frag", |_, me| Ok(me.frag().map(ToOwned::to_owned))); + reg.add_field_method_get("is_regular", |_, me| Ok(me.is_regular())); + reg.add_field_method_get("is_search", |_, me| Ok(me.is_search())); + reg.add_field_method_get("is_archive", |_, me| Ok(me.is_archive())); + + reg.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me == &*other)); + + reg.add_meta_method(MetaMethod::ToString, |lua, me, ()| { + lua.create_string(me.as_os_str().as_encoded_bytes()) + }); + + reg.add_meta_method(MetaMethod::Concat, |lua, me, other: mlua::String| { + let me = me.as_os_str().as_encoded_bytes(); + let other = other.as_bytes(); + + let mut out = Vec::with_capacity(me.len() + other.len()); + out.extend_from_slice(me); + out.extend_from_slice(other); + lua.create_string(out) + }); + }) + } +} + +impl> Cast for Url { + fn cast(lua: &Lua, data: T) -> mlua::Result { lua.create_any_userdata(data.into()) } +} diff --git a/yazi-plugin/src/cast.rs b/yazi-plugin/src/cast.rs new file mode 100644 index 00000000..f06e424f --- /dev/null +++ b/yazi-plugin/src/cast.rs @@ -0,0 +1,133 @@ +use std::collections::HashMap; + +use anyhow::bail; +use mlua::{prelude::{Lua, LuaResult}, AnyUserData, IntoLua, Value}; +use yazi_shared::OrderedFloat; + +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 { + None + } +} + +pub enum ValueSendable { + Nil, + Boolean(bool), + Integer(i64), + Number(f64), + String(Vec), + Table(HashMap), +} + +impl<'a> TryFrom> for ValueSendable { + type Error = anyhow::Error; + + fn try_from(value: Value) -> Result { + Ok(match value { + Value::Nil => ValueSendable::Nil, + Value::Boolean(b) => ValueSendable::Boolean(b), + Value::LightUserData(_) => bail!("light userdata is not supported"), + Value::Integer(n) => ValueSendable::Integer(n), + Value::Number(n) => ValueSendable::Number(n), + Value::String(s) => ValueSendable::String(s.as_bytes().to_vec()), + Value::Table(t) => { + let mut map = HashMap::with_capacity(t.len().map(|l| l as usize)?); + for result in t.pairs::() { + let (k, v) = result?; + map.insert(ValueSendable::try_from(k)?.try_into()?, v.try_into()?); + } + ValueSendable::Table(map) + } + Value::Function(_) => bail!("function is not supported"), + Value::Thread(_) => bail!("thread is not supported"), + Value::UserData(_) => bail!("userdata is not supported"), + Value::Error(_) => bail!("error is not supported"), + }) + } +} + +impl<'lua> IntoLua<'lua> for ValueSendable { + fn into_lua(self, lua: &Lua) -> LuaResult { + match self { + ValueSendable::Nil => Ok(Value::Nil), + ValueSendable::Boolean(b) => Ok(Value::Boolean(b)), + ValueSendable::Integer(n) => Ok(Value::Integer(n)), + ValueSendable::Number(n) => Ok(Value::Number(n)), + ValueSendable::String(s) => Ok(Value::String(lua.create_string(s)?)), + ValueSendable::Table(t) => { + let table = lua.create_table()?; + for (k, v) in t { + table.set(k.into_lua(lua)?, v.into_lua(lua)?)?; + } + Ok(Value::Table(table)) + } + } + } +} + +impl ValueSendable { + pub fn into_table_string(self) -> HashMap { + let ValueSendable::Table(table) = self else { + return Default::default(); + }; + + let mut map = HashMap::with_capacity(table.len()); + for pair in table { + let (ValueSendableKey::String(k), ValueSendable::String(v)) = pair else { + continue; + }; + if let (Ok(k), Ok(v)) = (String::from_utf8(k), String::from_utf8(v)) { + map.insert(k, v); + } + } + map + } +} + +#[derive(Hash, PartialEq, Eq)] +pub enum ValueSendableKey { + Nil, + Boolean(bool), + Integer(i64), + Number(OrderedFloat), + String(Vec), +} + +impl TryInto for ValueSendable { + type Error = anyhow::Error; + + fn try_into(self) -> Result { + Ok(match self { + ValueSendable::Nil => ValueSendableKey::Nil, + ValueSendable::Boolean(b) => ValueSendableKey::Boolean(b), + ValueSendable::Integer(n) => ValueSendableKey::Integer(n), + ValueSendable::Number(n) => ValueSendableKey::Number(OrderedFloat::new(n)), + ValueSendable::String(s) => ValueSendableKey::String(s), + ValueSendable::Table(_) => bail!("table is not supported"), + }) + } +} + +impl<'lua> IntoLua<'lua> for ValueSendableKey { + fn into_lua(self, lua: &Lua) -> LuaResult { + match self { + ValueSendableKey::Nil => Ok(Value::Nil), + ValueSendableKey::Boolean(b) => Ok(Value::Boolean(b)), + ValueSendableKey::Integer(n) => Ok(Value::Integer(n)), + ValueSendableKey::Number(n) => Ok(Value::Number(n.get())), + ValueSendableKey::String(s) => Ok(Value::String(lua.create_string(s)?)), + } + } +} diff --git a/yazi-plugin/src/components/base.rs b/yazi-plugin/src/components/base.rs deleted file mode 100644 index 789dfc39..00000000 --- a/yazi-plugin/src/components/base.rs +++ /dev/null @@ -1,43 +0,0 @@ -use mlua::{FromLua, Lua, Table, UserData, Value}; -use ratatui::widgets::Widget; - -use crate::{layout::Rect, GLOBALS, LUA}; - -#[derive(Clone)] -pub struct Base { - area: ratatui::layout::Rect, - - kind: u8, -} - -impl Base { - pub(crate) fn install() -> mlua::Result<()> { - let ui: Table = GLOBALS.get("ui")?; - let base: Table = ui.get("Base")?; - base.set( - "new", - LUA.create_function(|_, (area, kind): (Rect, u8)| Ok(Self { area: area.0, kind }))?, - ) - } - - pub fn render(self, cx: &yazi_core::Ctx, buf: &mut ratatui::buffer::Buffer) { - if self.kind == 0 { - super::Preview::new(cx).render(self.area, buf) - } - } -} - -impl<'lua> FromLua<'lua> for Base { - fn from_lua(value: Value<'lua>, _: &'lua Lua) -> mlua::Result { - match value { - Value::UserData(ud) => Ok(ud.borrow::()?.clone()), - _ => Err(mlua::Error::FromLuaConversionError { - from: value.type_name(), - to: "Base", - message: Some("expected a Base".to_string()), - }), - } - } -} - -impl UserData for Base {} diff --git a/yazi-plugin/src/components/components.rs b/yazi-plugin/src/components/components.rs deleted file mode 100644 index 72d58ffd..00000000 --- a/yazi-plugin/src/components/components.rs +++ /dev/null @@ -1,41 +0,0 @@ -use mlua::{AnyUserData, Table}; -use yazi_shared::RoCell; - -use super::Base; -use crate::{layout::{Bar, Border, Gauge, List, Paragraph}, GLOBALS}; - -pub(super) static COMP_FOLDER: RoCell
= RoCell::new(); -pub(super) static COMP_HEADER: RoCell
= RoCell::new(); -pub(super) static COMP_MANAGER: RoCell
= RoCell::new(); -pub(super) static COMP_STATUS: RoCell
= RoCell::new(); - -pub fn init() -> mlua::Result<()> { - COMP_FOLDER.init(GLOBALS.get("Folder")?); - COMP_HEADER.init(GLOBALS.get("Header")?); - COMP_MANAGER.init(GLOBALS.get("Manager")?); - COMP_STATUS.init(GLOBALS.get("Status")?); - Ok(()) -} - -pub(super) fn layout( - values: Vec, - cx: &yazi_core::Ctx, - buf: &mut ratatui::prelude::Buffer, -) -> mlua::Result<()> { - for value in values { - if let Ok(c) = value.take::() { - c.render(buf) - } else if let Ok(c) = value.take::() { - c.render(buf) - } else if let Ok(c) = value.take::() { - c.render(buf) - } else if let Ok(c) = value.take::() { - c.render(cx, buf) - } else if let Ok(c) = value.take::() { - c.render(buf) - } else if let Ok(c) = value.take::() { - c.render(buf) - } - } - Ok(()) -} diff --git a/yazi-plugin/src/components/folder.rs b/yazi-plugin/src/components/folder.rs deleted file mode 100644 index 9c48eb7a..00000000 --- a/yazi-plugin/src/components/folder.rs +++ /dev/null @@ -1,35 +0,0 @@ -use mlua::TableExt; -use ratatui::widgets::Widget; -use tracing::error; - -use super::{layout, COMP_FOLDER}; -use crate::{layout::Rect, LUA}; - -pub struct Folder<'a> { - cx: &'a yazi_core::Ctx, - kind: u8, -} - -impl<'a> Folder<'a> { - #[inline] - pub fn parent(cx: &'a yazi_core::Ctx) -> Self { Self { cx, kind: 0 } } - - #[inline] - pub fn current(cx: &'a yazi_core::Ctx) -> Self { Self { cx, kind: 1 } } - - #[inline] - pub fn preview(cx: &'a yazi_core::Ctx) -> Self { Self { cx, kind: 2 } } -} - -impl<'a> Widget for Folder<'a> { - fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { - let mut f = || { - let args = LUA.create_table()?; - args.set("kind", self.kind)?; - layout(COMP_FOLDER.call_method::<_, _>("render", (Rect(area), args))?, self.cx, buf) - }; - if let Err(e) = f() { - error!("{:?}", e); - } - } -} diff --git a/yazi-plugin/src/components/header.rs b/yazi-plugin/src/components/header.rs deleted file mode 100644 index 4a232c93..00000000 --- a/yazi-plugin/src/components/header.rs +++ /dev/null @@ -1,24 +0,0 @@ -use mlua::TableExt; -use ratatui::widgets::Widget; -use tracing::error; - -use super::{layout, COMP_HEADER}; -use crate::layout::Rect; - -pub struct Header<'a> { - cx: &'a yazi_core::Ctx, -} - -impl<'a> Header<'a> { - #[inline] - pub fn new(cx: &'a yazi_core::Ctx) -> Self { Self { cx } } -} - -impl<'a> Widget for Header<'a> { - fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { - let mut f = || layout(COMP_HEADER.call_method::<_, _>("render", Rect(area))?, self.cx, buf); - if let Err(e) = f() { - error!("{:?}", e); - } - } -} diff --git a/yazi-plugin/src/components/manager.rs b/yazi-plugin/src/components/manager.rs deleted file mode 100644 index 2ff4c990..00000000 --- a/yazi-plugin/src/components/manager.rs +++ /dev/null @@ -1,23 +0,0 @@ -use mlua::TableExt; -use ratatui::widgets::Widget; -use tracing::error; - -use super::{layout, COMP_MANAGER}; -use crate::layout::Rect; - -pub struct Manager<'a> { - cx: &'a yazi_core::Ctx, -} - -impl<'a> Manager<'a> { - pub fn new(cx: &'a yazi_core::Ctx) -> Self { Self { cx } } -} - -impl<'a> Widget for Manager<'a> { - fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { - let mut f = || layout(COMP_MANAGER.call_method::<_, _>("render", Rect(area))?, self.cx, buf); - if let Err(e) = f() { - error!("{:?}", e); - } - } -} diff --git a/yazi-plugin/src/components/mod.rs b/yazi-plugin/src/components/mod.rs deleted file mode 100644 index a247286a..00000000 --- a/yazi-plugin/src/components/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -#![allow(clippy::module_inception)] - -mod base; -mod components; -mod folder; -mod header; -mod manager; -mod preview; -mod status; - -pub use base::*; -pub use components::*; -pub use folder::*; -pub use header::*; -pub use manager::*; -use preview::*; -pub use status::*; diff --git a/yazi-plugin/src/components/preview.rs b/yazi-plugin/src/components/preview.rs deleted file mode 100644 index 74840c0b..00000000 --- a/yazi-plugin/src/components/preview.rs +++ /dev/null @@ -1,33 +0,0 @@ -use ansi_to_tui::IntoText; -use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}}; -use yazi_core::Ctx; -use yazi_shared::event::PreviewData; - -use super::Folder; - -pub(super) struct Preview<'a> { - cx: &'a Ctx, -} - -impl<'a> Preview<'a> { - pub(super) fn new(cx: &'a Ctx) -> Self { Self { cx } } -} - -impl<'a> Widget for Preview<'a> { - fn render(self, area: Rect, buf: &mut Buffer) { - let Some(ref lock) = self.cx.manager.active().preview.lock else { - return; - }; - - match &lock.data { - PreviewData::Folder => { - Folder::preview(self.cx).render(area, buf); - } - PreviewData::Text(s) => { - let p = Paragraph::new(s.as_bytes().into_text().unwrap()); - p.render(area, buf); - } - PreviewData::Image => {} - } - } -} diff --git a/yazi-plugin/src/components/status.rs b/yazi-plugin/src/components/status.rs deleted file mode 100644 index 6cc637e5..00000000 --- a/yazi-plugin/src/components/status.rs +++ /dev/null @@ -1,24 +0,0 @@ -use mlua::TableExt; -use ratatui::widgets::Widget; -use tracing::error; - -use super::{layout, COMP_STATUS}; -use crate::layout::Rect; - -pub struct Status<'a> { - cx: &'a yazi_core::Ctx, -} - -impl<'a> Status<'a> { - #[inline] - pub fn new(cx: &'a yazi_core::Ctx) -> Self { Self { cx } } -} - -impl<'a> Widget for Status<'a> { - fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { - let mut f = || layout(COMP_STATUS.call_method::<_, _>("render", Rect(area))?, self.cx, buf); - if let Err(e) = f() { - error!("{:?}", e); - } - } -} diff --git a/yazi-plugin/src/config.rs b/yazi-plugin/src/config.rs index 2ab52244..4b1a4a83 100644 --- a/yazi-plugin/src/config.rs +++ b/yazi-plugin/src/config.rs @@ -1,42 +1,28 @@ -use mlua::{LuaSerdeExt, SerializeOptions, Table}; -use yazi_config::{MANAGER, THEME}; +use mlua::{Lua, LuaSerdeExt, SerializeOptions}; +use yazi_config::{MANAGER, PREVIEW, THEME}; -use crate::{layout::Rect, GLOBALS, LUA}; +const OPTIONS: SerializeOptions = + SerializeOptions::new().serialize_none_to_null(false).serialize_unit_to_null(false); -#[derive(Clone, Copy)] -pub(super) struct Config; +pub struct Config<'a> { + lua: &'a Lua, +} -impl Config { - pub(super) fn install(self) -> mlua::Result<()> { - let options = - SerializeOptions::new().serialize_none_to_null(false).serialize_unit_to_null(false); +impl<'a> Config<'a> { + pub fn new(lua: &'a Lua) -> Self { Self { lua } } - self.theme(options)?; - self.manager(options)?; - Ok(()) + pub fn install_theme(self) -> mlua::Result { + self.lua.globals().set("THEME", self.lua.to_value_with(&*THEME, OPTIONS)?)?; + Ok(self) } - fn theme(self, options: SerializeOptions) -> mlua::Result<()> { - GLOBALS.set("THEME", LUA.to_value_with(&*THEME, options)?) + pub fn install_preview(self) -> mlua::Result { + self.lua.globals().set("PREVIEW", self.lua.to_value_with(&*PREVIEW, OPTIONS)?)?; + Ok(self) } - fn manager(self, options: SerializeOptions) -> mlua::Result<()> { - let manager = LUA.to_value_with(&*MANAGER, options)?; - { - let layout: Table = manager.as_table().unwrap().get("layout")?; - - layout.set( - "preview_rect", - LUA.create_function(|_, ()| Ok(Rect(MANAGER.layout.preview_rect())))?, - )?; - layout - .set("preview_height", LUA.create_function(|_, ()| Ok(MANAGER.layout.preview_height()))?)?; - layout - .set("folder_rect", LUA.create_function(|_, ()| Ok(Rect(MANAGER.layout.folder_rect())))?)?; - layout - .set("folder_height", LUA.create_function(|_, ()| Ok(MANAGER.layout.folder_height()))?)?; - } - - GLOBALS.set("MANAGER", manager) + pub fn install_manager(self) -> mlua::Result { + self.lua.globals().set("MANAGER", self.lua.to_value_with(&*MANAGER, OPTIONS)?)?; + Ok(self) } } diff --git a/yazi-plugin/src/layout/bar.rs b/yazi-plugin/src/elements/bar.rs similarity index 76% rename from yazi-plugin/src/layout/bar.rs rename to yazi-plugin/src/elements/bar.rs index 6f53dc97..d30726d3 100644 --- a/yazi-plugin/src/layout/bar.rs +++ b/yazi-plugin/src/elements/bar.rs @@ -1,7 +1,6 @@ -use mlua::{AnyUserData, FromLua, Lua, Table, UserData, Value}; +use mlua::{AnyUserData, ExternalError, Lua, Table, UserData, Value}; -use super::{Rect, Style}; -use crate::{GLOBALS, LUA}; +use super::{RectRef, Renderable, Style}; #[derive(Clone)] pub struct Bar { @@ -13,13 +12,12 @@ pub struct Bar { } impl Bar { - pub(crate) fn install() -> mlua::Result<()> { - let ui: Table = GLOBALS.get("ui")?; + pub fn install(lua: &Lua, ui: &Table) -> mlua::Result<()> { ui.set( "Bar", - LUA.create_function(|_, (area, direction): (Rect, u8)| { + lua.create_function(|_, (area, direction): (RectRef, u8)| { Ok(Self { - area: area.0, + area: *area, position: match direction { 1 => ratatui::widgets::Borders::TOP, @@ -34,8 +32,31 @@ impl Bar { })?, ) } +} - pub fn render(self, buf: &mut ratatui::buffer::Buffer) { +impl UserData for Bar { + fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_function("symbol", |_, (ud, symbol): (AnyUserData, String)| { + ud.borrow_mut::()?.symbol = symbol; + Ok(ud) + }); + methods.add_function("style", |_, (ud, value): (AnyUserData, Value)| { + { + let mut me = ud.borrow_mut::()?; + match value { + Value::Nil => me.style = None, + Value::Table(tbl) => me.style = Some(Style::from(tbl).0), + Value::UserData(ud) => me.style = Some(ud.borrow::