feat: plugin system

This commit is contained in:
sxyazi 2023-11-26 16:50:24 +08:00
parent df42ca799e
commit f997bfdea0
No known key found for this signature in database
225 changed files with 4103 additions and 3039 deletions

67
Cargo.lock generated
View file

@ -124,6 +124,12 @@ version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "arc-swap"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
[[package]] [[package]]
name = "async-channel" name = "async-channel"
version = "1.9.0" version = "1.9.0"
@ -911,6 +917,12 @@ dependencies = [
"tiff", "tiff",
] ]
[[package]]
name = "imagesize"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.1.0" version = "2.1.0"
@ -1158,7 +1170,9 @@ checksum = "7c81f8ac20188feb5461a73eabb22a34dd09d6d58513535eb587e46bff6ba250"
dependencies = [ dependencies = [
"bstr", "bstr",
"erased-serde", "erased-serde",
"futures-util",
"mlua-sys", "mlua-sys",
"mlua_derive",
"num-traits", "num-traits",
"once_cell", "once_cell",
"rustc-hash", "rustc-hash",
@ -1179,6 +1193,21 @@ dependencies = [
"pkg-config", "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]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@ -1981,6 +2010,19 @@ dependencies = [
"tokio", "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]] [[package]]
name = "toml" name = "toml"
version = "0.8.8" version = "0.8.8"
@ -2575,6 +2617,7 @@ dependencies = [
"base64", "base64",
"color_quant", "color_quant",
"image", "image",
"imagesize",
"ratatui", "ratatui",
"tokio", "tokio",
"tracing", "tracing",
@ -2587,6 +2630,7 @@ name = "yazi-config"
version = "0.1.5" version = "0.1.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap",
"clap", "clap",
"clap_complete", "clap_complete",
"clap_complete_fig", "clap_complete_fig",
@ -2626,11 +2670,12 @@ dependencies = [
"syntect", "syntect",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tokio-util",
"tracing", "tracing",
"unicode-width", "unicode-width",
"yazi-adaptor", "yazi-adaptor",
"yazi-config", "yazi-config",
"yazi-prebuild", "yazi-plugin",
"yazi-scheduler", "yazi-scheduler",
"yazi-shared", "yazi-shared",
] ]
@ -2646,6 +2691,7 @@ dependencies = [
"fdlimit", "fdlimit",
"futures", "futures",
"libc", "libc",
"mlua",
"ratatui", "ratatui",
"signal-hook-tokio", "signal-hook-tokio",
"tokio", "tokio",
@ -2657,6 +2703,7 @@ dependencies = [
"yazi-config", "yazi-config",
"yazi-core", "yazi-core",
"yazi-plugin", "yazi-plugin",
"yazi-scheduler",
"yazi-shared", "yazi-shared",
] ]
@ -2666,12 +2713,22 @@ version = "0.1.5"
dependencies = [ dependencies = [
"ansi-to-tui", "ansi-to-tui",
"anyhow", "anyhow",
"futures",
"libc",
"md-5",
"mlua", "mlua",
"parking_lot",
"ratatui", "ratatui",
"serde",
"serde_json",
"syntect",
"tokio",
"tokio-util",
"tracing", "tracing",
"unicode-width", "unicode-width",
"yazi-adaptor",
"yazi-config", "yazi-config",
"yazi-core", "yazi-prebuild",
"yazi-shared", "yazi-shared",
] ]
@ -2690,16 +2747,15 @@ dependencies = [
"base64", "base64",
"crossterm", "crossterm",
"futures", "futures",
"libc",
"parking_lot", "parking_lot",
"regex", "regex",
"serde",
"serde_json",
"tokio", "tokio",
"tokio-stream",
"tracing", "tracing",
"trash", "trash",
"yazi-adaptor", "yazi-adaptor",
"yazi-config", "yazi-config",
"yazi-plugin",
"yazi-shared", "yazi-shared",
] ]
@ -2716,6 +2772,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"ratatui", "ratatui",
"regex", "regex",
"serde",
"tokio", "tokio",
] ]

View file

@ -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"}

View file

@ -17,6 +17,7 @@ anyhow = "^1"
base64 = "^0" base64 = "^0"
color_quant = "^1" color_quant = "^1"
image = "^0" image = "^0"
imagesize = "^0"
ratatui = "^0" ratatui = "^0"
tokio = { version = "^1", features = [ "parking_lot", "io-util", "process" ] } tokio = { version = "^1", features = [ "parking_lot", "io-util", "process" ] }

View file

@ -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 anyhow::{anyhow, Result};
use ratatui::prelude::Rect; use ratatui::prelude::Rect;
use tokio::{fs, sync::mpsc::UnboundedSender};
use tracing::warn; use tracing::warn;
use yazi_config::PREVIEW; use yazi_shared::env_exists;
use yazi_shared::{env_exists, RoCell};
use super::{Iterm2, Kitty, KittyOld}; use super::{Iterm2, Kitty, KittyOld};
use crate::{ueberzug::Ueberzug, Sixel, TMUX}; use crate::{ueberzug::Ueberzug, Sixel, TMUX};
static IMAGE_SHOWN: AtomicBool = AtomicBool::new(false); static IMAGE_SHOWN: AtomicBool = AtomicBool::new(false);
#[allow(clippy::type_complexity)]
static UEBERZUG: RoCell<Option<UnboundedSender<Option<(PathBuf, Rect)>>>> = RoCell::new();
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Adaptor { pub enum Adaptor {
Kitty, Kitty,
@ -164,16 +159,9 @@ impl ToString for Adaptor {
} }
impl Adaptor { impl Adaptor {
pub(super) fn start(self) { pub(super) fn start(self) { Ueberzug::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 async fn image_show(self, path: &Path, rect: Rect) -> Result<(u32, u32)> {
self.image_hide(rect).ok(); self.image_hide(rect).ok();
IMAGE_SHOWN.store(true, Ordering::Relaxed); IMAGE_SHOWN.store(true, Ordering::Relaxed);
@ -182,9 +170,7 @@ impl Adaptor {
Self::KittyOld => KittyOld::image_show(path, rect).await, Self::KittyOld => KittyOld::image_show(path, rect).await,
Self::Iterm2 => Iterm2::image_show(path, rect).await, Self::Iterm2 => Iterm2::image_show(path, rect).await,
Self::Sixel => Sixel::image_show(path, rect).await, Self::Sixel => Sixel::image_show(path, rect).await,
_ => Ok(if let Some(tx) = &*UEBERZUG { _ => Ueberzug::image_show(path, rect).await,
tx.send(Some((path.to_path_buf(), rect)))?;
}),
} }
} }
@ -198,9 +184,7 @@ impl Adaptor {
Self::Iterm2 => Iterm2::image_hide(rect), Self::Iterm2 => Iterm2::image_hide(rect),
Self::KittyOld => KittyOld::image_hide(), Self::KittyOld => KittyOld::image_hide(),
Self::Sixel => Sixel::image_hide(rect), Self::Sixel => Sixel::image_hide(rect),
_ => Ok(if let Some(tx) = &*UEBERZUG { _ => Ueberzug::image_hide(rect),
tx.send(None)?;
}),
} }
} }

View file

@ -2,51 +2,13 @@ use std::{fs::File, io::BufReader, path::{Path, PathBuf}};
use anyhow::Result; use anyhow::Result;
use image::{imageops::FilterType, io::Limits, DynamicImage, ImageFormat}; use image::{imageops::FilterType, io::Limits, DynamicImage, ImageFormat};
use ratatui::layout::Rect;
use yazi_config::{PREVIEW, TASKS}; use yazi_config::{PREVIEW, TASKS};
use yazi_shared::term::Term; use yazi_shared::term::Term;
pub struct Image; pub struct Image;
impl Image { impl Image {
fn set_limits(mut r: image::io::Reader<BufReader<File>>) -> image::io::Reader<BufReader<File>> {
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<DynamicImage> {
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<()> { pub async fn precache(path: &Path, cache: PathBuf) -> Result<()> {
let path = path.to_owned(); let path = path.to_owned();
let mut img = tokio::task::spawn_blocking(move || { let mut img = tokio::task::spawn_blocking(move || {
@ -69,21 +31,45 @@ impl Image {
.await? .await?
} }
pub async fn precache_vec(bin: Vec<u8>, cache: PathBuf) -> Result<()> { pub(super) async fn downscale(path: &Path, rect: Rect) -> Result<DynamicImage> {
let mut img = tokio::task::spawn_blocking(move || image::load_from_memory(&bin)).await??; 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 || { tokio::task::spawn_blocking(move || {
let (w, h) = (PREVIEW.max_width, PREVIEW.max_height); Ok(if img.width() > w || img.height() > h {
if img.width() > w || img.height() > h { img.resize(w, h, FilterType::Triangle)
img = img.resize(w, h, FilterType::Triangle); } else {
} img
})
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),
}?)
}) })
.await? .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<BufReader<File>>) -> image::io::Reader<BufReader<File>> {
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
}
} }

View file

@ -12,12 +12,16 @@ use crate::{CLOSE, START};
pub(super) struct Iterm2; pub(super) struct Iterm2;
impl Iterm2 { impl Iterm2 {
pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> { pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> {
let img = Image::downscale(path, (rect.width, rect.height)).await?; let img = Image::downscale(path, rect).await?;
let size = (img.width(), img.height());
let b = Self::encode(img).await?; let b = Self::encode(img).await?;
Self::image_hide(rect)?; 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<()> { pub(super) fn image_hide(rect: Rect) -> Result<()> {

View file

@ -312,8 +312,9 @@ static DIACRITICS: [char; 297] = [
pub(super) struct Kitty; pub(super) struct Kitty;
impl Kitty { impl Kitty {
pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> { pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> {
let img = Image::downscale(path, (rect.width, rect.height)).await?; let img = Image::downscale(path, rect).await?;
let size = (img.width(), img.height());
let b = Self::encode(img).await?; let b = Self::encode(img).await?;
Self::image_hide(rect)?; Self::image_hide(rect)?;
@ -335,7 +336,7 @@ impl Kitty {
stdout.write_all(buf.as_bytes())?; stdout.write_all(buf.as_bytes())?;
} }
Ok(()) Ok(size)
}) })
} }

View file

@ -12,12 +12,16 @@ use crate::{CLOSE, ESCAPE, START};
pub(super) struct KittyOld; pub(super) struct KittyOld;
impl KittyOld { impl KittyOld {
pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> { pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> {
let img = Image::downscale(path, (rect.width, rect.height)).await?; let img = Image::downscale(path, rect).await?;
let size = (img.width(), img.height());
let b = Self::encode(img).await?; let b = Self::encode(img).await?;
Self::image_hide()?; 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] #[inline]

View file

@ -11,12 +11,16 @@ use crate::{Image, CLOSE, ESCAPE, START};
pub(super) struct Sixel; pub(super) struct Sixel;
impl Sixel { impl Sixel {
pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> { pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> {
let img = Image::downscale(path, (rect.width, rect.height)).await?; let img = Image::downscale(path, rect).await?;
let size = (img.width(), img.height());
let b = Self::encode(img).await?; let b = Self::encode(img).await?;
Self::image_hide(rect)?; 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<()> { pub(super) fn image_hide(rect: Rect) -> Result<()> {

View file

@ -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 ratatui::prelude::Rect;
use tokio::{io::AsyncWriteExt, process::{Child, Command}, sync::mpsc::{self, UnboundedSender}}; use tokio::{io::AsyncWriteExt, process::{Child, Command}, sync::mpsc::{self, UnboundedSender}};
use tracing::debug; use tracing::debug;
use yazi_config::PREVIEW; use yazi_config::PREVIEW;
use yazi_shared::RoCell;
use crate::Adaptor; use crate::{Adaptor, Image};
#[allow(clippy::type_complexity)]
static DEMON: RoCell<Option<UnboundedSender<Option<(PathBuf, Rect)>>>> = RoCell::new();
pub(super) struct Ueberzug; pub(super) struct Ueberzug;
impl Ueberzug { impl Ueberzug {
pub(super) fn start(adaptor: Adaptor) -> Result<UnboundedSender<Option<(PathBuf, Rect)>>> { pub(super) fn start(adaptor: Adaptor) {
if !adaptor.needs_ueberzug() {
return DEMON.init(None);
}
let mut child = Self::create_demon(adaptor).ok(); let mut child = Self::create_demon(adaptor).ok();
let (tx, mut rx) = mpsc::unbounded_channel(); 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<Child> { fn create_demon(adaptor: Adaptor) -> Result<Child> {

View file

@ -13,6 +13,7 @@ yazi-shared = { path = "../yazi-shared", version = "0.1.5" }
# External dependencies # External dependencies
anyhow = "^1" anyhow = "^1"
arc-swap = "^1"
clap = { version = "^4", features = [ "derive" ] } clap = { version = "^4", features = [ "derive" ] }
crossterm = "^0" crossterm = "^0"
dirs = "^5" dirs = "^5"

View file

@ -1,5 +1,5 @@
#[path = "src/boot/cli.rs"] #[path = "src/boot/args.rs"]
mod cli; mod args;
use std::{env, error::Error, fs}; use std::{env, error::Error, fs};
@ -14,7 +14,7 @@ fn main() -> Result<(), Box<dyn Error>> {
return Ok(()); return Ok(());
} }
let cmd = &mut cli::Args::command(); let cmd = &mut args::Args::command();
let bin = "yazi"; let bin = "yazi";
let out = "completions"; let out = "completions";

View file

@ -33,10 +33,10 @@ keymap = [
{ on = [ "H" ], exec = "back", desc = "Go back to the previous directory" }, { on = [ "H" ], exec = "back", desc = "Go back to the previous directory" },
{ on = [ "L" ], exec = "forward", desc = "Go forward to the next directory" }, { on = [ "L" ], exec = "forward", desc = "Go forward to the next directory" },
{ on = [ "<A-k>" ], exec = "peek -5", desc = "Peek up 5 units in the preview" }, { on = [ "<A-k>" ], exec = "seek -5", desc = "Peek up 5 units in the preview" },
{ on = [ "<A-j>" ], exec = "peek 5", desc = "Peek down 5 units in the preview" }, { on = [ "<A-j>" ], exec = "seek 5", desc = "Peek down 5 units in the preview" },
{ on = [ "<A-PageUp>" ], exec = "peek -5", desc = "Peek up 5 units in the preview" }, { on = [ "<A-PageUp>" ], exec = "seek -5", desc = "Peek up 5 units in the preview" },
{ on = [ "<A-PageDown>" ], exec = "peek 5", desc = "Peek down 5 units in the preview" }, { on = [ "<A-PageDown>" ], exec = "seek 5", desc = "Peek down 5 units in the preview" },
{ on = [ "<Up>" ], exec = "arrow -1", desc = "Move cursor up" }, { on = [ "<Up>" ], exec = "arrow -1", desc = "Move cursor up" },
{ on = [ "<Down>" ], exec = "arrow 1", desc = "Move cursor down" }, { on = [ "<Down>" ], exec = "arrow 1", desc = "Move cursor down" },

View file

@ -27,10 +27,6 @@ tab_width = 1
border_symbol = "│" border_symbol = "│"
border_style = { fg = "gray" } border_style = { fg = "gray" }
# Offset
folder_offset = [ 1, 0, 1, 0 ]
preview_offset = [ 1, 1, 1, 1 ]
# Highlighting # Highlighting
syntect_theme = "" syntect_theme = ""

View file

@ -61,6 +61,7 @@ rules = [
{ mime = "application/x-bzip2", use = [ "extract", "reveal" ] }, { mime = "application/x-bzip2", use = [ "extract", "reveal" ] },
{ mime = "application/x-7z-compressed", use = [ "extract", "reveal" ] }, { mime = "application/x-7z-compressed", use = [ "extract", "reveal" ] },
{ mime = "application/x-rar", use = [ "extract", "reveal" ] }, { mime = "application/x-rar", use = [ "extract", "reveal" ] },
{ mime = "application/xz", use = [ "extract", "reveal" ] },
{ mime = "*", use = [ "open", "reveal" ] }, { mime = "*", use = [ "open", "reveal" ] },
] ]
@ -73,8 +74,44 @@ image_alloc = 536870912 # 512MB
image_bound = [ 0, 0 ] image_bound = [ 0, 0 ]
suppress_preload = false suppress_preload = false
[plugins] [plugin]
preload = []
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] [input]
# cd # cd

View file

@ -4,7 +4,7 @@ use clap::{command, Parser};
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(name = "yazi")] #[command(name = "yazi")]
pub(super) struct Args { pub struct Args {
/// Set the current working entry /// Set the current working entry
#[arg(index = 1)] #[arg(index = 1)]
pub entry: Option<PathBuf>, pub entry: Option<PathBuf>,

View file

@ -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 clap::Parser;
use yazi_shared::fs::{current_cwd, expand_path}; use yazi_shared::fs::{current_cwd, expand_path};
use super::cli::Args; use super::Args;
use crate::{Xdg, PREVIEW}; use crate::{Xdg, ARGS};
#[derive(Debug)] #[derive(Debug)]
pub struct Boot { pub struct Boot {
pub cwd: PathBuf, pub cwd: PathBuf,
pub file: Option<OsString>, pub file: Option<OsString>,
pub config_dir: PathBuf,
pub plugin_dir: PathBuf,
pub state_dir: PathBuf, pub state_dir: PathBuf,
pub cwd_file: Option<PathBuf>,
pub chooser_file: Option<PathBuf>,
} }
impl Boot { impl Boot {
fn parse_entry(entry: Option<PathBuf>) -> (PathBuf, Option<OsString>) { fn parse_entry(entry: Option<&Path>) -> (PathBuf, Option<OsString>) {
let entry = match entry { let entry = match entry {
Some(p) => expand_path(p), Some(p) => expand_path(p),
None => return (current_cwd().unwrap(), None), None => return (current_cwd().unwrap(), None),
@ -35,7 +34,28 @@ impl Boot {
impl Default for Boot { impl Default for Boot {
fn default() -> Self { 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 { if args.version {
println!( println!(
"yazi {} ({} {})", "yazi {} ({} {})",
@ -46,37 +66,6 @@ impl Default for Boot {
process::exit(0); process::exit(0);
} }
let (cwd, file) = Self::parse_entry(args.entry); args
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
} }
} }

View file

@ -1,4 +1,5 @@
mod args;
mod boot; mod boot;
mod cli;
pub use args::*;
pub use boot::*; pub use boot::*;

View file

@ -34,7 +34,7 @@ where
type Value = Vec<Exec>; type Value = Vec<Exec>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 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<A>(self, mut seq: A) -> Result<Self::Value, A::Error> fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
@ -45,6 +45,9 @@ where
while let Some(value) = &seq.next_element::<String>()? { while let Some(value) = &seq.next_element::<String>()? {
execs.push(parse(value).map_err(de::Error::custom)?); 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) Ok(execs)
} }

View file

@ -4,7 +4,7 @@ mod key;
mod keymap; mod keymap;
pub use control::*; pub use control::*;
#[allow(unused_imports)]
pub use exec::*; pub use exec::*;
#[allow(unused_imports)]
pub use key::*; pub use key::*;
pub use keymap::*; pub use keymap::*;

12
yazi-config/src/layout.rs Normal file
View file

@ -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,
}

View file

@ -4,11 +4,12 @@ use yazi_shared::RoCell;
mod boot; mod boot;
pub mod keymap; pub mod keymap;
mod layout;
mod log; mod log;
pub mod manager; pub mod manager;
pub mod open; pub mod open;
mod pattern; mod pattern;
pub mod plugins; pub mod plugin;
pub mod popup; pub mod popup;
mod preset; mod preset;
pub mod preview; pub mod preview;
@ -17,10 +18,15 @@ pub mod theme;
mod validation; mod validation;
mod xdg; mod xdg;
pub use layout::*;
pub(crate) use pattern::*; pub(crate) use pattern::*;
pub(crate) use preset::*; pub(crate) use preset::*;
pub(crate) use xdg::*; pub(crate) use xdg::*;
pub static ARGS: RoCell<boot::Args> = RoCell::new();
pub static BOOT: RoCell<boot::Boot> = RoCell::new();
pub static LAYOUT: RoCell<arc_swap::ArcSwap<Layout>> = RoCell::new();
static MERGED_KEYMAP: RoCell<String> = RoCell::new(); static MERGED_KEYMAP: RoCell<String> = RoCell::new();
static MERGED_THEME: RoCell<String> = RoCell::new(); static MERGED_THEME: RoCell<String> = RoCell::new();
static MERGED_YAZI: RoCell<String> = RoCell::new(); static MERGED_YAZI: RoCell<String> = RoCell::new();
@ -29,16 +35,18 @@ pub static KEYMAP: RoCell<keymap::Keymap> = RoCell::new();
pub static LOG: RoCell<log::Log> = RoCell::new(); pub static LOG: RoCell<log::Log> = RoCell::new();
pub static MANAGER: RoCell<manager::Manager> = RoCell::new(); pub static MANAGER: RoCell<manager::Manager> = RoCell::new();
pub static OPEN: RoCell<open::Open> = RoCell::new(); pub static OPEN: RoCell<open::Open> = RoCell::new();
pub static PLUGINS: RoCell<plugins::Plugins> = RoCell::new(); pub static PLUGIN: RoCell<plugin::Plugin> = RoCell::new();
pub static PREVIEW: RoCell<preview::Preview> = RoCell::new(); pub static PREVIEW: RoCell<preview::Preview> = RoCell::new();
pub static TASKS: RoCell<tasks::Tasks> = RoCell::new(); pub static TASKS: RoCell<tasks::Tasks> = RoCell::new();
pub static THEME: RoCell<theme::Theme> = RoCell::new(); pub static THEME: RoCell<theme::Theme> = RoCell::new();
pub static INPUT: RoCell<popup::Input> = RoCell::new(); pub static INPUT: RoCell<popup::Input> = RoCell::new();
pub static SELECT: RoCell<popup::Select> = RoCell::new(); pub static SELECT: RoCell<popup::Select> = RoCell::new();
pub static BOOT: RoCell<boot::Boot> = RoCell::new();
pub fn init() { pub fn init() {
ARGS.with(Default::default);
BOOT.with(Default::default);
LAYOUT.with(Default::default);
MERGED_KEYMAP.with(Preset::keymap); MERGED_KEYMAP.with(Preset::keymap);
MERGED_THEME.with(Preset::theme); MERGED_THEME.with(Preset::theme);
MERGED_YAZI.with(Preset::yazi); MERGED_YAZI.with(Preset::yazi);
@ -47,12 +55,10 @@ pub fn init() {
LOG.with(Default::default); LOG.with(Default::default);
MANAGER.with(Default::default); MANAGER.with(Default::default);
OPEN.with(Default::default); OPEN.with(Default::default);
PLUGINS.with(Default::default); PLUGIN.with(Default::default);
PREVIEW.with(Default::default); PREVIEW.with(Default::default);
TASKS.with(Default::default); TASKS.with(Default::default);
THEME.with(Default::default); THEME.with(Default::default);
INPUT.with(Default::default); INPUT.with(Default::default);
SELECT.with(Default::default); SELECT.with(Default::default);
BOOT.with(Default::default);
} }

View file

@ -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<u16>")]
pub struct ManagerLayout {
pub parent: u16,
pub current: u16,
pub preview: u16,
pub all: u16,
}
impl TryFrom<Vec<u16>> for ManagerLayout {
type Error = anyhow::Error;
fn try_from(ratio: Vec<u16>) -> Result<Self, Self::Error> {
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 }
}

View file

@ -1,12 +1,13 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
use super::{ManagerLayout, SortBy}; use super::{ManagerRatio, SortBy};
use crate::{validation::check_validation, MERGED_YAZI}; use crate::{validation::check_validation, MERGED_YAZI};
#[derive(Debug, Deserialize, Serialize, Validate)] #[derive(Debug, Deserialize, Serialize, Validate)]
pub struct Manager { pub struct Manager {
pub layout: ManagerLayout, // FIXME: rename this to "ratio"
pub layout: ManagerRatio,
// Sorting // Sorting
pub sort_by: SortBy, pub sort_by: SortBy,

View file

@ -1,7 +1,7 @@
mod layout;
mod manager; mod manager;
mod ratio;
mod sorting; mod sorting;
pub use layout::*;
pub use manager::*; pub use manager::*;
pub use ratio::*;
pub use sorting::*; pub use sorting::*;

View file

@ -0,0 +1,31 @@
use anyhow::bail;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(try_from = "Vec<u16>")]
pub struct ManagerRatio {
pub parent: u16,
pub current: u16,
pub preview: u16,
pub all: u16,
}
impl TryFrom<Vec<u16>> for ManagerRatio {
type Error = anyhow::Error;
fn try_from(ratio: Vec<u16>) -> Result<Self, Self::Error> {
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],
})
}
}

View file

@ -23,7 +23,7 @@ impl Open {
P: AsRef<Path>, P: AsRef<Path>,
M: AsRef<str>, M: AsRef<str>,
{ {
let is_folder = Some(mime.as_ref() == MIME_DIR); let is_folder = mime.as_ref() == MIME_DIR;
self.rules.iter().find_map(|rule| { self.rules.iter().find_map(|rule| {
if rule.mime.as_ref().is_some_and(|m| m.matches(&mime)) 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)) || rule.name.as_ref().is_some_and(|n| n.match_path(&path, is_folder))

View file

@ -23,14 +23,14 @@ impl Pattern {
} }
#[inline] #[inline]
pub fn match_path(&self, path: impl AsRef<Path>, is_folder: Option<bool>) -> bool { pub fn match_path(&self, path: impl AsRef<Path>, is_folder: bool) -> bool {
let path = path.as_ref(); let path = path.as_ref();
let s = if self.full_path { let s = if self.full_path {
path.to_str() path.to_str()
} else { } else {
path.file_name().and_then(|n| n.to_str()).or_else(|| path.to_str()) 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))
} }
} }

View file

@ -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<Exec, D::Error>
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<A>(self, _: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
Err(de::Error::custom("`exec` within [plugin] must be a string"))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
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)
}

View file

@ -0,0 +1,7 @@
mod exec;
mod plugin;
pub use exec::*;
pub use plugin::*;
pub const MAX_PRELOADERS: u8 = 32;

View file

@ -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<PluginRule>,
pub previewers: Vec<PluginRule>,
}
#[derive(Deserialize)]
pub struct PluginRule {
#[serde(default)]
pub id: u8,
pub cond: Option<Condition>,
pub name: Option<Pattern>,
pub mime: Option<Pattern>,
#[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::<Outer>(&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))
})
}
}

View file

@ -1,3 +0,0 @@
mod plugins;
pub use plugins::*;

View file

@ -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<PathBuf>,
}
impl Default for Plugins {
fn default() -> Self {
#[derive(Deserialize)]
struct Outer {
plugins: Plugins,
}
let mut plugins = toml::from_str::<Outer>(&MERGED_YAZI).unwrap().plugins;
plugins.preload.iter_mut().for_each(|p| {
*p = expand_path(&p);
});
plugins
}
}

View file

@ -2,7 +2,7 @@ use std::fs;
use toml::Table; use toml::Table;
use crate::xdg::Xdg; use crate::BOOT;
pub(crate) struct Preset; pub(crate) struct Preset;
@ -29,7 +29,7 @@ impl Preset {
} }
fn merge_str(user: &str, base: &str) -> String { 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::<Table>().unwrap(); let mut user = fs::read_to_string(path).unwrap_or_default().parse::<Table>().unwrap();
let base = base.parse::<Table>().unwrap(); let base = base.parse::<Table>().unwrap();

View file

@ -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, Serialize};
use serde::Deserialize;
use yazi_shared::fs::expand_path; 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 struct Preview {
pub tab_size: u8, pub tab_size: u8,
pub max_width: u32, pub max_width: u32,
@ -37,10 +36,26 @@ impl Default for Preview {
} }
let preview = toml::from_str::<Outer>(&MERGED_YAZI).unwrap().preview; let preview = toml::from_str::<Outer>(&MERGED_YAZI).unwrap().preview;
let cache_dir = let cache_dir =
preview.cache_dir.filter(|p| !p.is_empty()).map_or_else(Xdg::cache_dir, expand_path); 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 { Preview {
tab_size: preview.tab_size, tab_size: preview.tab_size,
max_width: preview.max_width, max_width: preview.max_width,
@ -55,13 +70,6 @@ impl Default for Preview {
} }
impl 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] #[inline]
pub fn tmpfile(&self, prefix: &str) -> PathBuf { pub fn tmpfile(&self, prefix: &str) -> PathBuf {
let nanos = SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap().as_nanos(); let nanos = SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap().as_nanos();

View file

@ -1,6 +1,7 @@
use std::path::Path; use std::path::Path;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use yazi_shared::MIME_DIR;
use super::{Color, Style, StyleShadow}; use super::{Color, Style, StyleShadow};
use crate::Pattern; use crate::Pattern;
@ -12,14 +13,10 @@ pub struct Filetype {
} }
impl Filetype { impl Filetype {
pub fn matches(&self, path: &Path, mime: Option<impl AsRef<str>>, is_dir: bool) -> bool { pub fn matches(&self, path: &Path, mime: Option<&str>) -> bool {
if self.name.as_ref().is_some_and(|e| e.match_path(path, Some(is_dir))) { let is_dir = mime == Some(MIME_DIR);
return true; 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))
if let Some(mime) = mime {
return self.mime.as_ref().is_some_and(|m| m.matches(mime));
}
false
} }
} }

View file

@ -34,10 +34,6 @@ pub struct Manager {
pub border_symbol: String, pub border_symbol: String,
pub border_style: Style, pub border_style: Style,
// Offset
pub(crate) folder_offset: (u16, u16, u16, u16),
pub(crate) preview_offset: (u16, u16, u16, u16),
// Highlighting // Highlighting
pub syntect_theme: PathBuf, pub syntect_theme: PathBuf,
} }

View file

@ -24,6 +24,9 @@ impl Xdg {
} }
} }
#[inline]
pub(super) fn plugin_dir() -> Option<PathBuf> { Self::config_dir().map(|p| p.join("plugins")) }
pub(super) fn state_dir() -> Option<PathBuf> { pub(super) fn state_dir() -> Option<PathBuf> {
#[cfg(windows)] #[cfg(windows)]
{ {

View file

@ -11,6 +11,7 @@ repository = "https://github.com/sxyazi/yazi"
[dependencies] [dependencies]
yazi-adaptor = { path = "../yazi-adaptor", version = "0.1.5" } yazi-adaptor = { path = "../yazi-adaptor", version = "0.1.5" }
yazi-config = { path = "../yazi-config", 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-scheduler = { path = "../yazi-scheduler", version = "0.1.5" }
yazi-shared = { path = "../yazi-shared", 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" ] } 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 = { version = "^1", features = [ "parking_lot", "macros", "rt-multi-thread", "sync", "time", "fs", "process", "io-std", "io-util" ] }
tokio-stream = "^0" tokio-stream = "^0"
tokio-util = "^0"
unicode-width = "^0" unicode-width = "^0"
yazi-prebuild = "^0"
# Logging # Logging
tracing = { version = "^0", features = [ "max_level_debug", "release_max_level_warn" ] } tracing = { version = "^0", features = [ "max_level_debug", "release_max_level_warn" ] }

View file

@ -1,4 +1,4 @@
mod commands; mod commands;
mod completion; mod completion;
pub(super) use completion::*; pub use completion::*;

View file

@ -1,8 +1,8 @@
use ratatui::layout::Rect; use ratatui::layout::Rect;
use yazi_config::MANAGER; use yazi_config::LAYOUT;
use yazi_shared::{emit, fs::{File, FilesOp}, fs::Url}; use yazi_shared::{emit, fs::{File, FilesOp, Url}};
use crate::{files::Files, Step}; use crate::{folder::Files, Step};
#[derive(Default)] #[derive(Default)]
pub struct Folder { pub struct Folder {
@ -50,7 +50,7 @@ impl Folder {
} }
pub fn set_page(&mut self, force: bool) { 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 { if limit == 0 {
return; return;
} }
@ -81,8 +81,8 @@ impl Folder {
let old = (self.cursor, self.offset); let old = (self.cursor, self.offset);
let len = self.files.len(); let len = self.files.len();
let limit = MANAGER.layout.folder_height(); let limit = LAYOUT.load().current.height as usize;
self.cursor = step.add(self.cursor, || limit).min(len.saturating_sub(1)); 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) { self.offset = if self.cursor >= (self.offset + limit).min(len).saturating_sub(5) {
len.saturating_sub(limit).min(self.offset + self.cursor - old.0) len.saturating_sub(limit).min(self.offset + self.cursor - old.0)
} else { } else {
@ -97,7 +97,7 @@ impl Folder {
let old = (self.cursor, self.offset); let old = (self.cursor, self.offset);
let max = self.files.len().saturating_sub(1); 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 = if self.cursor < self.offset + 5 {
self.offset.saturating_sub(old.0 - self.cursor) self.offset.saturating_sub(old.0 - self.cursor)
} else { } else {
@ -133,7 +133,7 @@ impl Folder {
pub fn paginate(&self, page: usize) -> &[File] { pub fn paginate(&self, page: usize) -> &[File] {
let len = self.files.len(); 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 start = (page * limit).min(len.saturating_sub(1));
let end = (start + limit).min(len); let end = (start + limit).min(len);
@ -143,7 +143,7 @@ impl Folder {
pub fn rect_current(&self, url: &Url) -> Option<Rect> { pub fn rect_current(&self, url: &Url) -> Option<Rect> {
let y = self.files.position(url)? - self.offset; 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.y = rect.y.saturating_sub(1) + y as u16;
rect.height = 1; rect.height = 1;
Some(rect) Some(rect)

View file

@ -1,5 +1,7 @@
mod files; mod files;
mod folder;
mod sorter; mod sorter;
pub use files::*; pub use files::*;
pub use folder::*;
pub use sorter::*; pub use sorter::*;

View file

@ -68,16 +68,16 @@ impl FilesSorter {
let mut entities = Vec::with_capacity(items.len()); let mut entities = Vec::with_capacity(items.len());
for (i, file) in items.iter().enumerate() { for (i, file) in items.iter().enumerate() {
indices.push(i); 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| { 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 { if promote != Ordering::Equal {
return promote; 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 } if self.reverse { ordering.reverse() } else { ordering }
}); });

View file

@ -1,8 +1,8 @@
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use syntect::{easy::HighlightLines, util::as_24_bit_terminal_escaped}; use syntect::{easy::HighlightLines, util::as_24_bit_terminal_escaped};
use yazi_plugin::external::Highlighter;
use super::Input; use super::Input;
use crate::Highlighter;
impl Input { impl Input {
pub fn value_pretty(&self) -> Result<String> { pub fn value_pretty(&self) -> Result<String> {

View file

@ -8,13 +8,10 @@
mod clipboard; mod clipboard;
pub mod completion; pub mod completion;
mod context; pub mod folder;
pub mod files;
pub mod help; pub mod help;
mod highlighter;
pub mod input; pub mod input;
pub mod manager; pub mod manager;
pub mod preview;
pub mod select; pub mod select;
mod step; mod step;
pub mod tab; pub mod tab;
@ -22,8 +19,6 @@ pub mod tasks;
pub mod which; pub mod which;
pub use clipboard::*; pub use clipboard::*;
pub use context::*;
pub use highlighter::*;
pub use step::*; pub use step::*;
pub fn init() { pub fn init() {

View file

@ -27,7 +27,7 @@ impl Manager {
let mut b = self.current_mut().repos(opt.url); let mut b = self.current_mut().repos(opt.url);
// Re-peek // Re-peek
b |= self.peek(0); b |= self.peek(());
// Refresh watcher // Refresh watcher
let mut to_watch = BTreeSet::new(); let mut to_watch = BTreeSet::new();

View file

@ -9,9 +9,11 @@ mod quit;
mod refresh; mod refresh;
mod remove; mod remove;
mod rename; mod rename;
mod seek;
mod suspend; mod suspend;
mod tab_close; mod tab_close;
mod tab_create; mod tab_create;
mod tab_swap; mod tab_swap;
mod tab_switch; mod tab_switch;
mod update_mimetype;
mod yank; mod yank;

View file

@ -1,7 +1,7 @@
use std::ffi::OsString; use std::ffi::OsString;
use yazi_config::{popup::SelectCfg, OPEN}; use yazi_config::{popup::SelectCfg, OPEN};
use yazi_scheduler::external; use yazi_plugin::external;
use yazi_shared::{event::Exec, MIME_DIR}; use yazi_shared::{event::Exec, MIME_DIR};
use crate::{manager::Manager, select::Select, tasks::Tasks}; use crate::{manager::Manager, select::Select, tasks::Tasks};

View file

@ -1,11 +1,10 @@
use yazi_config::MANAGER; use yazi_shared::{event::Exec, fs::Url, MIME_DIR};
use yazi_shared::{emit, event::Exec, fs::Url, Layer, MIME_DIR};
use crate::manager::Manager; use crate::manager::Manager;
#[derive(Debug)] #[derive(Debug, Default)]
pub struct Opt { pub struct Opt {
step: isize, skip: Option<usize>,
only_if: Option<Url>, only_if: Option<Url>,
upper_bound: bool, upper_bound: bool,
} }
@ -13,28 +12,17 @@ pub struct Opt {
impl From<&Exec> for Opt { impl From<&Exec> for Opt {
fn from(e: &Exec) -> Self { fn from(e: &Exec) -> Self {
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), only_if: e.named.get("only-if").map(Url::from),
upper_bound: e.named.contains_key("upper-bound"), upper_bound: e.named.contains_key("upper-bound"),
} }
} }
} }
impl From<isize> for Opt { impl From<()> for Opt {
fn from(step: isize) -> Self { Self { step, only_if: None, upper_bound: false } } fn from(_: ()) -> Self { Default::default() }
} }
impl Manager { 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<Opt>) -> bool { pub fn peek(&mut self, opt: impl Into<Opt>) -> bool {
let Some(hovered) = self.hovered() else { let Some(hovered) = self.hovered() else {
return self.active_mut().preview.reset(); return self.active_mut().preview.reset();
@ -45,42 +33,30 @@ impl Manager {
return false; return false;
} }
if hovered.is_dir() { let mime = if hovered.is_dir() {
return self.peek_folder(opt, hovered.url.clone()); MIME_DIR.to_owned()
} } else if let Some(s) = self.mimetype.get(&hovered.url) {
s.to_owned()
let Some(mime) = self.mimetype.get(&hovered.url).cloned() else { } else {
return self.active_mut().preview.reset(); return self.active_mut().preview.reset();
}; };
let (url, cha) = (hovered.url.clone(), hovered.cha); let hovered = hovered.clone();
if opt.upper_bound { if !self.active().preview.same_url(&hovered.url) {
self.active_mut().preview.arrow(0, &mime, Some(opt.step as usize)); self.active_mut().preview.skip = 0;
} 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));
self.active_mut().preview.reset(); self.active_mut().preview.reset();
} }
self.active_mut().preview.go(&url, cha, &mime); if let Some(skip) = opt.skip {
false let preview = &mut self.active_mut().preview;
} if opt.upper_bound {
preview.skip = preview.skip.min(skip);
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 { } else {
self.active_mut().preview.arrow(skip as isize, MIME_DIR, Some(skip)); preview.skip = skip;
self.active_mut().preview.go_folder(url, in_chunks); }
}
self.active_mut().preview.go(hovered, mime);
false false
} }
} }
}

View file

@ -3,10 +3,11 @@ use std::{collections::BTreeMap, ffi::OsStr, io::{stdout, BufWriter, Write}, pat
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}}; use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}};
use yazi_config::{popup::InputCfg, OPEN, PREVIEW}; 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 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 { pub struct Opt {
force: bool, force: bool,
@ -87,10 +88,10 @@ impl Manager {
let _guard = BLOCKER.acquire().await.unwrap(); let _guard = BLOCKER.acquire().await.unwrap();
let _defer = Defer::new(|| { let _defer = Defer::new(|| {
Ctx::resume(); Scheduler::app_resume();
tokio::spawn(fs::remove_file(tmp.clone())) tokio::spawn(fs::remove_file(tmp.clone()))
}); });
Ctx::stop().await; Scheduler::app_stop().await;
let mut child = external::shell(ShellOpt { let mut child = external::shell(ShellOpt {
cmd: (*opener.exec).into(), cmd: (*opener.exec).into(),

View file

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

View file

@ -1,6 +1,7 @@
use yazi_scheduler::Scheduler;
use yazi_shared::event::Exec; use yazi_shared::event::Exec;
use crate::{manager::Manager, Ctx}; use crate::manager::Manager;
pub struct Opt; pub struct Opt;
impl From<&Exec> for Opt { impl From<&Exec> for Opt {
@ -11,7 +12,7 @@ impl Manager {
pub fn suspend(&mut self, _: impl Into<Opt>) -> bool { pub fn suspend(&mut self, _: impl Into<Opt>) -> bool {
#[cfg(unix)] #[cfg(unix)]
tokio::spawn(async move { tokio::spawn(async move {
Ctx::stop().await; Scheduler::app_stop().await;
unsafe { libc::raise(libc::SIGTSTP) }; unsafe { libc::raise(libc::SIGTSTP) };
}); });
false false

View file

@ -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<Self, Self::Error> { Ok(Self { data: e.take_data().ok_or(())? }) }
}
impl Manager {
pub fn update_mimetype(&mut self, opt: impl TryInto<Opt>, 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
}
}

View file

@ -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 super::{Tabs, Watcher};
use crate::{tab::{Folder, Tab}, tasks::Tasks}; use crate::{folder::Folder, tab::Tab};
pub struct Manager { pub struct Manager {
pub tabs: Tabs, pub tabs: Tabs,
@ -65,20 +65,6 @@ impl Manager {
false false
} }
} }
pub fn update_mimetype(&mut self, mut mimes: BTreeMap<Url, String>, 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 { impl Manager {

View file

@ -31,7 +31,8 @@ impl Tabs {
#[inline] #[inline]
pub(super) fn set_idx(&mut self, idx: usize) { pub(super) fn set_idx(&mut self, idx: usize) {
self.idx = idx; self.idx = idx;
self.active_mut().preview.reset_image(); // TODO: plugin system
// self.active_mut().preview.reset_image();
Manager::_refresh(); Manager::_refresh();
} }
} }

View file

@ -5,10 +5,9 @@ use notify::{event::{MetadataKind, ModifyKind}, EventKind, RecommendedWatcher, R
use parking_lot::RwLock; use parking_lot::RwLock;
use tokio::{fs, pin, sync::mpsc::{self, UnboundedReceiver}}; use tokio::{fs, pin, sync::mpsc::{self, UnboundedReceiver}};
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
use yazi_scheduler::external;
use yazi_shared::{emit, fs::{File, FilesOp, Url}}; use yazi_shared::{emit, fs::{File, FilesOp, Url}};
use crate::files::Files; use crate::folder::Files;
pub struct Watcher { pub struct Watcher {
watcher: RecommendedWatcher, watcher: RecommendedWatcher,
@ -164,23 +163,24 @@ impl Watcher {
} }
async fn files_changed(urls: &[Url], watched: &IndexMap<Url, Option<Url>>) { async fn files_changed(urls: &[Url], watched: &IndexMap<Url, Option<Url>>) {
let Ok(mut mimes) = external::file(urls).await else { // TODO: plugin system
return; // 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( // let linked: Vec<_> = watched.iter().filter_map(|(k, v)|
Vec::new(), // v.as_ref().map(|v| (k, v))).fold( Vec::new(),
|mut aac, (k, v)| { // |mut aac, (k, v)| {
mimes // mimes
.iter() // .iter()
.filter(|(u, _)| u.parent().map(|p| p == **v) == Some(true)) // .filter(|(u, _)| u.parent().map(|p| p == **v) == Some(true))
.for_each(|(u, m)| aac.push((k.join(u.file_name().unwrap()), m.clone()))); // .for_each(|(u, m)| aac.push((k.join(u.file_name().unwrap()),
aac // m.clone()))); aac
}, // },
); // );
mimes.extend(linked); // mimes.extend(linked);
emit!(Mimetype(mimes)); // emit!(Mimetype(mimes));
} }
async fn dir_changed(url: &Url, watched: &IndexMap<Url, Option<Url>>) { async fn dir_changed(url: &Url, watched: &IndexMap<Url, Option<Url>>) {

View file

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

View file

@ -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<PreviewLock>,
skip: usize,
handle: Option<JoinHandle<()>>,
}
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<usize>) {
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
}
}
}
}

View file

@ -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<PreviewData, PeekError> {
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<PreviewData, PeekError> {
ADAPTOR.image_show(path, MANAGER.layout.image_rect()).await?;
Ok(PreviewData::Image)
}
pub(super) async fn video(path: &Path, skip: usize) -> Result<PreviewData, PeekError> {
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<PreviewData, PeekError> {
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<String, PeekError> {
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<String, PeekError> {
Ok(
external::lsar(path, skip, MANAGER.layout.preview_height())
.await?
.into_iter()
.map(|f| f.name)
.collect::<Vec<_>>()
.join("\n"),
)
}
pub(super) async fn highlight(path: &Path, skip: usize) -> Result<String, PeekError> {
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)))
}
}

View file

@ -36,11 +36,11 @@ impl Step {
impl Step { impl Step {
#[inline] #[inline]
pub fn add<F: FnOnce() -> usize>(self, pos: usize, f: F) -> usize { pub fn add(self, pos: usize, limit: usize) -> usize {
let fixed = match self { let fixed = match self {
Self::Fixed(n) => n, Self::Fixed(n) => n,
Self::Percent(0) => 0, 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()) } if fixed > 0 { pos + fixed as usize } else { pos.saturating_sub(fixed.unsigned_abs()) }
} }

View file

@ -35,7 +35,7 @@ impl Tab {
pub fn cd(&mut self, opt: impl Into<Opt>) -> bool { pub fn cd(&mut self, opt: impl Into<Opt>) -> bool {
let opt = opt.into() as Opt; let opt = opt.into() as Opt;
if opt.interactive { if opt.interactive {
return self.cd_interactive(opt); return self.cd_interactive();
} }
if self.current.cwd == opt.target { if self.current.cwd == opt.target {
@ -68,11 +68,9 @@ impl Tab {
true true
} }
fn cd_interactive(&mut self, opt: impl Into<Opt>) -> bool { fn cd_interactive(&mut self) -> bool {
let opt = opt.into() as Opt;
tokio::spawn(async move { 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)); let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50));
pin!(rx); pin!(rx);

View file

@ -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 yazi_shared::{event::Exec, fs::ends_with_slash, Defer};
use crate::{tab::Tab, Ctx}; use crate::tab::Tab;
pub struct Opt { pub struct Opt {
type_: OptType, type_: OptType,
@ -36,8 +37,8 @@ impl Tab {
let cwd = self.current.cwd.clone(); let cwd = self.current.cwd.clone();
tokio::spawn(async move { tokio::spawn(async move {
let _guard = BLOCKER.acquire().await.unwrap(); let _guard = BLOCKER.acquire().await.unwrap();
let _defer = Defer::new(Ctx::resume); let _defer = Defer::new(Scheduler::app_resume);
Ctx::stop().await; Scheduler::app_stop().await;
let result = if opt.type_ == OptType::Fzf { let result = if opt.type_ == OptType::Fzf {
external::fzf(FzfOpt { cwd }).await external::fzf(FzfOpt { cwd }).await

View file

@ -9,6 +9,7 @@ mod hidden;
mod jump; mod jump;
mod leave; mod leave;
mod linemode; mod linemode;
mod preview;
mod reveal; mod reveal;
mod search; mod search;
mod select; mod select;

View file

@ -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<Self, Self::Error> {
Ok(Self { lock: e.take_data().ok_or_else(|| anyhow!("invalid data"))? })
}
}
impl Tab {
pub fn preview(&mut self, opt: impl TryInto<Opt>) -> 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
}
}

View file

@ -4,7 +4,7 @@ use anyhow::bail;
use tokio::pin; use tokio::pin;
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
use yazi_config::popup::InputCfg; use yazi_config::popup::InputCfg;
use yazi_scheduler::external; use yazi_plugin::external;
use yazi_shared::{emit, event::Exec, fs::FilesOp}; use yazi_shared::{emit, event::Exec, fs::FilesOp};
use crate::{input::Input, manager::Manager, tab::Tab}; use crate::{input::Input, manager::Manager, tab::Tab};
@ -78,7 +78,8 @@ impl Tab {
handle.abort(); handle.abort();
} }
if self.current.cwd.is_search() { 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()); let rep = self.history_new(&self.current.cwd.to_regular());
drop(mem::replace(&mut self.current, rep)); drop(mem::replace(&mut self.current, rep));

View file

@ -1,6 +1,6 @@
use yazi_config::{manager::SortBy, MANAGER}; use yazi_config::{manager::SortBy, MANAGER};
use crate::files::FilesSorter; use crate::folder::FilesSorter;
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct Config { pub struct Config {

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use regex::bytes::{Regex, RegexBuilder}; use regex::bytes::{Regex, RegexBuilder};
use yazi_shared::fs::Url; use yazi_shared::fs::Url;
use crate::files::Files; use crate::folder::Files;
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
pub enum FinderCase { pub enum FinderCase {

View file

@ -2,13 +2,13 @@ mod backstack;
mod commands; mod commands;
mod config; mod config;
mod finder; mod finder;
mod folder;
mod mode; mod mode;
mod preview;
mod tab; mod tab;
pub use backstack::*; pub use backstack::*;
pub use config::*; pub use config::*;
pub use finder::*; pub use finder::*;
pub use folder::*;
pub use mode::*; pub use mode::*;
pub use preview::*;
pub use tab::*; pub use tab::*;

View file

@ -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<PreviewLock>,
pub skip: usize,
previewer_ct: Option<CancellationToken>,
}
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
}
}
}
}

View file

@ -2,10 +2,10 @@ use std::{borrow::Cow, collections::BTreeMap};
use anyhow::Result; use anyhow::Result;
use tokio::task::JoinHandle; 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 super::{Backstack, Config, Finder, Mode, Preview};
use crate::preview::Preview; use crate::folder::Folder;
pub struct Tab { pub struct Tab {
pub mode: Mode, pub mode: Mode,
@ -46,21 +46,6 @@ impl From<&Url> for Tab {
fn from(url: &Url) -> Self { Self::from(url.clone()) } 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 { impl Tab {
// --- Mode // --- Mode
#[inline] #[inline]

View file

@ -2,10 +2,10 @@ use std::io::{stdout, Write};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use tokio::{io::{stdin, AsyncReadExt}, select, sync::mpsc, time}; 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 yazi_shared::{event::Exec, term::Term, Defer};
use crate::{tasks::Tasks, Ctx}; use crate::tasks::Tasks;
pub struct Opt; pub struct Opt;
@ -32,10 +32,10 @@ impl Tasks {
task.logs.clone() task.logs.clone()
}; };
Ctx::stop().await; Scheduler::app_stop().await;
let _defer = Defer::new(|| { let _defer = Defer::new(|| {
disable_raw_mode().ok(); disable_raw_mode().ok();
Ctx::resume(); Scheduler::app_resume();
}); });
Term::clear(&mut stdout()).ok(); Term::clear(&mut stdout()).ok();

View file

@ -1,7 +1,7 @@
use std::ffi::OsString; use std::ffi::OsString;
use anyhow::anyhow; use anyhow::anyhow;
use yazi_config::{open::Opener, BOOT}; use yazi_config::{open::Opener, ARGS};
use yazi_shared::{emit, event::Exec, Layer}; use yazi_shared::{emit, event::Exec, Layer};
use crate::tasks::Tasks; use crate::tasks::Tasks;
@ -29,7 +29,7 @@ impl Tasks {
return false; 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, _)| { let paths = opt.targets.into_iter().fold(OsString::new(), |mut s, (p, _)| {
s.push(p); s.push(p);
s.push("\n"); s.push("\n");

View file

@ -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 tokio::time::sleep;
use tracing::debug; 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_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 super::{TasksProgress, TASKS_PADDING, TASKS_PERCENT};
use crate::{files::Files, input::Input}; use crate::{folder::Files, input::Input};
pub struct Tasks { pub struct Tasks {
pub(super) scheduler: Arc<Scheduler>, pub(super) scheduler: Arc<Scheduler>,
@ -149,76 +149,94 @@ impl Tasks {
false false
} }
#[inline] pub fn plugin_micro(&self, name: &str) -> bool {
pub fn precache_size(&self, targets: &Files) -> 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<Url, String>) {
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<Url, String>) {
{
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 { if targets.sorter().by != SortBy::Size {
return false; return;
} }
let targets: Vec<_> = targets let targets: Vec<_> = {
let loading = self.scheduler.preload.size_loading.read();
targets
.iter() .iter()
.filter(|f| f.is_dir() && !targets.sizes.contains_key(&f.url)) .filter(|f| f.is_dir() && !targets.sizes.contains_key(&f.url) && !loading.contains(&f.url))
.map(|f| &f.url) .map(|f| &f.url)
.collect(); .collect()
};
if !targets.is_empty() { if targets.is_empty() {
self.scheduler.precache_size(targets); return;
} }
false let mut loading = self.scheduler.preload.size_loading.write();
for &target in &targets {
loading.insert(target.clone());
} }
#[inline] self.scheduler.preload_size(targets);
pub fn precache_mime(&self, targets: &[File], mimetype: &HashMap<Url, String>) -> 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);
}
false
}
pub fn precache_image(&self, mimetype: &BTreeMap<Url, String>) -> 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<Url, String>) -> 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<Url, String>) -> 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
} }
} }

View file

@ -13,6 +13,7 @@ yazi-adaptor = { path = "../yazi-adaptor", version = "0.1.5" }
yazi-config = { path = "../yazi-config", version = "0.1.5" } yazi-config = { path = "../yazi-config", version = "0.1.5" }
yazi-core = { path = "../yazi-core", version = "0.1.5" } yazi-core = { path = "../yazi-core", version = "0.1.5" }
yazi-plugin = { path = "../yazi-plugin", 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" } yazi-shared = { path = "../yazi-shared", version = "0.1.5" }
# External dependencies # External dependencies
@ -35,6 +36,12 @@ tracing-subscriber = "^0"
libc = "^0" libc = "^0"
signal-hook-tokio = { version = "^0", features = [ "futures-v0_3" ] } 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]] [[bin]]
name = "yazi" name = "yazi"
path = "src/main.rs" path = "src/main.rs"

View file

@ -3,11 +3,11 @@ use std::sync::atomic::Ordering;
use anyhow::{Ok, Result}; use anyhow::{Ok, Result};
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
use ratatui::{backend::Backend, prelude::Rect}; use ratatui::{backend::Backend, prelude::Rect};
use yazi_config::{keymap::Key, BOOT}; use yazi_config::{keymap::Key, ARGS};
use yazi_core::{input::InputMode, preview::COLLISION, Ctx}; use yazi_core::input::InputMode;
use yazi_shared::{emit, event::{Event, Exec}, fs::FilesOp, term::Term, Layer}; 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) struct App {
pub(crate) cx: Ctx, pub(crate) cx: Ctx,
@ -19,11 +19,14 @@ impl App {
pub(crate) async fn run() -> Result<()> { pub(crate) async fn run() -> Result<()> {
Panic::install(); Panic::install();
let _log = Logs::init()?; let _log = Logs::init()?;
let term = Term::start()?;
let term = Term::start()?;
let signals = Signals::start()?; let signals = Signals::start()?;
Lives::register()?;
let mut app = Self { cx: Ctx::make(), term: Some(term), signals }; let mut app = Self { cx: Ctx::make(), term: Some(term), signals };
app.dispatch_render()?;
while let Some(event) = app.signals.recv().await { while let Some(event) = app.signals.recv().await {
match event { match event {
Event::Quit(no_cwd_file) => { Event::Quit(no_cwd_file) => {
@ -42,7 +45,7 @@ impl App {
} }
fn dispatch_quit(&mut self, no_cwd_file: bool) { 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(); let cwd = self.cx.manager.cwd().as_os_str();
std::fs::write(p, cwd.as_encoded_bytes()).ok(); std::fs::write(p, cwd.as_encoded_bytes()).ok();
} }
@ -72,7 +75,7 @@ impl App {
let collision = COLLISION.swap(false, Ordering::Relaxed); let collision = COLLISION.swap(false, Ordering::Relaxed);
let frame = term.draw(|f| { let frame = term.draw(|f| {
yazi_plugin::scope(&self.cx, |_| { Lives::scope(&self.cx, |_| {
f.render_widget(Root::new(&self.cx), f.size()); f.render_widget(Root::new(&self.cx), f.size());
}); });
@ -83,8 +86,9 @@ impl App {
if !COLLISION.load(Ordering::Relaxed) { if !COLLISION.load(Ordering::Relaxed) {
if collision { if collision {
// Reload preview if collision is resolved // Reload preview if collision is resolved
self.cx.manager.active_mut().preview.reset_image(); // TODO: plugin system
self.cx.manager.peek(0); // self.cx.manager.active_mut().preview.reset_image();
self.cx.manager.peek(());
} }
return Ok(()); return Ok(());
} }
@ -115,7 +119,7 @@ impl App {
self.cx.manager.current_mut().set_page(true); self.cx.manager.current_mut().set_page(true);
self.cx.manager.active_mut().preview.reset(); self.cx.manager.active_mut().preview.reset();
self.cx.manager.peek(0); self.cx.manager.peek(());
emit!(Render); emit!(Render);
} }
@ -140,23 +144,12 @@ impl App {
emit!(Render); emit!(Render);
} }
if calc { if calc {
tasks.precache_size(&manager.current().files); tasks.preload_sorted(&manager.current().files);
} }
} }
Event::Pages(page) => { Event::Pages(page) => {
let targets = self.cx.manager.current().paginate(page); let targets = self.cx.manager.current().paginate(page);
tasks.precache_mime(targets, &self.cx.manager.mimetype); tasks.preload_paged(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);
}
} }
_ => unreachable!(), _ => unreachable!(),
} }

View file

@ -1 +1,2 @@
mod plugin;
mod stop; mod stop;

View file

@ -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<yazi_plugin::Opt>) -> 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<yazi_plugin::Opt>) -> 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<Value> = Err("uninitialized plugin".into_lua_err());
Lives::scope(&self.cx, |_| {
let mut plugin: Option<Table> = 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
}
}

View file

@ -24,7 +24,8 @@ impl App {
return false; return false;
}; };
self.cx.manager.active_mut().preview.reset_image(); // TODO: plugin system
// self.cx.manager.active_mut().preview.reset_image();
if opt.state { if opt.state {
self.signals.stop_term(true); self.signals.stop_term(true);
self.term = None; self.term = None;

View file

@ -2,9 +2,8 @@ use std::path::MAIN_SEPARATOR;
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, List, ListItem, Widget}}; use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, List, ListItem, Widget}};
use yazi_config::{popup::{Offset, Position}, THEME}; use yazi_config::{popup::{Offset, Position}, THEME};
use yazi_core::Ctx;
use crate::widgets; use crate::{widgets, Ctx};
pub(crate) struct Completion<'a> { pub(crate) struct Completion<'a> {
cx: &'a Ctx, cx: &'a Ctx,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
use ratatui::prelude::Rect; use ratatui::prelude::Rect;
use yazi_config::popup::{Origin, Position}; use yazi_config::popup::{Origin, Position};
use yazi_core::{completion::Completion, help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which};
use crate::{completion::Completion, help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which};
pub struct Ctx { pub struct Ctx {
pub manager: Manager, 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 { pub fn area(&self, position: &Position) -> Rect {
if position.origin != Origin::Hovered { if position.origin != Origin::Hovered {
return position.rect(); return position.rect();

View file

@ -84,6 +84,8 @@ impl<'a> Executor<'a> {
}; };
} }
on!(plugin);
on!(plugin_do);
on!(stop); on!(stop);
false 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, hover);
on!(MANAGER, peek);
on!(MANAGER, seek);
on!(MANAGER, refresh); on!(MANAGER, refresh);
on!(MANAGER, quit, &self.app.cx.tasks); on!(MANAGER, quit, &self.app.cx.tasks);
on!(MANAGER, close, &self.app.cx.tasks); on!(MANAGER, close, &self.app.cx.tasks);
on!(MANAGER, suspend); on!(MANAGER, suspend);
on!(ACTIVE, escape); on!(ACTIVE, escape);
on!(ACTIVE, preview);
// Navigation // Navigation
on!(ACTIVE, arrow); on!(ACTIVE, arrow);

View file

@ -1,6 +1,7 @@
use ratatui::{layout::{self, Constraint}, prelude::{Buffer, Direction, Rect}, widgets::{List, ListItem, Widget}}; use ratatui::{layout::{self, Constraint}, prelude::{Buffer, Direction, Rect}, widgets::{List, ListItem, Widget}};
use yazi_config::THEME; use yazi_config::THEME;
use yazi_core::Ctx;
use crate::Ctx;
pub(super) struct Bindings<'a> { pub(super) struct Bindings<'a> {
cx: &'a Ctx, cx: &'a Ctx,

View file

@ -1,9 +1,8 @@
use ratatui::{buffer::Buffer, layout::{self, Rect}, prelude::{Constraint, Direction}, widgets::{Paragraph, Widget}}; use ratatui::{buffer::Buffer, layout::{self, Rect}, prelude::{Constraint, Direction}, widgets::{Paragraph, Widget}};
use yazi_config::THEME; use yazi_config::THEME;
use yazi_core::Ctx;
use super::Bindings; use super::Bindings;
use crate::widgets; use crate::{Ctx, widgets};
pub(crate) struct Layout<'a> { pub(crate) struct Layout<'a> {
cx: &'a Ctx, cx: &'a Ctx,

View file

@ -3,10 +3,10 @@ use std::ops::Range;
use ansi_to_tui::IntoText; use ansi_to_tui::IntoText;
use ratatui::{buffer::Buffer, layout::Rect, text::{Line, Text}, widgets::{Block, BorderType, Borders, Paragraph, Widget}}; use ratatui::{buffer::Buffer, layout::Rect, text::{Line, Text}, widgets::{Block, BorderType, Borders, Paragraph, Widget}};
use yazi_config::THEME; use yazi_config::THEME;
use yazi_core::{input::InputMode, Ctx}; use yazi_core::input::InputMode;
use yazi_shared::term::Term; use yazi_shared::term::Term;
use crate::widgets; use crate::{Ctx, widgets};
pub(crate) struct Input<'a> { pub(crate) struct Input<'a> {
cx: &'a Ctx, cx: &'a Ctx,

View file

@ -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::<yazi_core::tab::Mode>(|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::<yazi_core::tab::Config>(|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::<yazi_core::tab::Preview>(|reg| {
reg.add_field_method_get("skip", |_, me| Ok(me.skip));
reg.add_field_function_get("folder", |_, me| me.named_user_value::<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<AnyUserData<'a>> {
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<AnyUserData<'a>> {
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)
}
}

195
yazi-fm/src/lives/folder.rs Normal file
View file

@ -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::<yazi_core::folder::Folder>(|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::<Value>("window"));
reg.add_field_function_get("files", |_, me| me.named_user_value::<AnyUserData>("files"));
reg.add_field_function_get("hovered", |_, me| me.named_user_value::<Value>("hovered"));
})?;
lua.register_userdata_type::<yazi_core::folder::Files>(|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::<yazi_core::folder::Files>()?;
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::<yazi_shared::fs::File>()?;
if !file.is_dir() {
return Ok(Some(file.len));
}
let folder = me.named_user_value::<FolderRef>("folder")?;
Ok(folder.files.sizes.get(&file.url).copied())
});
reg.add_function("mime", |lua, me: AnyUserData| {
let cx = lua.named_registry_value::<CtxRef>("cx")?;
let file = me.borrow::<yazi_shared::fs::File>()?;
Ok(cx.manager.mimetype.get(&file.url).cloned())
});
reg.add_function("prefix", |lua, me: AnyUserData| {
let folder = me.named_user_value::<FolderRef>("folder")?;
if !folder.cwd.is_search() {
return Ok(None);
}
let file = me.borrow::<yazi_shared::fs::File>()?;
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::<CtxRef>("cx")?;
let file = me.borrow::<yazi_shared::fs::File>()?;
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::<FolderRef>("folder")?;
let file = me.borrow::<yazi_shared::fs::File>()?;
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::<CtxRef>("cx")?;
let file = me.borrow::<yazi_shared::fs::File>()?;
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::<CtxRef>("cx")?;
let folder = me.named_user_value::<FolderRef>("folder")?;
let file = me.borrow::<yazi_shared::fs::File>()?;
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::<CtxRef>("cx")?;
let Some(finder) = &cx.manager.active().finder else {
return Ok(None);
};
let file = me.borrow::<yazi_shared::fs::File>()?;
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::<CtxRef>("cx")?;
let Some(finder) = &cx.manager.active().finder else {
return Ok(None);
};
let file = me.borrow::<yazi_shared::fs::File>()?;
let Some(h) = file.name().and_then(|n| finder.highlighted(n)) else {
return Ok(None);
};
Ok(Some(h.into_iter().map(Range::from).collect::<Vec<_>>()))
});
})?;
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<AnyUserData<'a>> {
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::<Vec<_>>(),
)?;
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<AnyUserData<'a>> {
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)
}
}

View file

@ -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}");
}
}
}

16
yazi-fm/src/lives/mod.rs Normal file
View file

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

View file

@ -1,6 +1,4 @@
use mlua::{AnyUserData, MetaMethod, UserDataFields, UserDataMethods, Value}; use mlua::{AnyUserData, Lua, MetaMethod, UserDataFields, UserDataMethods, Value};
use crate::LUA;
pub struct Tabs<'a, 'b> { pub struct Tabs<'a, 'b> {
scope: &'b mlua::Scope<'a, 'a>, scope: &'b mlua::Scope<'a, 'a>,
@ -9,8 +7,8 @@ pub struct Tabs<'a, 'b> {
} }
impl<'a, 'b> Tabs<'a, 'b> { impl<'a, 'b> Tabs<'a, 'b> {
pub(crate) fn init() -> mlua::Result<()> { pub(super) fn register(lua: &Lua) -> mlua::Result<()> {
LUA.register_userdata_type::<yazi_core::manager::Tabs>(|reg| { lua.register_userdata_type::<yazi_core::manager::Tabs>(|reg| {
reg.add_field_method_get("idx", |_, me| Ok(me.idx)); reg.add_field_method_get("idx", |_, me| Ok(me.idx));
reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len())); reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len()));
reg.add_meta_function(MetaMethod::Index, |_, (me, index): (AnyUserData, usize)| { reg.add_meta_function(MetaMethod::Index, |_, (me, index): (AnyUserData, usize)| {
@ -19,17 +17,15 @@ impl<'a, 'b> Tabs<'a, 'b> {
}); });
})?; })?;
LUA.register_userdata_type::<yazi_core::tab::Tab>(|reg| { lua.register_userdata_type::<yazi_core::tab::Tab>(|reg| {
reg.add_method("name", |_, me, ()| { reg.add_method("name", |lua, me, ()| {
Ok( Some(lua.create_string(
me.current me.current.cwd.file_name().map_or_else(
.cwd || me.current.cwd.as_os_str().as_encoded_bytes(),
.file_name() |n| n.as_encoded_bytes(),
.map(|n| n.to_string_lossy()) ),
.or_else(|| Some(me.current.cwd.to_string_lossy())) ))
.unwrap_or_default() .transpose()
.into_owned(),
)
}); });
reg.add_field_function_get("mode", |_, me| me.named_user_value::<AnyUserData>("mode")); reg.add_field_function_get("mode", |_, me| me.named_user_value::<AnyUserData>("mode"));
@ -41,7 +37,9 @@ impl<'a, 'b> Tabs<'a, 'b> {
Ok(()) Ok(())
} }
}
impl<'a, 'b> Tabs<'a, 'b> {
pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, inner: &'a yazi_core::manager::Tabs) -> Self { pub(crate) fn new(scope: &'b mlua::Scope<'a, 'a>, inner: &'a yazi_core::manager::Tabs) -> Self {
Self { scope, inner } 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("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("current", self.folder(&inner.current)?)?;
ud.set_named_user_value("preview", self.preview(inner)?)?;
Ok(ud) Ok(ud)
} }
pub(crate) fn folder(&self, inner: &'a yazi_core::tab::Folder) -> mlua::Result<AnyUserData<'a>> { pub(crate) fn folder(
&self,
inner: &'a yazi_core::folder::Folder,
) -> mlua::Result<AnyUserData<'a>> {
let ud = self.scope.create_any_userdata_ref(inner)?; let ud = self.scope.create_any_userdata_ref(inner)?;
ud.set_named_user_value("files", self.files(&inner.files)?)?; ud.set_named_user_value("files", self.scope.create_any_userdata_ref(&inner.files)?)?;
Ok(ud)
}
fn files(&self, inner: &'a yazi_core::files::Files) -> mlua::Result<AnyUserData<'a>> {
self.scope.create_any_userdata_ref(inner)
}
fn preview(&self, tab: &'a yazi_core::tab::Tab) -> mlua::Result<AnyUserData<'a>> {
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()),
)?;
Ok(ud) Ok(ud)
} }

View file

@ -1,6 +1,4 @@
use mlua::{AnyUserData, LuaSerdeExt, UserDataFields}; use mlua::{AnyUserData, Lua, LuaSerdeExt, UserDataFields};
use crate::LUA;
pub struct Tasks<'a, 'b> { pub struct Tasks<'a, 'b> {
scope: &'b mlua::Scope<'a, 'a>, scope: &'b mlua::Scope<'a, 'a>,
@ -9,8 +7,8 @@ pub struct Tasks<'a, 'b> {
} }
impl<'a, 'b> Tasks<'a, 'b> { impl<'a, 'b> Tasks<'a, 'b> {
pub(crate) fn init() -> mlua::Result<()> { pub(super) fn register(lua: &Lua) -> mlua::Result<()> {
LUA.register_userdata_type::<yazi_core::tasks::Tasks>(|reg| { lua.register_userdata_type::<yazi_core::tasks::Tasks>(|reg| {
reg.add_field_method_get("progress", |lua, me| lua.to_value(&me.progress)) reg.add_field_method_get("progress", |lua, me| lua.to_value(&me.progress))
})?; })?;

View file

@ -2,9 +2,12 @@
mod app; mod app;
mod completion; mod completion;
mod components;
mod context;
mod executor; mod executor;
mod help; mod help;
mod input; mod input;
mod lives;
mod logs; mod logs;
mod panic; mod panic;
mod root; mod root;
@ -14,6 +17,7 @@ mod tasks;
mod which; mod which;
mod widgets; mod widgets;
use context::*;
use executor::*; use executor::*;
use logs::*; use logs::*;
use panic::*; use panic::*;
@ -29,6 +33,8 @@ async fn main() -> anyhow::Result<()> {
yazi_core::init(); yazi_core::init();
yazi_scheduler::init();
yazi_plugin::init(); yazi_plugin::init();
yazi_adaptor::init(); yazi_adaptor::init();

View file

@ -1,9 +1,7 @@
use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, widgets::Widget}; 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 super::{completion, input, select, tasks, which};
use crate::help; use crate::{components, help, Ctx};
pub(super) struct Root<'a> { pub(super) struct Root<'a> {
cx: &'a Ctx, cx: &'a Ctx,
@ -20,9 +18,10 @@ impl<'a> Widget for Root<'a> {
.constraints([Constraint::Length(1), Constraint::Min(0), Constraint::Length(1)]) .constraints([Constraint::Length(1), Constraint::Min(0), Constraint::Length(1)])
.split(area); .split(area);
components::Header::new(self.cx).render(chunks[0], buf); components::Header.render(chunks[0], buf);
components::Manager::new(self.cx).render(chunks[1], buf); components::Manager.render(chunks[1], buf);
components::Status::new(self.cx).render(chunks[2], buf); components::Status.render(chunks[2], buf);
components::Preview::new(self.cx).render(chunks[2], buf);
if self.cx.tasks.visible { if self.cx.tasks.visible {
tasks::Layout::new(self.cx).render(area, buf); tasks::Layout::new(self.cx).render(area, buf);

View file

@ -1,8 +1,7 @@
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, List, ListItem, Widget}}; use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, List, ListItem, Widget}};
use yazi_config::THEME; use yazi_config::THEME;
use yazi_core::Ctx;
use crate::widgets; use crate::{Ctx, widgets};
pub(crate) struct Select<'a> { pub(crate) struct Select<'a> {
cx: &'a Ctx, cx: &'a Ctx,

View file

@ -50,7 +50,7 @@ impl Signals {
#[cfg(unix)] #[cfg(unix)]
fn spawn_system_task(&self) -> Result<JoinHandle<()>> { fn spawn_system_task(&self) -> Result<JoinHandle<()>> {
use libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGTERM}; use libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGTERM};
use yazi_core::Ctx; use yazi_scheduler::Scheduler;
let tx = self.tx.clone(); let tx = self.tx.clone();
let mut signals = signal_hook_tokio::Signals::new([ let mut signals = signal_hook_tokio::Signals::new([
@ -68,7 +68,7 @@ impl Signals {
break; break;
} }
} }
SIGCONT => Ctx::resume(), SIGCONT => Scheduler::app_resume(),
_ => {} _ => {}
} }
} }

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