feat: spotter (#1802)

This commit is contained in:
三咲雅 · Misaki Masa 2024-11-20 16:43:38 +08:00 committed by GitHub
parent af565fbc40
commit 1f32601dc4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
115 changed files with 1739 additions and 587 deletions

45
Cargo.lock generated
View file

@ -266,9 +266,9 @@ dependencies = [
[[package]]
name = "bstr"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c"
checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22"
dependencies = [
"memchr",
"serde",
@ -327,9 +327,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aeb932158bd710538c73702db6945cb68a8fb08c519e6e12706b94263b36db8"
checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47"
dependencies = [
"jobserver",
"libc",
@ -366,9 +366,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.20"
version = "4.5.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
dependencies = [
"clap_builder",
"clap_derive",
@ -376,9 +376,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.20"
version = "4.5.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
dependencies = [
"anstream",
"anstyle",
@ -388,9 +388,9 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.37"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11611dca53440593f38e6b25ec629de50b14cdfa63adc0fb856115a2c6d97595"
checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01"
dependencies = [
"clap",
]
@ -429,9 +429,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7"
[[package]]
name = "clipboard-win"
@ -819,9 +819,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.0.34"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
dependencies = [
"crc32fast",
"miniz_oxide",
@ -1237,12 +1237,6 @@ dependencies = [
"quick-error",
]
[[package]]
name = "imagesize"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
[[package]]
name = "imgref"
version = "1.11.0"
@ -1402,9 +1396,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.162"
version = "0.2.164"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
[[package]]
name = "libfuzzer-sys"
@ -2292,9 +2286,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.132"
version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
dependencies = [
"itoa",
"memchr",
@ -3100,7 +3094,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@ -3363,7 +3357,6 @@ dependencies = [
"crossterm",
"futures",
"image",
"imagesize",
"ratatui",
"scopeguard",
"tokio",

View file

@ -14,20 +14,20 @@ ansi-to-tui = "7.0.0"
anyhow = "1.0.93"
base64 = "0.22.1"
bitflags = "2.6.0"
clap = { version = "4.5.20", features = [ "derive" ] }
clap = { version = "4.5.21", features = [ "derive" ] }
crossterm = { version = "0.28.1", features = [ "event-stream" ] }
dirs = "5.0.1"
futures = "0.3.31"
globset = "0.4.15"
libc = "0.2.162"
libc = "0.2.164"
md-5 = "0.10.6"
mlua = { version = "0.10.1", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serialize" ] }
parking_lot = "0.12.3"
ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info" ] }
regex = "1.11.1"
scopeguard = "1.2.0"
serde = { version = "1.0.214", features = [ "derive" ] }
serde_json = "1.0.132"
serde = { version = "1.0.215", features = [ "derive" ] }
serde_json = "1.0.133"
shell-words = "1.1.0"
tokio = { version = "1.41.1", features = [ "full" ] }
tokio-stream = "0.1.16"

View file

@ -1 +1 @@
{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp"," Überzug"," Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","Sysinfo"],"language":"en","version":"0.2"}
{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp"," Überzug"," Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","Sysinfo"],"language":"en","version":"0.2"}

View file

@ -21,7 +21,6 @@ color_quant = "1.1.0"
crossterm = { workspace = true }
futures = { workspace = true }
image = { version = "0.25.5", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "webp" ] }
imagesize = "0.13.0"
ratatui = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }

44
yazi-adapter/src/info.rs Normal file
View file

@ -0,0 +1,44 @@
use std::path::Path;
use image::{ImageDecoder, ImageError};
pub type ImageFormat = image::ImageFormat;
pub type ImageColor = image::ColorType;
pub type ImageOrientation = image::metadata::Orientation;
#[derive(Clone, Copy)]
pub struct ImageInfo {
pub format: ImageFormat,
pub width: u32,
pub height: u32,
pub color: ImageColor,
pub orientation: Option<ImageOrientation>,
}
impl ImageInfo {
pub async fn new(path: &Path) -> image::ImageResult<Self> {
let path = path.to_owned();
tokio::task::spawn_blocking(move || {
let reader = image::ImageReader::open(path)?.with_guessed_format()?;
let Some(format) = reader.format() else {
return Err(ImageError::IoError(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"unknown image format",
)));
};
let mut decoder = reader.into_decoder()?;
let (width, height) = decoder.dimensions();
Ok(Self {
format,
width,
height,
color: decoder.color_type(),
orientation: decoder.orientation().ok(),
})
})
.await
.map_err(|e| ImageError::IoError(e.into()))?
}
}

View file

@ -1,7 +1,7 @@
#![allow(clippy::unit_arg)]
yazi_macro::mod_flat!(
adapter chafa dimension emulator iip image kgp kgp_old mux sixel ueberzug
adapter chafa dimension emulator iip image info kgp kgp_old mux sixel ueberzug
);
use yazi_shared::{RoCell, SyncCell, env_exists, in_wsl};

View file

@ -1,7 +1,6 @@
use std::{path::{Path, PathBuf}, process::Stdio};
use anyhow::{Result, bail};
use imagesize::ImageSize;
use ratatui::layout::Rect;
use tokio::{io::AsyncWriteExt, process::{Child, Command}, sync::mpsc::{self, UnboundedSender}};
use tracing::{debug, warn};
@ -48,8 +47,7 @@ impl Ueberzug {
};
let p = path.to_owned();
let ImageSize { width: w, height: h } =
tokio::task::spawn_blocking(move || imagesize::size(p)).await??;
let (w, h) = tokio::task::spawn_blocking(move || image::image_dimensions(p)).await??;
let area = Dimension::ratio()
.map(|(r1, r2)| Rect {

View file

@ -21,7 +21,7 @@ serde = { workspace = true }
[build-dependencies]
clap = { workspace = true }
clap_complete = "4.5.37"
clap_complete = "4.5.38"
clap_complete_fig = "4.5.2"
clap_complete_nushell = "4.5.4"
vergen-gitcl = { version = "1.0.1", features = [ "build", "rustc" ] }

View file

@ -27,7 +27,7 @@ toml_edit = "0.22.22"
# External build dependencies
anyhow = { workspace = true }
clap = { workspace = true }
clap_complete = "4.5.37"
clap_complete = "4.5.38"
clap_complete_fig = "4.5.2"
clap_complete_nushell = "4.5.4"
serde_json = { workspace = true }

View file

@ -42,10 +42,6 @@ keymap = [
{ on = "H", run = "back", desc = "Go back to the previous directory" },
{ on = "L", run = "forward", desc = "Go forward to the next directory" },
# Seeking
{ on = "K", run = "seek -5", desc = "Seek up 5 units in the preview" },
{ on = "J", run = "seek 5", desc = "Seek down 5 units in the preview" },
# Toggle
{ on = "<Space>", run = [ "toggle", "arrow 1" ], desc = "Toggle the current selection state" },
{ on = "<C-a>", run = "toggle_all on", desc = "Select all files" },
@ -55,6 +51,13 @@ keymap = [
{ on = "v", run = "visual_mode", desc = "Enter visual mode (selection mode)" },
{ on = "V", run = "visual_mode --unset", desc = "Enter visual mode (unset mode)" },
# Seeking
{ on = "K", run = "seek -5", desc = "Seek up 5 units in the preview" },
{ on = "J", run = "seek 5", desc = "Seek down 5 units in the preview" },
# Spotting
{ on = "<Tab>", run = "spot", desc = "Spot hovered file" },
# Operation
{ on = "o", run = "open", desc = "Open selected files" },
{ on = "O", run = "open --interactive", desc = "Open selected files interactively" },
@ -175,6 +178,32 @@ keymap = [
{ on = "<F1>", run = "help", desc = "Open help" },
]
[spot]
keymap = [
{ on = "<Esc>", run = "close", desc = "Close the spot" },
{ on = "<C-[>", run = "close", desc = "Close the spot" },
{ on = "<C-c>", run = "close", desc = "Close the spot" },
{ on = "<Tab>", run = "close", desc = "Close the spot" },
{ on = "k", run = "arrow -1", desc = "Move cursor up" },
{ on = "j", run = "arrow 1", desc = "Move cursor down" },
{ on = "h", run = "swipe -1", desc = "Swipe to the next file" },
{ on = "l", run = "swipe 1", desc = "Swipe to the previous file" },
{ on = "<Up>", run = "arrow -1", desc = "Move cursor up" },
{ on = "<Down>", run = "arrow 1", desc = "Move cursor down" },
{ on = "<Left>", run = "swipe -1", desc = "Swipe to the next file" },
{ on = "<Right>", run = "swipe 1", desc = "Swipe to the previous file" },
# Copy
{ on = [ "c", "c" ], run = "copy cell", desc = "Copy selected cell" },
# Help
{ on = "~", run = "help", desc = "Open help" },
{ on = "<F1>", run = "help", desc = "Open help" },
]
[pick]
keymap = [

View file

@ -92,6 +92,19 @@ fetchers = [
# Mimetype
{ id = "mime", name = "*", run = "mime", if = "!mime", prio = "high" },
]
spotters = [
{ name = "*/", run = "folder" },
# Code
{ mime = "text/*", run = "code" },
{ mime = "*/{xml,javascript,x-wine-extension-ini}", run = "code" },
# Image
{ mime = "image/{avif,hei?,jxl,svg+xml}", run = "magick" },
{ mime = "image/*", run = "image" },
# Video
{ mime = "video/*", run = "video" },
# Fallback
{ name = "*", run = "file" },
]
preloaders = [
# Image
{ mime = "image/{avif,hei?,jxl,svg+xml}", run = "magick" },

View file

@ -12,6 +12,7 @@ use crate::Preset;
pub struct Keymap {
pub manager: Vec<Chord>,
pub tasks: Vec<Chord>,
pub spot: Vec<Chord>,
pub pick: Vec<Chord>,
pub input: Vec<Chord>,
pub confirm: Vec<Chord>,
@ -26,6 +27,7 @@ impl Keymap {
Layer::App => unreachable!(),
Layer::Manager => &self.manager,
Layer::Tasks => &self.tasks,
Layer::Spot => &self.spot,
Layer::Pick => &self.pick,
Layer::Input => &self.input,
Layer::Confirm => &self.confirm,
@ -53,6 +55,7 @@ impl<'de> Deserialize<'de> for Keymap {
struct Shadow {
manager: Inner,
tasks: Inner,
spot: Inner,
pick: Inner,
input: Inner,
confirm: Inner,
@ -90,7 +93,9 @@ impl<'de> Deserialize<'de> for Keymap {
#[rustfmt::skip]
tasks: mix(shadow.tasks.prepend_keymap, shadow.tasks.keymap, shadow.tasks.append_keymap),
#[rustfmt::skip]
pick: mix(shadow.pick.prepend_keymap, shadow.pick.keymap, shadow.pick.append_keymap),
spot: mix(shadow.spot.prepend_keymap, shadow.spot.keymap, shadow.spot.append_keymap),
#[rustfmt::skip]
pick: mix(shadow.pick.prepend_keymap, shadow.pick.keymap, shadow.pick.append_keymap),
#[rustfmt::skip]
input: mix(shadow.input.prepend_keymap, shadow.input.keymap, shadow.input.append_keymap),
#[rustfmt::skip]

View file

@ -32,12 +32,12 @@ impl Fetcher {
#[derive(Debug, Clone)]
pub struct FetcherProps {
pub id: u8,
pub name: String,
pub name: &'static str,
pub prio: Priority,
}
impl From<&Fetcher> for FetcherProps {
fn from(fetcher: &Fetcher) -> Self {
Self { id: fetcher.idx, name: fetcher.run.name.to_owned(), prio: fetcher.prio }
impl From<&'static Fetcher> for FetcherProps {
fn from(fetcher: &'static Fetcher) -> Self {
Self { id: fetcher.idx, name: &fetcher.run.name, prio: fetcher.prio }
}
}

View file

@ -1,3 +1,3 @@
yazi_macro::mod_flat!(fetcher plugin preloader previewer);
yazi_macro::mod_flat!(fetcher plugin preloader previewer spotter);
pub const MAX_PREWORKERS: u8 = 32;

View file

@ -3,22 +3,23 @@ use std::{collections::HashSet, path::Path, str::FromStr};
use anyhow::Context;
use serde::{Deserialize, Deserializer};
use super::{Fetcher, Preloader, Previewer};
use super::{Fetcher, Preloader, Previewer, Spotter};
use crate::{Preset, plugin::MAX_PREWORKERS};
pub struct Plugin {
pub fetchers: Vec<Fetcher>,
pub spotters: Vec<Spotter>,
pub preloaders: Vec<Preloader>,
pub previewers: Vec<Previewer>,
}
impl Plugin {
pub fn fetchers<'a>(
&'a self,
pub fn fetchers<'a, 'b: 'a>(
&'b self,
path: &'a Path,
mime: &'a str,
factor: impl Fn(&str) -> bool + Copy,
) -> impl Iterator<Item = &'a Fetcher> {
factor: impl Fn(&str) -> bool + Copy + 'a,
) -> impl Iterator<Item = &'b Fetcher> + 'a {
let mut seen = HashSet::new();
self.fetchers.iter().filter(move |&f| {
if seen.contains(&f.id) || !f.matches(path, mime, factor) {
@ -34,11 +35,15 @@ impl Plugin {
self.fetchers.iter().fold(0, |n, f| if f.mime.is_some() { n } else { n | 1 << f.idx as u32 })
}
pub fn preloaders<'a>(
&'a self,
pub fn spotter(&self, path: &Path, mime: &str) -> Option<&Spotter> {
self.spotters.iter().find(|&p| p.matches(path, mime))
}
pub fn preloaders<'a, 'b: 'a>(
&'b self,
path: &'a Path,
mime: &'a str,
) -> impl Iterator<Item = &'a Preloader> {
) -> impl Iterator<Item = &'b Preloader> + 'a {
let mut next = true;
self.preloaders.iter().filter(move |&p| {
if !next || !p.matches(path, mime) {
@ -80,6 +85,12 @@ impl<'de> Deserialize<'de> for Plugin {
#[serde(default)]
append_fetchers: Vec<Fetcher>,
spotters: Vec<Spotter>,
#[serde(default)]
prepend_spotters: Vec<Spotter>,
#[serde(default)]
append_spotters: Vec<Spotter>,
preloaders: Vec<Preloader>,
#[serde(default)]
prepend_preloaders: Vec<Preloader>,
@ -94,6 +105,12 @@ impl<'de> Deserialize<'de> for Plugin {
}
let mut shadow = Outer::deserialize(deserializer)?.plugin;
if shadow.append_spotters.iter().any(|r| r.any_file()) {
shadow.spotters.retain(|r| !r.any_file());
}
if shadow.append_spotters.iter().any(|r| r.any_dir()) {
shadow.spotters.retain(|r| !r.any_dir());
}
if shadow.append_previewers.iter().any(|r| r.any_file()) {
shadow.previewers.retain(|r| !r.any_file());
}
@ -103,6 +120,8 @@ impl<'de> Deserialize<'de> for Plugin {
shadow.fetchers =
Preset::mix(shadow.prepend_fetchers, shadow.fetchers, shadow.append_fetchers).collect();
shadow.spotters =
Preset::mix(shadow.prepend_spotters, shadow.spotters, shadow.append_spotters).collect();
shadow.preloaders =
Preset::mix(shadow.prepend_preloaders, shadow.preloaders, shadow.append_preloaders).collect();
shadow.previewers =
@ -121,6 +140,7 @@ impl<'de> Deserialize<'de> for Plugin {
Ok(Self {
fetchers: shadow.fetchers,
spotters: shadow.spotters,
preloaders: shadow.preloaders,
previewers: shadow.previewers,
})

View file

@ -30,12 +30,12 @@ impl Preloader {
#[derive(Debug, Clone)]
pub struct PreloaderProps {
pub id: u8,
pub name: String,
pub name: &'static str,
pub prio: Priority,
}
impl From<&Preloader> for PreloaderProps {
fn from(preloader: &Preloader) -> Self {
Self { id: preloader.idx, name: preloader.run.name.to_owned(), prio: preloader.prio }
impl From<&'static Preloader> for PreloaderProps {
fn from(preloader: &'static Preloader) -> Self {
Self { id: preloader.idx, name: &preloader.run.name, prio: preloader.prio }
}
}

View file

@ -0,0 +1,27 @@
use std::path::Path;
use serde::Deserialize;
use yazi_shared::{MIME_DIR, event::Cmd};
use crate::Pattern;
#[derive(Debug, Deserialize)]
pub struct Spotter {
pub name: Option<Pattern>,
pub mime: Option<Pattern>,
pub run: Cmd,
}
impl Spotter {
#[inline]
pub fn matches(&self, path: &Path, mime: &str) -> bool {
self.mime.as_ref().is_some_and(|p| p.match_mime(mime))
|| self.name.as_ref().is_some_and(|p| p.match_path(path, mime == MIME_DIR))
}
#[inline]
pub fn any_file(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_file()) }
#[inline]
pub fn any_dir(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_dir()) }
}

View file

@ -6,7 +6,7 @@
clippy::unit_arg
)]
yazi_macro::mod_pub!(completion confirm help input manager notify pick tab tasks which);
yazi_macro::mod_pub!(completion confirm help input manager notify pick spot tab tasks which);
pub fn init() {
manager::WATCHED.with(<_>::default);

View file

@ -13,6 +13,7 @@ yazi_macro::mod_flat!(
remove
rename
seek
spot
suspend
tab_close
tab_create

View file

@ -0,0 +1,40 @@
use std::borrow::Cow;
use yazi_shared::{MIME_DIR, event::{Cmd, Data}};
use crate::manager::Manager;
struct Opt {
skip: Option<usize>,
}
impl From<Cmd> for Opt {
fn from(c: Cmd) -> Self { Self { skip: c.get("skip").and_then(Data::as_usize) } }
}
impl Manager {
#[yazi_codegen::command]
pub fn spot(&mut self, opt: Opt) {
let Some(hovered) = self.hovered().cloned() else {
return;
};
let mime = if hovered.is_dir() {
Cow::Borrowed(MIME_DIR)
} else {
Cow::Owned(self.mimetype.get_owned(&hovered.url).unwrap_or_default())
};
if !self.active().spot.same_file(&hovered, &mime) {
// self.active_mut().spot.reset();
}
if let Some(skip) = opt.skip {
self.active_mut().spot.skip = skip;
} else if !self.active().spot.same_url(&hovered.url) {
self.active_mut().spot.skip = 0;
}
self.active_mut().spot.go(hovered, mime);
}
}

View file

@ -40,11 +40,11 @@ impl Tabs {
if !opt.current {
tab.cd(opt.url);
} else if let Some(h) = self.active().hovered() {
tab.conf = self.active().conf.clone();
tab.pref = self.active().pref.clone();
tab.apply_files_attrs();
tab.reveal(h.url.to_regular());
} else {
tab.conf = self.active().conf.clone();
tab.pref = self.active().pref.clone();
tab.apply_files_attrs();
tab.cd(self.active().cwd().to_regular());
}

View file

@ -0,0 +1,31 @@
use yazi_macro::render;
use yazi_proxy::ManagerProxy;
use yazi_shared::event::{Cmd, Data};
use crate::spot::Spot;
struct Opt {
step: isize,
}
impl From<Cmd> for Opt {
fn from(c: Cmd) -> Self { Self { step: c.first().and_then(Data::as_isize).unwrap_or(0) } }
}
impl Spot {
#[yazi_codegen::command]
pub fn arrow(&mut self, opt: Opt) {
let Some(lock) = &mut self.lock else { return };
let Some(old) = lock.selected() else {
let new = self.skip.saturating_add_signed(opt.step);
return ManagerProxy::spot(Some(new));
};
lock.select(Some(old.saturating_add_signed(opt.step)));
let new = lock.selected().unwrap();
self.skip = new;
render!(new != old);
}
}

View file

@ -0,0 +1,21 @@
use yazi_macro::render;
use yazi_shared::event::Cmd;
use crate::spot::Spot;
struct Opt;
impl From<Cmd> for Opt {
fn from(_: Cmd) -> Self { Self }
}
impl From<()> for Opt {
fn from(_: ()) -> Self { Self }
}
impl Spot {
#[yazi_codegen::command]
pub fn close(&mut self, _: Opt) {
self.ct.take().map(|h| h.cancel());
render!(self.lock.take().is_some());
}
}

View file

@ -0,0 +1,34 @@
use yazi_plugin::CLIPBOARD;
use yazi_shared::event::Cmd;
use crate::spot::Spot;
struct Opt {
type_: String,
}
impl From<Cmd> for Opt {
fn from(mut c: Cmd) -> Self { Self { type_: c.take_first_str().unwrap_or_default() } }
}
impl Spot {
#[yazi_codegen::command]
pub fn copy(&mut self, opt: Opt) {
let Some(lock) = &self.lock else { return };
let Some(table) = lock.table() else { return };
let mut s = String::new();
match opt.type_.as_str() {
"cell" => {
let Some(cell) = table.selected_cell() else { return };
s = cell.to_string();
}
"line" => {
// TODO
}
_ => {}
}
futures::executor::block_on(CLIPBOARD.set(s));
}
}

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(arrow close copy swipe);

View file

@ -0,0 +1,20 @@
use yazi_proxy::{ManagerProxy, TabProxy};
use yazi_shared::event::{Cmd, Data};
use crate::spot::Spot;
struct Opt {
step: isize,
}
impl From<Cmd> for Opt {
fn from(c: Cmd) -> Self { Self { step: c.first().and_then(Data::as_isize).unwrap_or(0) } }
}
impl Spot {
#[yazi_codegen::command]
pub fn swipe(&mut self, opt: Opt) {
TabProxy::arrow(opt.step);
ManagerProxy::spot(None);
}
}

View file

@ -0,0 +1,3 @@
yazi_macro::mod_pub!(commands);
yazi_macro::mod_flat!(spot);

View file

@ -0,0 +1,54 @@
use std::borrow::Cow;
use tokio_util::sync::CancellationToken;
use yazi_config::PLUGIN;
use yazi_macro::render;
use yazi_plugin::{isolate, utils::SpotLock};
use yazi_shared::fs::{File, Url};
#[derive(Default)]
pub struct Spot {
pub lock: Option<SpotLock>,
pub skip: usize,
pub(super) ct: Option<CancellationToken>,
}
impl Spot {
pub fn go(&mut self, file: File, mime: Cow<'static, str>) {
if mime.is_empty() {
return; // Wait till mimetype is resolved to avoid flickering
} else if self.same_lock(&file, &mime) {
return;
}
let Some(spotter) = PLUGIN.spotter(&file.url, &mime) else {
return self.reset();
};
self.ct = Some(isolate::spot(&spotter.run, file, mime, self.skip));
}
#[inline]
pub fn visible(&self) -> bool { self.lock.is_some() }
#[inline]
pub fn reset(&mut self) {
self.ct.take().map(|ct| ct.cancel());
render!(self.lock.take().is_some());
}
#[inline]
pub fn same_url(&self, url: &Url) -> bool { self.lock.as_ref().is_some_and(|l| *url == l.url) }
#[inline]
pub fn same_file(&self, file: &File, mime: &str) -> bool {
self.same_url(&file.url)
&& self.lock.as_ref().is_some_and(|l| file.cha.hits(l.cha) && mime == l.mime)
}
#[inline]
pub fn same_lock(&self, file: &File, mime: &str) -> bool {
self.same_file(file, mime) && self.lock.as_ref().is_some_and(|l| self.skip == l.skip)
}
}

View file

@ -5,10 +5,10 @@ use crate::tab::Tab;
impl Tab {
pub fn hidden(&mut self, mut c: Cmd) {
self.conf.show_hidden = match c.take_first_str().as_deref() {
self.pref.show_hidden = match c.take_first_str().as_deref() {
Some("show") => true,
Some("hide") => false,
_ => !self.conf.show_hidden,
_ => !self.pref.show_hidden,
};
let hovered = self.hovered().map(|f| f.url_owned());

View file

@ -5,7 +5,7 @@ use crate::tab::Tab;
impl Tab {
pub fn linemode(&mut self, mut c: Cmd) {
render!(self.conf.patch(|new| {
render!(self.pref.patch(|new| {
let Some(mode) = c.take_first_str() else {
return;
};

View file

@ -20,7 +20,6 @@ yazi_macro::mod_flat!(
select_all
shell
sort
spot
toggle
toggle_all
update_peeked

View file

@ -45,7 +45,7 @@ impl Tab {
}
let cwd = self.cwd().to_search(&opt.subject);
let hidden = self.conf.show_hidden;
let hidden = self.pref.show_hidden;
self.search = Some(tokio::spawn(async move {
let rx = if opt.via == SearchOptVia::Rg {

View file

@ -8,15 +8,15 @@ use crate::{tab::Tab, tasks::Tasks};
impl Tab {
pub fn sort(&mut self, mut c: Cmd, tasks: &Tasks) {
let conf = &mut self.conf;
let pref = &mut self.pref;
if let Some(by) = c.take_first_str() {
conf.sort_by = SortBy::from_str(&by).unwrap_or_default();
pref.sort_by = SortBy::from_str(&by).unwrap_or_default();
}
conf.sort_reverse = c.maybe_bool("reverse").unwrap_or(conf.sort_reverse);
conf.sort_dir_first = c.maybe_bool("dir-first").unwrap_or(conf.sort_dir_first);
conf.sort_sensitive = c.maybe_bool("sensitive").unwrap_or(conf.sort_sensitive);
conf.sort_translit = c.maybe_bool("translit").unwrap_or(conf.sort_translit);
pref.sort_reverse = c.maybe_bool("reverse").unwrap_or(pref.sort_reverse);
pref.sort_dir_first = c.maybe_bool("dir-first").unwrap_or(pref.sort_dir_first);
pref.sort_sensitive = c.maybe_bool("sensitive").unwrap_or(pref.sort_sensitive);
pref.sort_translit = c.maybe_bool("translit").unwrap_or(pref.sort_translit);
self.apply_files_attrs();
ManagerProxy::update_paged();

View file

@ -1,20 +0,0 @@
use yazi_shared::event::{Cmd, Data};
use crate::tab::Tab;
struct Opt {
skip: Option<usize>,
}
impl From<Cmd> for Opt {
fn from(c: Cmd) -> Self { Self { skip: c.first().and_then(Data::as_usize) } }
}
impl Tab {
#[yazi_codegen::command]
pub fn spot(&mut self, c: Cmd) {
let Some(hovered) = self.hovered().cloned() else {
return self.preview.reset();
};
}
}

View file

@ -1,10 +1,11 @@
use yazi_plugin::utils::PreviewLock;
use yazi_macro::render;
use yazi_plugin::utils::SpotLock;
use yazi_shared::event::Cmd;
use crate::tab::Tab;
pub struct Opt {
lock: PreviewLock,
lock: SpotLock,
}
impl TryFrom<Cmd> for Opt {
@ -17,6 +18,20 @@ impl TryFrom<Cmd> for Opt {
impl Tab {
pub fn update_spotted(&mut self, opt: impl TryInto<Opt>) {
todo!();
let Some(hovered) = self.hovered().map(|h| &h.url) else {
return self.preview.reset();
};
let Ok(opt) = opt.try_into() else {
return;
};
if opt.lock.url != *hovered {
return;
}
self.spot.skip = opt.lock.selected().unwrap_or_default();
self.spot.lock = Some(opt.lock);
render!();
}
}

View file

@ -1,3 +1,3 @@
yazi_macro::mod_pub!(commands);
yazi_macro::mod_flat!(backstack config finder history mode preview selected tab);
yazi_macro::mod_flat!(backstack preference finder history mode preview selected tab);

View file

@ -2,7 +2,7 @@ use yazi_config::{MANAGER, manager::SortBy};
use yazi_fs::FilesSorter;
#[derive(Clone, PartialEq)]
pub struct Config {
pub struct Preference {
// Sorting
pub sort_by: SortBy,
pub sort_sensitive: bool,
@ -15,7 +15,7 @@ pub struct Config {
pub show_hidden: bool,
}
impl Default for Config {
impl Default for Preference {
fn default() -> Self {
Self {
// Sorting
@ -32,7 +32,7 @@ impl Default for Config {
}
}
impl Config {
impl Preference {
pub(super) fn patch<F: FnOnce(&mut Self)>(&mut self, f: F) -> bool {
let old = self.clone();
f(self);

View file

@ -9,13 +9,13 @@ use yazi_fs::{Folder, FolderStage};
use yazi_macro::render;
use yazi_shared::{Id, Ids, fs::{File, Url}};
use super::{Backstack, Config, Finder, History, Mode, Preview};
use crate::tab::Selected;
use super::{Backstack, Finder, History, Mode, Preference, Preview};
use crate::{spot::Spot, tab::Selected};
pub struct Tab {
pub id: Id,
pub mode: Mode,
pub conf: Config,
pub pref: Preference,
pub current: Folder,
pub parent: Option<Folder>,
@ -23,6 +23,7 @@ pub struct Tab {
pub history: History,
pub selected: Selected,
pub spot: Spot,
pub preview: Preview,
pub finder: Option<Finder>,
pub search: Option<JoinHandle<Result<()>>>,
@ -35,7 +36,7 @@ impl Default for Tab {
Self {
id: IDS.next(),
mode: Default::default(),
conf: Default::default(),
pref: Default::default(),
current: Default::default(),
parent: Default::default(),
@ -43,6 +44,7 @@ impl Default for Tab {
history: Default::default(),
selected: Default::default(),
spot: Default::default(),
preview: Default::default(),
finder: Default::default(),
search: Default::default(),
@ -123,8 +125,8 @@ impl Tab {
}
let hovered = f.hovered().filter(|_| f.tracing).map(|h| h.urn_owned());
f.files.set_show_hidden(self.conf.show_hidden);
f.files.set_sorter(self.conf.sorter());
f.files.set_show_hidden(self.pref.show_hidden);
f.files.set_sorter(self.pref.sorter());
render!(f.files.catchup_revision());
render!(f.repos(hovered.as_ref().map(|u| u.as_urn())));

View file

@ -49,7 +49,7 @@ impl App {
let frame = term
.draw_partial(|f| {
_ = Lives::scope(&self.cx, || {
f.render_widget(crate::tasks::Progress, f.area());
f.render_widget(crate::tasks::Progress::new(&self.cx), f.area());
f.render_widget(crate::notify::Notify::new(&self.cx), f.area());
Ok(())
});

View file

@ -15,10 +15,10 @@ impl From<()> for Opt {
impl App {
#[yazi_codegen::command]
pub fn resize(&mut self, _: Opt) {
self.cx.manager.active_mut().preview.reset();
self.cx.active_mut().preview.reset();
self.reflow(());
self.cx.manager.current_mut().sync_page(true);
self.cx.current_mut().sync_page(true);
self.cx.manager.hover(None);
}
}

View file

@ -4,7 +4,7 @@ use crate::{Term, app::App};
impl App {
pub(crate) fn resume(&mut self, _: Cmd) {
self.cx.manager.active_mut().preview.reset_image();
self.cx.active_mut().preview.reset_image();
self.term = Some(Term::start().unwrap());
// While the app resumes, it's possible that the terminal size has changed.

View file

@ -14,7 +14,7 @@ impl From<Cmd> for Opt {
impl App {
#[yazi_codegen::command]
pub fn stop(&mut self, opt: Opt) {
self.cx.manager.active_mut().preview.reset_image();
self.cx.active_mut().preview.reset_image();
// We need to destroy the `term` first before stopping the `signals`
// to prevent any signal from triggering the term to render again

View file

@ -1,5 +1,6 @@
use ratatui::layout::Rect;
use yazi_core::{completion::Completion, confirm::Confirm, help::Help, input::Input, manager::Manager, notify::Notify, pick::Pick, tasks::Tasks, which::Which};
use yazi_core::{completion::Completion, confirm::Confirm, help::Help, input::Input, manager::Manager, notify::Notify, pick::Pick, tab::Tab, tasks::Tasks, which::Which};
use yazi_fs::Folder;
pub struct Ctx {
pub manager: Manager,
@ -40,3 +41,17 @@ impl Ctx {
None
}
}
impl Ctx {
#[inline]
pub fn active(&self) -> &Tab { self.manager.active() }
#[inline]
pub fn active_mut(&mut self) -> &mut Tab { self.manager.active_mut() }
#[inline]
pub fn current(&self) -> &Folder { self.manager.current() }
#[inline]
pub fn current_mut(&mut self) -> &mut Folder { self.manager.current_mut() }
}

View file

@ -17,6 +17,7 @@ impl<'a> Executor<'a> {
Layer::App => self.app(cmd),
Layer::Manager => self.manager(cmd),
Layer::Tasks => self.tasks(cmd),
Layer::Spot => self.spot(cmd),
Layer::Pick => self.pick(cmd),
Layer::Input => self.input(cmd),
Layer::Confirm => self.confirm(cmd),
@ -79,7 +80,7 @@ impl<'a> Executor<'a> {
on!(MANAGER, hover);
on!(MANAGER, peek);
on!(MANAGER, seek);
on!(ACTIVE, spot);
on!(MANAGER, spot);
on!(MANAGER, refresh, &self.app.cx.tasks);
on!(MANAGER, quit, &self.app.cx.tasks);
on!(MANAGER, close, &self.app.cx.tasks);
@ -182,6 +183,29 @@ impl<'a> Executor<'a> {
}
}
fn spot(&mut self, cmd: Cmd) {
macro_rules! on {
($name:ident) => {
if cmd.name == stringify!($name) {
return self.app.cx.active_mut().spot.$name(cmd);
}
};
}
on!(arrow);
on!(close);
on!(swipe);
on!(copy);
match cmd.name.as_str() {
// Help
"help" => self.app.cx.help.toggle(Layer::Spot),
// Plugin
"plugin" => self.app.plugin(cmd),
_ => {}
}
}
fn pick(&mut self, cmd: Cmd) {
macro_rules! on {
($name:ident) => {

View file

@ -110,7 +110,7 @@ impl UserData for File {
});
methods.add_method("found", |lua, me, ()| {
lua.named_registry_value::<AnyUserData>("cx")?.borrow_scoped(|cx: &Ctx| {
let Some(finder) = &cx.manager.active().finder else {
let Some(finder) = &cx.active().finder else {
return Ok(None);
};
@ -124,7 +124,7 @@ impl UserData for File {
});
methods.add_method("highlights", |lua, me, ()| {
lua.named_registry_value::<AnyUserData>("cx")?.borrow_scoped(|cx: &Ctx| {
let Some(finder) = &cx.manager.active().finder else {
let Some(finder) = &cx.active().finder else {
return None;
};
if me.folder().url != me.tab().current.url {

View file

@ -30,7 +30,7 @@ impl Lives {
LUA.globals().raw_set(
"cx",
LUA.create_table_from([
("active", super::Tab::make(cx.manager.active())?),
("active", super::Tab::make(cx.active())?),
("tabs", super::Tabs::make(&cx.manager.tabs)?),
("tasks", super::Tasks::make(&cx.tasks)?),
("yanked", super::Yanked::make(&cx.manager.yanked)?),

View file

@ -1,6 +1,6 @@
#![allow(clippy::module_inception)]
yazi_macro::mod_flat!(
config file files filter finder folder iter lives mode preview selected tab tabs
preference file files filter finder folder iter lives mode preview selected tab tabs
tasks yanked
);

View file

@ -4,24 +4,24 @@ use mlua::{AnyUserData, UserData, UserDataFields};
use super::Lives;
pub(super) struct Config {
inner: *const yazi_core::tab::Config,
pub(super) struct Preference {
inner: *const yazi_core::tab::Preference,
}
impl Deref for Config {
type Target = yazi_core::tab::Config;
impl Deref for Preference {
type Target = yazi_core::tab::Preference;
fn deref(&self) -> &Self::Target { unsafe { &*self.inner } }
}
impl Config {
impl Preference {
#[inline]
pub(super) fn make(inner: &yazi_core::tab::Config) -> mlua::Result<AnyUserData> {
pub(super) fn make(inner: &yazi_core::tab::Preference) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { inner })
}
}
impl UserData for Config {
impl UserData for Preference {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("sort_by", |_, me| Ok(me.sort_by.to_string()));
fields.add_field_method_get("sort_sensitive", |_, me| Ok(me.sort_sensitive));

View file

@ -3,7 +3,7 @@ use std::ops::Deref;
use mlua::{AnyUserData, UserData, UserDataFields, UserDataMethods};
use yazi_plugin::url::UrlRef;
use super::{Config, Finder, Folder, Lives, Mode, Preview, Selected};
use super::{Finder, Folder, Lives, Mode, Preference, Preview, Selected};
pub(super) struct Tab {
inner: *const yazi_core::tab::Tab,
@ -26,7 +26,29 @@ impl UserData for Tab {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, me| Ok(me.id.get()));
fields.add_field_method_get("mode", |_, me| Mode::make(&me.mode));
fields.add_field_method_get("conf", |_, me| Config::make(&me.conf));
// TODO: remove `conf` once v0.4 is released
fields.add_field_method_get("conf", |lua, me| {
static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
let id = match lua.named_registry_value::<yazi_plugin::RtRef>("rt")?.current() {
Some(id) => format!("`{id}.yazi` plugin"),
None => "`init.lua` config".to_owned(),
};
let s = "The `conf` property of `tab::Tab` has been deprecated in Yazi v0.4.
Please use `pref` instead, e.g. `cx.active.conf` => `cx.active.pref`, in your {id}.";
yazi_proxy::AppProxy::notify(yazi_proxy::options::NotifyOpt {
title: "Deprecated API".to_owned(),
content: s.replace("{id}", &id),
level: yazi_proxy::options::NotifyLevel::Warn,
timeout: std::time::Duration::from_secs(20),
});
}
Preference::make(&me.pref)
});
fields.add_field_method_get("pref", |_, me| Preference::make(&me.pref));
fields.add_field_method_get("current", |_, me| Folder::make(None, &me.current, me));
fields.add_field_method_get("parent", |_, me| {
me.parent.as_ref().map(|f| Folder::make(None, f, me)).transpose()

View file

@ -5,7 +5,7 @@
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
yazi_macro::mod_pub!(app completion confirm help input lives manager notify pick tasks which);
yazi_macro::mod_pub!(app completion confirm help input lives manager notify pick spot tasks which);
yazi_macro::mod_flat!(context executor logs panic root router signals term);

View file

@ -1,4 +1,5 @@
use ratatui::{buffer::Buffer, widgets::Widget};
use yazi_config::LAYOUT;
use crate::Ctx;
@ -12,18 +13,17 @@ impl<'a> Preview<'a> {
}
impl Widget for Preview<'_> {
fn render(self, area: ratatui::layout::Rect, buf: &mut Buffer) {
let preview = &self.cx.manager.active().preview;
let Some(lock) = &preview.lock else {
fn render(self, _: ratatui::layout::Rect, buf: &mut Buffer) {
let Some(lock) = &self.cx.active().preview.lock else {
return;
};
if (lock.window.rows, lock.window.cols) != (area.height, area.width) {
if *lock.area != LAYOUT.get().preview {
return;
}
for w in &lock.data {
w.clone_render(buf);
w.clone().render(buf, |p| self.cx.manager.area(p));
}
}
}

View file

@ -1,9 +1,9 @@
use mlua::{ObjectLike, Table};
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
use tracing::error;
use yazi_plugin::{LUA, elements::render_widgets};
use yazi_plugin::{LUA, elements::render_once};
use super::{completion, confirm, help, input, manager, pick, tasks, which};
use super::{completion, confirm, help, input, manager, pick, spot, tasks, which};
use crate::Ctx;
pub(super) struct Root<'a> {
@ -26,7 +26,7 @@ impl Widget for Root<'_> {
let area = yazi_plugin::elements::Rect::from(area);
let root = LUA.globals().raw_get::<Table>("Root")?.call_method::<Table>("new", area)?;
render_widgets(root.call_method("redraw", ())?, buf);
render_once(root.call_method("redraw", ())?, buf, |p| self.cx.manager.area(p));
Ok::<_, mlua::Error>(())
};
if let Err(e) = f() {
@ -39,6 +39,10 @@ impl Widget for Root<'_> {
tasks::Tasks::new(self.cx).render(area, buf);
}
if self.cx.active().spot.visible() {
spot::Spot::new(self.cx).render(area, buf);
}
if self.cx.pick.visible {
pick::Pick::new(self.cx).render(area, buf);
}

View file

@ -36,6 +36,8 @@ impl<'a> Router<'a> {
self.matches(Layer::Confirm, key)
} else if cx.pick.visible {
self.matches(Layer::Pick, key)
} else if cx.active().spot.visible() {
self.matches(Layer::Spot, key)
} else if cx.tasks.visible {
self.matches(Layer::Tasks, key)
} else {

1
yazi-fm/src/spot/mod.rs Normal file
View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(spot);

23
yazi-fm/src/spot/spot.rs Normal file
View file

@ -0,0 +1,23 @@
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
use crate::Ctx;
pub(crate) struct Spot<'a> {
cx: &'a Ctx,
}
impl<'a> Spot<'a> {
pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } }
}
impl Widget for Spot<'_> {
fn render(self, _: Rect, buf: &mut Buffer) {
let Some(lock) = &self.cx.active().spot.lock else {
return;
};
for w in &lock.data {
w.clone().render(buf, |p| self.cx.manager.area(p));
}
}
}

View file

@ -2,18 +2,26 @@ use mlua::{ObjectLike, Table};
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
use tracing::error;
use yazi_config::LAYOUT;
use yazi_plugin::{LUA, elements::render_widgets};
use yazi_plugin::{LUA, elements::render_once};
pub(crate) struct Progress;
use crate::Ctx;
impl Widget for Progress {
pub(crate) struct Progress<'a> {
cx: &'a Ctx,
}
impl<'a> Progress<'a> {
pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } }
}
impl Widget for Progress<'_> {
fn render(self, _: Rect, buf: &mut Buffer) {
let mut f = || {
let area = yazi_plugin::elements::Rect::from(LAYOUT.get().progress);
let progress =
LUA.globals().raw_get::<Table>("Progress")?.call_method::<Table>("use", area)?;
render_widgets(progress.call_method("redraw", ())?, buf);
render_once(progress.call_method("redraw", ())?, buf, |p| self.cx.manager.area(p));
Ok::<_, mlua::Error>(())
};
if let Err(e) = f() {

View file

@ -11,7 +11,7 @@ function Linemode:new(file) return setmetatable({ _file = file }, { __index = se
function Linemode:space() return ui.Line(" ") end
function Linemode:solo()
local mode = cx.active.conf.linemode
local mode = cx.active.pref.linemode
if mode == "none" or mode == "solo" then
return ui.Line("")
elseif not self[mode] then

View file

@ -47,17 +47,17 @@ end
-- Mouse events
function Root:click(event, up)
local c = ya.child_at(ui.Position { x = event.x, y = event.y }, self._children)
local c = ya.child_at(ui.Rect { x = event.x, y = event.y }, self._children)
return c and c:click(event, up)
end
function Root:scroll(event, step)
local c = ya.child_at(ui.Position { x = event.x, y = event.y }, self._children)
local c = ya.child_at(ui.Rect { x = event.x, y = event.y }, self._children)
return c and c:scroll(event, step)
end
function Root:touch(event, step)
local c = ya.child_at(ui.Position { x = event.x, y = event.y }, self._children)
local c = ya.child_at(ui.Rect { x = event.x, y = event.y }, self._children)
return c and c:touch(event, step)
end

View file

@ -64,7 +64,7 @@ function Status:name()
return ui.Line {}
end
return ui.Line(" " .. h.name)
return ui.Line(" " .. h.name:gsub("\r", "?", 1))
end
function Status:perm()

View file

@ -47,16 +47,16 @@ end
-- Mouse events
function Tab:click(event, up)
local c = ya.child_at(ui.Position { x = event.x, y = event.y }, self._children)
local c = ya.child_at(ui.Rect { x = event.x, y = event.y }, self._children)
return c and c:click(event, up)
end
function Tab:scroll(event, step)
local c = ya.child_at(ui.Position { x = event.x, y = event.y }, self._children)
local c = ya.child_at(ui.Rect { x = event.x, y = event.y }, self._children)
return c and c:scroll(event, step)
end
function Tab:touch(event, step)
local c = ya.child_at(ui.Position { x = event.x, y = event.y }, self._children)
local c = ya.child_at(ui.Rect { x = event.x, y = event.y }, self._children)
return c and c:touch(event, step)
end

View file

@ -26,4 +26,6 @@ function M:seek(units)
})
end
function M:spot(args) require("file"):spot(args) end
return M

View file

@ -16,13 +16,63 @@ end
function M:seek() end
function M:spot(skip)
local rect = ui.Rect { x = 10, y = 10, w = 20, h = 20 }
-- TODO: remove this
local hovered_mime = ya.sync(function()
local h = cx.active.current.hovered
if not h then
return nil
elseif h.cha.is_dir then
return "inode/directory"
else
return h:mime()
end
end)
ya.spot_widgets(self, {
ui.Clear(rect),
ui.Table(rect),
})
function M:spot(args)
local mime = hovered_mime()
if not mime then
return
end
local file = args.file
local spotter = PLUGIN.spotter(file.url, mime)
local previewer = PLUGIN.previewer(file.url, mime)
local fetchers = PLUGIN.fetchers(file.url, mime)
local preloaders = PLUGIN.preloaders(file.url, mime)
for i, v in ipairs(fetchers) do
fetchers[i] = v.cmd
end
for i, v in ipairs(preloaders) do
preloaders[i] = v.cmd
end
local rows = {}
local row = function(key, value)
local h = type(value) == "table" and #value or 1
rows[#rows + 1] = ui.Row({ key, value }):height(h)
end
rows[#rows + 1] = ui.Row({ "Metadata", "" }):style(ui.Style():fg("red"))
row(" Created:", file.cha.btime and os.date("%y/%m/%d %H:%M", math.floor(file.cha.btime)) or "-")
row(" Modified:", file.cha.mtime and os.date("%y/%m/%d %H:%M", math.floor(file.cha.mtime)) or "-")
row(" Mimetype:", mime)
rows[#rows + 1] = ui.Row({ { "", "Plugins" }, "" }):height(2):style(ui.Style():fg("red"))
row(" Spotter:", spotter and spotter.cmd or "-")
row(" Previewer:", previewer and previewer.cmd or "-")
row(" Fetchers:", #fetchers ~= 0 and fetchers or "-")
row(" Preloaders:", #preloaders ~= 0 and preloaders or "-")
ya.spot_table(
args,
ui.Table(rows)
:area(ui.Pos { "center", w = 60, h = 20 })
:row(1)
:col(1)
:col_style(ui.Style():fg("blue"))
:cell_style(ui.Style():fg("yellow"):reverse())
:widths { ui.Constraint.Length(14), ui.Constraint.Fill(1) }
)
end
return M

View file

@ -40,4 +40,6 @@ function M:seek(units)
end
end
function M:spot(args) require("file"):spot(args) end
return M

View file

@ -22,4 +22,30 @@ function M:preload()
return ya.image_precache(self.file.url, cache) and 1 or 2
end
function M:spot(args)
local info = ya.image_info(args.file.url)
local rows = {}
local row = function(key, value)
local h = type(value) == "table" and #value or 1
rows[#rows + 1] = ui.Row({ key, value }):height(h)
end
row("Format:", tostring(info.format))
row("Width:", string.format("%dpx", info.w))
row("Height:", string.format("%dpx", info.h))
row("Color:", tostring(info.color))
ya.spot_table(
args,
ui.Table(rows)
:area(ui.Pos { "center", w = 60, h = 20 })
:row(args.skip)
:col(1)
:col_style(ui.Style():fg("blue"))
:cell_style(ui.Style():fg("yellow"):reverse())
:widths { ui.Constraint.Length(12), ui.Constraint.Fill(1) }
)
end
return M

View file

@ -41,4 +41,6 @@ function M:preload()
return status and status.success and 1 or 2
end
function M:spot(args) require("file"):spot(args) end
return M

View file

@ -9,9 +9,9 @@ local function match_mimetype(s)
end
end
function M:fetch()
function M:fetch(args)
local urls = {}
for _, file in ipairs(self.files) do
for _, file in ipairs(args.files) do
urls[#urls + 1] = tostring(file.url)
end

View file

@ -62,4 +62,6 @@ function M:preload()
return status and status.success and 1 or 2
end
function M:spot(args) require("file"):spot(args) end
return M

View file

@ -47,9 +47,9 @@ function ya.readable_path(path)
end
end
function ya.child_at(position, children)
function ya.child_at(pos, children)
for i = #children, 1, -1 do
if children[i]._area:contains(position) then
if children[i]._area:contains(pos) then
return children[i]
end
end

View file

@ -0,0 +1,83 @@
use std::ops::Deref;
use mlua::{MetaMethod, UserData};
pub struct ImageInfo(yazi_adapter::ImageInfo);
impl Deref for ImageInfo {
type Target = yazi_adapter::ImageInfo;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<yazi_adapter::ImageInfo> for ImageInfo {
fn from(value: yazi_adapter::ImageInfo) -> Self { Self(value) }
}
impl UserData for ImageInfo {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("w", |_, me| Ok(me.width));
fields.add_field_method_get("h", |_, me| Ok(me.height));
fields.add_field_method_get("ori", |_, me| Ok(me.orientation.map(|o| o.to_exif())));
fields.add_field_method_get("format", |_, me| Ok(ImageFormat(me.format)));
fields.add_field_method_get("color", |_, me| Ok(ImageColor(me.color)));
}
}
// --- ImageFormat
struct ImageFormat(yazi_adapter::ImageFormat);
impl UserData for ImageFormat {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(MetaMethod::ToString, |_, me, ()| {
use yazi_adapter::ImageFormat as F;
Ok(match me.0 {
F::Png => "PNG",
F::Jpeg => "JPEG",
F::Gif => "GIF",
F::WebP => "WEBP",
F::Pnm => "PNM",
F::Tiff => "TIFF",
F::Tga => "TGA",
F::Dds => "DDS",
F::Bmp => "BMP",
F::Ico => "ICO",
F::Hdr => "HDR",
F::OpenExr => "OpenEXR",
F::Farbfeld => "Farbfeld",
F::Avif => "AVIF",
F::Qoi => "QOI",
F::Pcx => "PCX",
_ => "Unknown",
})
});
}
}
// --- ImageColor
struct ImageColor(yazi_adapter::ImageColor);
impl UserData for ImageColor {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(MetaMethod::ToString, |_, me, ()| {
use yazi_adapter::ImageColor as C;
Ok(match me.0 {
C::L8 => "L8",
C::La8 => "La8",
C::Rgb8 => "Rgb8",
C::Rgba8 => "Rgba8",
C::L16 => "L16",
C::La16 => "La16",
C::Rgb16 => "Rgb16",
C::Rgba16 => "Rgba16",
C::Rgb32F => "Rgb32F",
C::Rgba32F => "Rgba32F",
_ => "Unknown",
})
});
}
}

View file

@ -1,3 +1,3 @@
#![allow(clippy::module_inception)]
yazi_macro::mod_flat!(bindings cha icon input mouse permit position range window);
yazi_macro::mod_flat!(bindings cha icon image input mouse permit range window);

View file

@ -1,47 +0,0 @@
use std::{ops::Deref, str::FromStr};
use mlua::{ExternalResult, IntoLua, Lua};
pub struct Position(yazi_config::popup::Position);
impl Deref for Position {
type Target = yazi_config::popup::Position;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<Position> for yazi_config::popup::Position {
fn from(value: Position) -> Self { value.0 }
}
impl TryFrom<mlua::Table> for Position {
type Error = mlua::Error;
fn try_from(t: mlua::Table) -> Result<Self, Self::Error> {
use yazi_config::popup::{Offset, Origin, Position};
Ok(Self(Position {
origin: Origin::from_str(&t.raw_get::<mlua::String>(1)?.to_str()?).into_lua_err()?,
offset: Offset {
x: t.raw_get("x").unwrap_or_default(),
y: t.raw_get("y").unwrap_or_default(),
width: t.raw_get("w")?,
height: t.raw_get("h").unwrap_or(3),
},
}))
}
}
impl IntoLua for Position {
fn into_lua(self, lua: &Lua) -> mlua::Result<mlua::Value> {
lua
.create_table_from([
(1.into_lua(lua)?, self.origin.to_string().into_lua(lua)?),
("x".into_lua(lua)?, self.offset.x.into_lua(lua)?),
("y".into_lua(lua)?, self.offset.y.into_lua(lua)?),
("w".into_lua(lua)?, self.offset.width.into_lua(lua)?),
("h".into_lua(lua)?, self.offset.height.into_lua(lua)?),
])?
.into_lua(lua)
}
}

View file

@ -9,13 +9,24 @@ pub struct Window {
pub height: u16,
}
impl Default for Window {
fn default() -> Self {
impl Window {
pub fn get() -> Self {
let ws = Dimension::available();
Self { rows: ws.rows, cols: ws.columns, width: ws.width, height: ws.height }
}
}
impl From<Window> for crossterm::terminal::WindowSize {
fn from(value: Window) -> Self {
crossterm::terminal::WindowSize {
rows: value.rows,
columns: value.cols,
width: value.width,
height: value.height,
}
}
}
impl UserData for Window {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("rows", |_, me| Ok(me.rows));

View file

@ -1,23 +0,0 @@
use mlua::AnyUserData;
use crate::elements::Renderable;
pub fn cast_to_renderable(ud: &AnyUserData) -> Option<Box<dyn Renderable + Send>> {
if let Ok(c) = ud.take::<crate::elements::Text>() {
Some(Box::new(c))
} else if let Ok(c) = ud.take::<crate::elements::List>() {
Some(Box::new(c))
} else if let Ok(c) = ud.take::<crate::elements::Bar>() {
Some(Box::new(c))
} else if let Ok(c) = ud.take::<crate::elements::Clear>() {
Some(Box::new(c))
} else if let Ok(c) = ud.take::<crate::elements::Border>() {
Some(Box::new(c))
} else if let Ok(c) = ud.take::<crate::elements::Gauge>() {
Some(Box::new(c))
} else if let Ok(c) = ud.take::<crate::elements::Table>() {
Some(Box::new(c))
} else {
None
}
}

View file

@ -1,7 +1,9 @@
use mlua::{Lua, LuaSerdeExt, SerializeOptions};
use mlua::{IntoLua, Lua, LuaSerdeExt, SerializeOptions, Table, Value};
use yazi_boot::BOOT;
use yazi_config::{MANAGER, PREVIEW, THEME};
use super::Plugin;
const OPTIONS: SerializeOptions =
SerializeOptions::new().serialize_none_to_null(false).serialize_unit_to_null(false);
@ -31,4 +33,26 @@ impl<'a> Config<'a> {
self.lua.globals().raw_set("PREVIEW", self.lua.to_value_with(&*PREVIEW, OPTIONS)?)?;
Ok(self)
}
pub fn install_plugin(self) -> mlua::Result<Self> {
let index = self.lua.create_function(|lua, (ts, key): (Table, mlua::String)| {
let value = match key.as_bytes().as_ref() {
b"fetchers" => Plugin::fetchers(lua)?,
b"spotter" => Plugin::spotter(lua)?,
b"preloaders" => Plugin::preloaders(lua)?,
b"previewer" => Plugin::previewer(lua)?,
_ => return Ok(Value::Nil),
}
.into_lua(lua)?;
ts.raw_set(key, value.clone())?;
Ok(value)
})?;
let fetcher = self.lua.create_table()?;
fetcher.set_metatable(Some(self.lua.create_table_from([("__index", index)])?));
self.lua.globals().raw_set("PLUGIN", fetcher)?;
Ok(self)
}
}

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(config plugin);

View file

@ -0,0 +1,72 @@
use mlua::{Function, Lua, UserData};
use yazi_config::PLUGIN;
use crate::url::UrlRef;
pub(super) struct Plugin;
impl Plugin {
pub(super) fn fetchers(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|lua, (url, mime): (UrlRef, mlua::String)| {
let factors = |s: &str| match s {
"mime" => !mime.as_bytes().is_empty(),
_ => false,
};
lua.create_sequence_from(PLUGIN.fetchers(&url, &mime.to_str()?, factors).map(Fetcher))
})
}
pub(super) fn spotter(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|_, (url, mime): (UrlRef, mlua::String)| {
Ok(PLUGIN.spotter(&url, &mime.to_str()?).map(Spotter))
})
}
pub(super) fn preloaders(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|lua, (url, mime): (UrlRef, mlua::String)| {
lua.create_sequence_from(PLUGIN.preloaders(&url, &mime.to_str()?).map(Preloader))
})
}
pub(super) fn previewer(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|_, (url, mime): (UrlRef, mlua::String)| {
Ok(PLUGIN.previewer(&url, &mime.to_str()?).map(Previewer))
})
}
}
// --- Fetcher
struct Fetcher(&'static yazi_config::plugin::Fetcher);
impl UserData for Fetcher {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("cmd", |lua, me| lua.create_string(&me.0.run.name));
}
}
// --- Spotter
struct Spotter(&'static yazi_config::plugin::Spotter);
impl UserData for Spotter {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("cmd", |lua, me| lua.create_string(&me.0.run.name));
}
}
// --- Preloader
struct Preloader(&'static yazi_config::plugin::Preloader);
impl UserData for Preloader {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("cmd", |lua, me| lua.create_string(&me.0.run.name));
}
}
// --- Previewer
struct Previewer(&'static yazi_config::plugin::Previewer);
impl UserData for Previewer {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("cmd", |lua, me| lua.create_string(&me.0.run.name));
}
}

View file

@ -0,0 +1,92 @@
use std::fmt::Debug;
use mlua::{AnyUserData, ExternalError, FromLua, IntoLua, Value};
use super::{Pos, Rect};
const EXPECTED: &str = "expected a Pos or Rect";
#[derive(Clone, Copy)]
pub enum Area {
Pos(Pos),
Rect(Rect),
}
impl Default for Area {
fn default() -> Self { Self::Rect(Default::default()) }
}
impl Area {
#[inline]
pub fn size(self) -> ratatui::layout::Size {
match self {
Self::Pos(pos) => {
ratatui::layout::Size { width: pos.offset.width, height: pos.offset.height }
}
Self::Rect(rect) => (*rect).into(),
}
}
#[inline]
pub fn inner(self, padding: ratatui::widgets::Padding) -> Self {
match self {
Self::Pos(mut pos) => {
pos.pad += padding;
Self::Pos(pos)
}
Self::Rect(rect) => Self::Rect(rect.pad(padding.into())),
}
}
#[inline]
pub fn transform(
self,
f: impl FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect,
) -> ratatui::layout::Rect {
match self {
Self::Pos(pos) => *Rect(f(*pos)).pad(pos.pad),
Self::Rect(rect) => *rect,
}
}
}
impl TryFrom<AnyUserData> for Area {
type Error = mlua::Error;
fn try_from(value: AnyUserData) -> Result<Self, Self::Error> {
Ok(if let Ok(rect) = value.borrow::<Rect>() {
Self::Rect(*rect)
} else if let Ok(pos) = value.borrow::<Pos>() {
Self::Pos(*pos)
} else {
return Err(EXPECTED.into_lua_err());
})
}
}
impl FromLua for Area {
fn from_lua(value: Value, _: &mlua::Lua) -> mlua::Result<Self> {
match value {
Value::UserData(ud) => Self::try_from(ud),
_ => Err(EXPECTED.into_lua_err()),
}
}
}
impl IntoLua for Area {
fn into_lua(self, lua: &mlua::Lua) -> mlua::Result<mlua::Value> {
match self {
Self::Pos(pos) => pos.into_lua(lua),
Self::Rect(rect) => rect.into_lua(lua),
}
}
}
impl Debug for Area {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pos(pos) => write!(f, "{:?}", **pos),
Self::Rect(rect) => write!(f, "{:?}", **rect),
}
}
}

View file

@ -1,11 +1,11 @@
use mlua::{AnyUserData, Lua, Table, UserData};
use mlua::{Lua, Table, UserData};
use ratatui::widgets::Borders;
use super::{Rect, Renderable};
use super::Area;
#[derive(Clone, Default)]
pub struct Bar {
area: Rect,
area: Area,
direction: ratatui::widgets::Borders,
symbol: String,
@ -31,6 +31,50 @@ impl Bar {
bar.set_metatable(Some(lua.create_table_from([("__call", new)])?));
Ok(bar)
}
pub(super) fn render(
self,
buf: &mut ratatui::buffer::Buffer,
trans: impl FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect,
) {
let rect = self.area.transform(trans);
if rect.area() == 0 {
return;
}
let symbol = if !self.symbol.is_empty() {
&self.symbol
} else if self.direction.intersects(Borders::TOP | Borders::BOTTOM) {
""
} else if self.direction.intersects(Borders::LEFT | Borders::RIGHT) {
""
} else {
" "
};
if self.direction.contains(Borders::LEFT) {
for y in rect.top()..rect.bottom() {
buf[(rect.left(), y)].set_style(self.style).set_symbol(symbol);
}
}
if self.direction.contains(Borders::TOP) {
for x in rect.left()..rect.right() {
buf[(x, rect.top())].set_style(self.style).set_symbol(symbol);
}
}
if self.direction.contains(Borders::RIGHT) {
let x = rect.right() - 1;
for y in rect.top()..rect.bottom() {
buf[(x, y)].set_style(self.style).set_symbol(symbol);
}
}
if self.direction.contains(Borders::BOTTOM) {
let y = rect.bottom() - 1;
for x in rect.left()..rect.right() {
buf[(x, y)].set_style(self.style).set_symbol(symbol);
}
}
}
}
impl UserData for Bar {
@ -48,48 +92,3 @@ impl UserData for Bar {
});
}
}
impl Renderable for Bar {
fn area(&self) -> ratatui::layout::Rect { *self.area }
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
if self.area.area() == 0 {
return;
}
let symbol = if !self.symbol.is_empty() {
&self.symbol
} else if self.direction.intersects(Borders::TOP | Borders::BOTTOM) {
""
} else if self.direction.intersects(Borders::LEFT | Borders::RIGHT) {
""
} else {
" "
};
if self.direction.contains(Borders::LEFT) {
for y in self.area.top()..self.area.bottom() {
buf[(self.area.left(), y)].set_style(self.style).set_symbol(symbol);
}
}
if self.direction.contains(Borders::TOP) {
for x in self.area.left()..self.area.right() {
buf[(x, self.area.top())].set_style(self.style).set_symbol(symbol);
}
}
if self.direction.contains(Borders::RIGHT) {
let x = self.area.right() - 1;
for y in self.area.top()..self.area.bottom() {
buf[(x, y)].set_style(self.style).set_symbol(symbol);
}
}
if self.direction.contains(Borders::BOTTOM) {
let y = self.area.bottom() - 1;
for x in self.area.left()..self.area.right() {
buf[(x, y)].set_style(self.style).set_symbol(symbol);
}
}
}
fn clone_render(&self, buf: &mut ratatui::buffer::Buffer) { Box::new(self.clone()).render(buf) }
}

View file

@ -1,7 +1,8 @@
use mlua::{AnyUserData, Lua, Table, UserData};
use mlua::{Lua, Table, UserData, Value};
use ratatui::widgets::{Borders, Widget};
use super::{Rect, Renderable};
use super::Area;
use crate::elements::Line;
// Type
const PLAIN: u8 = 0;
@ -13,11 +14,13 @@ const QUADRANT_OUTSIDE: u8 = 5;
#[derive(Clone, Default)]
pub struct Border {
area: Rect,
pub(crate) area: Area,
position: ratatui::widgets::Borders,
type_: ratatui::widgets::BorderType,
style: ratatui::style::Style,
pub(crate) position: ratatui::widgets::Borders,
pub(crate) type_: ratatui::widgets::BorderType,
pub(crate) style: ratatui::style::Style,
pub(crate) titles: Vec<(ratatui::widgets::block::Position, ratatui::text::Line<'static>)>,
}
impl Border {
@ -49,6 +52,26 @@ impl Border {
border.set_metatable(Some(lua.create_table_from([("__call", new)])?));
Ok(border)
}
pub(super) fn render(
self,
buf: &mut ratatui::buffer::Buffer,
trans: impl FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect,
) {
let mut block = ratatui::widgets::Block::default()
.borders(self.position)
.border_type(self.type_)
.border_style(self.style);
for title in self.titles {
block = match title {
(ratatui::widgets::block::Position::Top, line) => block.title(line),
(ratatui::widgets::block::Position::Bottom, line) => block.title(line),
};
}
block.render(self.area.transform(trans), buf);
}
}
impl UserData for Border {
@ -56,10 +79,6 @@ impl UserData for Border {
crate::impl_area_method!(methods);
crate::impl_style_method!(methods, style);
methods.add_function_mut("position", |_, (ud, position): (AnyUserData, u8)| {
ud.borrow_mut::<Self>()?.position = ratatui::widgets::Borders::from_bits_truncate(position);
Ok(ud)
});
methods.add_function_mut("type", |_, (ud, value): (AnyUserData, u8)| {
ud.borrow_mut::<Self>()?.type_ = match value {
ROUNDED => ratatui::widgets::BorderType::Rounded,
@ -71,19 +90,22 @@ impl UserData for Border {
};
Ok(ud)
});
methods.add_function_mut(
"title",
|_, (ud, line, position): (AnyUserData, Value, Option<u8>)| {
let position = if position == Some(Borders::BOTTOM.bits()) {
ratatui::widgets::block::Position::Bottom
} else {
ratatui::widgets::block::Position::Top
};
ud.borrow_mut::<Self>()?.titles.push((position, Line::try_from(line)?.0));
Ok(ud)
},
);
methods.add_function_mut("position", |_, (ud, position): (AnyUserData, u8)| {
ud.borrow_mut::<Self>()?.position = ratatui::widgets::Borders::from_bits_truncate(position);
Ok(ud)
});
}
}
impl Renderable for Border {
fn area(&self) -> ratatui::layout::Rect { *self.area }
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
ratatui::widgets::Block::default()
.borders(self.position)
.border_type(self.type_)
.border_style(self.style)
.render(*self.area, buf);
}
fn clone_render(&self, buf: &mut ratatui::buffer::Buffer) { Box::new(self.clone()).render(buf); }
}

View file

@ -0,0 +1,20 @@
use mlua::{ExternalError, FromLua};
use super::Text;
const EXPECTED: &str = "expected a table of strings, Texts, Lines or Spans";
#[derive(Clone)]
pub struct Cell {
pub(super) text: ratatui::text::Text<'static>,
}
impl From<Cell> for ratatui::widgets::Cell<'static> {
fn from(value: Cell) -> Self { Self::new(value.text) }
}
impl FromLua for Cell {
fn from_lua(value: mlua::Value, _: &mlua::Lua) -> mlua::Result<Self> {
Ok(Self { text: Text::try_from(value).map_err(|_| EXPECTED.into_lua_err())?.inner })
}
}

View file

@ -3,24 +3,32 @@ use std::sync::atomic::{AtomicBool, Ordering};
use mlua::{Lua, Table, UserData};
use yazi_adapter::ADAPTOR;
use super::{Rect, Renderable};
use super::Area;
pub static COLLISION: AtomicBool = AtomicBool::new(false);
#[derive(Clone, Copy, Default)]
pub struct Clear {
pub area: Rect,
pub area: Area,
}
impl Clear {
pub fn compose(lua: &Lua) -> mlua::Result<Table> {
let new = lua.create_function(|_, (_, area): (Table, Rect)| Ok(Clear { area }))?;
let new = lua.create_function(|_, (_, area): (Table, Area)| Ok(Clear { area }))?;
let clear = lua.create_table()?;
clear.set_metatable(Some(lua.create_table_from([("__call", new)])?));
Ok(clear)
}
pub(super) fn render(
self,
buf: &mut ratatui::buffer::Buffer,
trans: impl FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect,
) {
<Self as ratatui::widgets::Widget>::render(Default::default(), self.area.transform(trans), buf);
}
}
impl ratatui::widgets::Widget for Clear {
@ -44,16 +52,6 @@ impl ratatui::widgets::Widget for Clear {
}
}
impl Renderable for Clear {
fn area(&self) -> ratatui::layout::Rect { *self.area }
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
<Self as ratatui::widgets::Widget>::render(Default::default(), *self.area, buf);
}
fn clone_render(&self, buf: &mut ratatui::buffer::Buffer) { Box::new(*self).render(buf); }
}
impl UserData for Clear {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
crate::impl_area_method!(methods);

View file

@ -1,7 +1,7 @@
use mlua::{AnyUserData, IntoLua, Lua, Table, Value};
use tracing::error;
use crate::cast_to_renderable;
use super::Renderable;
pub fn compose(lua: &Lua) -> mlua::Result<Table> {
let index = lua.create_function(|lua, (ts, key): (Table, mlua::String)| {
@ -14,14 +14,16 @@ pub fn compose(lua: &Lua) -> mlua::Result<Table> {
b"Layout" => super::Layout::compose(lua)?,
b"Line" => super::Line::compose(lua)?,
b"List" => super::List::compose(lua)?,
b"Padding" => super::Padding::compose(lua)?,
b"Pad" => super::Pad::compose(lua, true)?,
// TODO: deprecate this
b"Padding" => super::Pad::compose(lua, false)?,
b"Paragraph" => super::Paragraph::compose(lua)?,
b"Position" => super::Position::compose(lua)?,
b"Pos" => super::Pos::compose(lua)?,
b"Rect" => super::Rect::compose(lua)?,
b"Row" => super::Row::compose(lua)?,
b"Span" => super::Span::compose(lua)?,
b"Style" => super::Style::compose(lua)?,
b"Table" => super::Table::compose(lua)?,
b"TableRow" => super::TableRow::compose(lua)?,
b"Text" => super::Text::compose(lua)?,
_ => return Ok(Value::Nil),
}
@ -37,24 +39,19 @@ pub fn compose(lua: &Lua) -> mlua::Result<Table> {
Ok(ui)
}
pub trait Renderable {
fn area(&self) -> ratatui::layout::Rect;
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer);
fn clone_render(&self, buf: &mut ratatui::buffer::Buffer);
}
pub fn render_widgets(widgets: Table, buf: &mut ratatui::buffer::Buffer) {
pub fn render_once<F>(widgets: Table, buf: &mut ratatui::buffer::Buffer, trans: F)
where
F: Fn(yazi_config::popup::Position) -> ratatui::layout::Rect + Copy,
{
for widget in widgets.sequence_values::<AnyUserData>() {
let Ok(widget) = widget else {
error!("Failed to convert to renderable UserData: {}", widget.unwrap_err());
continue;
};
match cast_to_renderable(&widget) {
Some(w) => w.render(buf),
None => error!("Only the UserData of renderable element is accepted: {widget:#?}"),
match Renderable::try_from(widget) {
Ok(w) => w.render(buf, trans),
Err(e) => error!("{e}"),
}
}
}

View file

@ -1,12 +1,12 @@
use mlua::{AnyUserData, ExternalError, Lua, Table, UserData, UserDataMethods, Value};
use mlua::{ExternalError, Lua, Table, UserData, UserDataMethods, Value};
use ratatui::widgets::Widget;
use super::{Rect, Renderable, Span};
use super::{Area, Span};
use crate::elements::Style;
#[derive(Clone, Default)]
pub struct Gauge {
area: Rect,
area: Area,
ratio: f64,
label: Option<ratatui::text::Span<'static>>,
@ -23,6 +23,23 @@ impl Gauge {
Ok(gauge)
}
pub(super) fn render(
self,
buf: &mut ratatui::buffer::Buffer,
trans: impl FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect,
) {
let mut gauge = ratatui::widgets::Gauge::default()
.ratio(self.ratio)
.style(self.style)
.gauge_style(self.gauge_style);
if let Some(s) = self.label {
gauge = gauge.label(s)
}
gauge.render(self.area.transform(trans), buf);
}
}
impl UserData for Gauge {
@ -59,22 +76,3 @@ impl UserData for Gauge {
});
}
}
impl Renderable for Gauge {
fn area(&self) -> ratatui::layout::Rect { *self.area }
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
let mut gauge = ratatui::widgets::Gauge::default()
.ratio(self.ratio)
.style(self.style)
.gauge_style(self.gauge_style);
if let Some(s) = self.label {
gauge = gauge.label(s)
}
gauge.render(*self.area, buf);
}
fn clone_render(&self, buf: &mut ratatui::buffer::Buffer) { Box::new(self.clone()).render(buf) }
}

View file

@ -10,7 +10,7 @@ const LEFT: u8 = 0;
const CENTER: u8 = 1;
const RIGHT: u8 = 2;
const EXPECTED: &str = "expected a string, ui.Span, ui.Line, or a table of them";
const EXPECTED: &str = "expected a string, Span, Line, or a table of them";
#[derive(Clone, FromLua)]
pub struct Line(pub(super) ratatui::text::Line<'static>);

View file

@ -1,12 +1,14 @@
use mlua::{ExternalError, Lua, Table, UserData, Value};
use ratatui::widgets::Widget;
use super::{Rect, Renderable, Text};
use super::{Area, Text};
const EXPECTED: &str = "expected a table of strings, Texts, Lines or Spans";
// --- List
#[derive(Clone, Default)]
pub struct List {
area: Rect,
area: Area,
inner: ratatui::widgets::List<'static>,
}
@ -16,10 +18,7 @@ impl List {
let new = lua.create_function(|_, (_, seq): (Table, Table)| {
let mut items = Vec::with_capacity(seq.raw_len());
for v in seq.sequence_values::<Value>() {
match v? {
Value::Table(_) => Err("Nested table not supported".into_lua_err())?,
v => items.push(Text::try_from(v)?),
}
items.push(Text::try_from(v?).map_err(|_| EXPECTED.into_lua_err())?);
}
Ok(Self { inner: ratatui::widgets::List::new(items), ..Default::default() })
@ -30,6 +29,14 @@ impl List {
Ok(list)
}
pub(super) fn render(
self,
buf: &mut ratatui::buffer::Buffer,
trans: impl FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect,
) {
self.inner.render(self.area.transform(trans), buf);
}
}
impl UserData for List {
@ -37,13 +44,3 @@ impl UserData for List {
crate::impl_area_method!(methods);
}
}
impl Renderable for List {
fn area(&self) -> ratatui::layout::Rect { *self.area }
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
self.inner.render(*self.area, buf);
}
fn clone_render(&self, buf: &mut ratatui::buffer::Buffer) { Box::new(self.clone()).render(buf) }
}

View file

@ -1,3 +1,3 @@
#![allow(clippy::module_inception)]
yazi_macro::mod_flat!(bar border clear constraint elements gauge layout line list padding paragraph position rect span style table text);
yazi_macro::mod_flat!(area bar border cell clear constraint elements gauge layout line list pad paragraph pos rect renderable row span style table text);

View file

@ -1,24 +1,33 @@
use std::ops::Deref;
use std::ops::{Add, AddAssign, Deref};
use mlua::{FromLua, Lua, Table, UserData};
#[derive(Clone, Copy, FromLua)]
pub struct Padding(ratatui::widgets::Padding);
#[derive(Clone, Copy, Default, FromLua)]
pub struct Pad(ratatui::widgets::Padding);
impl Deref for Padding {
impl Deref for Pad {
type Target = ratatui::widgets::Padding;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl Padding {
pub fn compose(lua: &Lua) -> mlua::Result<Table> {
let new =
impl From<ratatui::widgets::Padding> for Pad {
fn from(pad: ratatui::widgets::Padding) -> Self { Self(pad) }
}
impl Pad {
pub fn compose(lua: &Lua, v4: bool) -> mlua::Result<Table> {
let new = if v4 {
lua.create_function(|_, (_, top, right, bottom, left): (Table, u16, u16, u16, u16)| {
Ok(Self(ratatui::widgets::Padding::new(left, right, top, bottom)))
})?
} else {
lua.create_function(|_, (_, left, right, top, bottom): (Table, u16, u16, u16, u16)| {
Ok(Self(ratatui::widgets::Padding::new(left, right, top, bottom)))
})?;
})?
};
let padding = lua.create_table_from([
let pad = lua.create_table_from([
(
"left",
lua.create_function(|_, left: u16| Ok(Self(ratatui::widgets::Padding::left(left))))?,
@ -42,12 +51,12 @@ impl Padding {
),
])?;
padding.set_metatable(Some(lua.create_table_from([("__call", new)])?));
Ok(padding)
pad.set_metatable(Some(lua.create_table_from([("__call", new)])?));
Ok(pad)
}
}
impl UserData for Padding {
impl UserData for Pad {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("left", |_, me| Ok(me.left));
fields.add_field_method_get("right", |_, me| Ok(me.right));
@ -55,3 +64,20 @@ impl UserData for Padding {
fields.add_field_method_get("bottom", |_, me| Ok(me.bottom));
}
}
impl Add<ratatui::widgets::Padding> for Pad {
type Output = Self;
fn add(self, rhs: ratatui::widgets::Padding) -> Self::Output {
Self(ratatui::widgets::Padding::new(
self.left.saturating_add(rhs.left),
self.right.saturating_add(rhs.right),
self.top.saturating_add(rhs.top),
self.bottom.saturating_add(rhs.bottom),
))
}
}
impl AddAssign<ratatui::widgets::Padding> for Pad {
fn add_assign(&mut self, rhs: ratatui::widgets::Padding) { *self = *self + rhs; }
}

View file

@ -0,0 +1,80 @@
use std::{ops::Deref, str::FromStr};
use mlua::{AnyUserData, ExternalResult, Lua, Table, UserData};
use super::Pad;
#[derive(Clone, Copy, Default)]
pub struct Pos {
inner: yazi_config::popup::Position,
pub(super) pad: Pad,
}
impl Deref for Pos {
type Target = yazi_config::popup::Position;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl From<yazi_config::popup::Position> for Pos {
fn from(value: yazi_config::popup::Position) -> Self {
Self { inner: value, ..Default::default() }
}
}
impl From<Pos> for yazi_config::popup::Position {
fn from(value: Pos) -> Self { value.inner }
}
impl TryFrom<mlua::Table> for Pos {
type Error = mlua::Error;
fn try_from(t: mlua::Table) -> Result<Self, Self::Error> {
use yazi_config::popup::{Offset, Origin, Position};
Ok(Self::from(Position {
origin: Origin::from_str(&t.raw_get::<mlua::String>(1)?.to_str()?).into_lua_err()?,
offset: Offset {
x: t.raw_get("x").unwrap_or_default(),
y: t.raw_get("y").unwrap_or_default(),
width: t.raw_get("w").unwrap_or_default(),
height: t.raw_get("h").unwrap_or_default(),
},
}))
}
}
impl Pos {
pub fn compose(lua: &Lua) -> mlua::Result<Table> {
let new = lua.create_function(|_, (_, t): (Table, Table)| Self::try_from(t))?;
let position = lua.create_table()?;
position.set_metatable(Some(lua.create_table_from([("__call", new)])?));
Ok(position)
}
pub fn new_input(t: mlua::Table) -> mlua::Result<Self> {
let mut p = Self::try_from(t)?;
p.inner.offset.height = 3;
Ok(p)
}
}
impl UserData for Pos {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get(1, |_, me| Ok(me.origin.to_string()));
fields.add_field_method_get("x", |_, me| Ok(me.offset.x));
fields.add_field_method_get("y", |_, me| Ok(me.offset.y));
fields.add_field_method_get("w", |_, me| Ok(me.offset.width));
fields.add_field_method_get("h", |_, me| Ok(me.offset.height));
}
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_function_mut("pad", |_, (ud, pad): (AnyUserData, Pad)| {
ud.borrow_mut::<Self>()?.pad = pad;
Ok(ud)
});
}
}

View file

@ -1,36 +0,0 @@
use std::ops::Deref;
use mlua::{FromLua, Lua, Table, UserData, UserDataFields};
#[derive(Clone, Copy, FromLua)]
pub struct Position(ratatui::layout::Position);
impl Deref for Position {
type Target = ratatui::layout::Position;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<ratatui::layout::Rect> for Position {
fn from(rect: ratatui::layout::Rect) -> Self { Self(rect.as_position()) }
}
impl Position {
pub fn compose(lua: &Lua) -> mlua::Result<Table> {
let new = lua.create_function(|_, (_, args): (Table, Table)| {
Ok(Self(ratatui::layout::Position { x: args.raw_get("x")?, y: args.raw_get("y")? }))
})?;
let position = lua.create_table_from([("default", Self(Default::default()))])?;
position.set_metatable(Some(lua.create_table_from([("__call", new)])?));
Ok(position)
}
}
impl UserData for Position {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("x", |_, me| Ok(me.x));
fields.add_field_method_get("y", |_, me| Ok(me.y));
}
}

View file

@ -2,10 +2,10 @@ use std::ops::Deref;
use mlua::{FromLua, Lua, Table, UserData};
use super::{Padding, Position};
use super::Pad;
#[derive(Clone, Copy, Default, FromLua)]
pub struct Rect(ratatui::layout::Rect);
pub struct Rect(pub(super) ratatui::layout::Rect);
impl Deref for Rect {
type Target = ratatui::layout::Rect;
@ -27,10 +27,10 @@ impl Rect {
pub fn compose(lua: &Lua) -> mlua::Result<Table> {
let new = lua.create_function(|_, (_, args): (Table, Table)| {
Ok(Self(ratatui::layout::Rect {
x: args.raw_get("x")?,
y: args.raw_get("y")?,
width: args.raw_get("w")?,
height: args.raw_get("h")?,
x: args.raw_get("x").unwrap_or_default(),
y: args.raw_get("y").unwrap_or_default(),
width: args.raw_get("w").unwrap_or_default(),
height: args.raw_get("h").unwrap_or_default(),
}))
})?;
@ -39,6 +39,16 @@ impl Rect {
rect.set_metatable(Some(lua.create_table_from([("__call", new)])?));
Ok(rect)
}
pub(super) fn pad(self, pad: Pad) -> Self {
let mut r = *self;
r.x = r.x.saturating_add(pad.left);
r.y = r.y.saturating_add(pad.top);
r.width = r.width.saturating_sub(pad.left + pad.right);
r.height = r.height.saturating_sub(pad.top + pad.bottom);
Self(r)
}
}
impl UserData for Rect {
@ -55,16 +65,9 @@ impl UserData for Rect {
}
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("padding", |_, me, padding: Padding| {
let mut r = **me;
r.x = r.x.saturating_add(padding.left);
r.y = r.y.saturating_add(padding.top);
r.width = r.width.saturating_sub(padding.left + padding.right);
r.height = r.height.saturating_sub(padding.top + padding.bottom);
Ok(Self(r))
});
methods.add_method("position", |_, me, ()| Ok(Position::from(**me)));
methods.add_method("contains", |_, me, position: Position| Ok(me.contains(*position)));
methods.add_method("pad", |_, me, pad: Pad| Ok(me.pad(pad)));
// TODO: deprecate this
methods.add_method("padding", |_, me, pad: Pad| Ok(me.pad(pad)));
methods.add_method("contains", |_, me, Rect(rect)| Ok(me.contains(rect.into())));
}
}

View file

@ -0,0 +1,58 @@
use mlua::{AnyUserData, ExternalError};
use super::{Bar, Border, Clear, Gauge, List, Table, Text};
#[derive(Clone)]
pub enum Renderable {
Text(Text),
List(List),
Bar(Bar),
Clear(Clear),
Border(Border),
Gauge(Gauge),
Table(Table),
}
impl Renderable {
pub fn render(
self,
buf: &mut ratatui::buffer::Buffer,
trans: impl Fn(yazi_config::popup::Position) -> ratatui::layout::Rect,
) {
match self {
Self::Text(text) => text.render(buf, trans),
Self::List(list) => list.render(buf, trans),
Self::Bar(bar) => bar.render(buf, trans),
Self::Clear(clear) => clear.render(buf, trans),
Self::Border(border) => border.render(buf, trans),
Self::Gauge(gauge) => gauge.render(buf, trans),
Self::Table(table) => table.render(buf, trans),
}
}
}
impl TryFrom<AnyUserData> for Renderable {
type Error = mlua::Error;
fn try_from(ud: AnyUserData) -> Result<Self, Self::Error> {
Ok(if let Ok(c) = ud.take::<crate::elements::Text>() {
Self::Text(c)
} else if let Ok(c) = ud.take::<crate::elements::List>() {
Self::List(c)
} else if let Ok(c) = ud.take::<crate::elements::Bar>() {
Self::Bar(c)
} else if let Ok(c) = ud.take::<crate::elements::Clear>() {
Self::Clear(c)
} else if let Ok(c) = ud.take::<crate::elements::Border>() {
Self::Border(c)
} else if let Ok(c) = ud.take::<crate::elements::Gauge>() {
Self::Gauge(c)
} else if let Ok(c) = ud.take::<crate::elements::Table>() {
Self::Table(c)
} else {
return Err(
format!("expected a UserData of renderable element, not: {ud:#?}").into_lua_err(),
);
})
}
}

View file

@ -0,0 +1,54 @@
use mlua::{AnyUserData, FromLua, Lua, Table, UserData};
use super::Cell;
#[derive(Clone, Default, FromLua)]
pub struct Row {
pub(super) cells: Vec<Cell>,
height: u16,
top_margin: u16,
bottom_margin: u16,
style: ratatui::style::Style,
}
impl Row {
pub fn compose(lua: &Lua) -> mlua::Result<Table> {
let new = lua.create_function(|_, (_, cells): (Table, Vec<Cell>)| {
Ok(Self { cells, ..Default::default() })
})?;
let row = lua.create_table()?;
row.set_metatable(Some(lua.create_table_from([("__call", new)])?));
Ok(row)
}
}
impl From<Row> for ratatui::widgets::Row<'static> {
fn from(value: Row) -> Self {
Self::new(value.cells)
.height(value.height.max(1))
.top_margin(value.top_margin)
.bottom_margin(value.bottom_margin)
.style(value.style)
}
}
impl UserData for Row {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
crate::impl_style_method!(methods, style);
methods.add_function_mut("height", |_, (ud, value): (AnyUserData, u16)| {
ud.borrow_mut::<Self>()?.height = value;
Ok(ud)
});
methods.add_function_mut("margin_t", |_, (ud, value): (AnyUserData, u16)| {
ud.borrow_mut::<Self>()?.top_margin = value;
Ok(ud)
});
methods.add_function_mut("margin_b", |_, (ud, value): (AnyUserData, u16)| {
ud.borrow_mut::<Self>()?.bottom_margin = value;
Ok(ud)
});
}
}

View file

@ -1,7 +1,7 @@
use mlua::{ExternalError, FromLua, Lua, Table, UserData, UserDataMethods, Value};
use unicode_width::UnicodeWidthChar;
const EXPECTED: &str = "expected a string or ui.Span";
const EXPECTED: &str = "expected a string or Span";
#[derive(Clone, FromLua)]
pub struct Span(pub(super) ratatui::text::Span<'static>);

View file

@ -1,33 +1,38 @@
use mlua::{AnyUserData, FromLua, Lua, UserData};
use mlua::{AnyUserData, Lua, UserData, Value};
use ratatui::widgets::StatefulWidget;
use super::{Rect, Renderable, Text};
use crate::elements::Constraint;
use super::{Area, Row};
use crate::elements::{Constraint, Style};
// --- Table
#[derive(Clone, Default)]
pub struct Table {
area: Rect,
pub(crate) area: Area,
rows: Vec<ratatui::widgets::Row<'static>>,
header: Option<ratatui::widgets::Row<'static>>,
footer: Option<ratatui::widgets::Row<'static>>,
widths: Vec<ratatui::layout::Constraint>,
column_spacing: u16,
block: Option<ratatui::widgets::Block<'static>>,
style: ratatui::style::Style,
row_highlight_style: ratatui::style::Style,
highlight_symbol: ratatui::text::Text<'static>,
highlight_spacing: ratatui::widgets::HighlightSpacing,
flex: ratatui::layout::Flex,
rows: Vec<Row>,
header: Option<ratatui::widgets::Row<'static>>,
footer: Option<ratatui::widgets::Row<'static>>,
widths: Vec<ratatui::layout::Constraint>,
column_spacing: u16,
block: Option<ratatui::widgets::Block<'static>>, // TODO
style: ratatui::style::Style,
row_highlight_style: ratatui::style::Style,
column_highlight_style: ratatui::style::Style,
cell_highlight_style: ratatui::style::Style,
highlight_symbol: ratatui::text::Text<'static>, // TODO
highlight_spacing: ratatui::widgets::HighlightSpacing, // TODO
flex: ratatui::layout::Flex,
state: ratatui::widgets::TableState,
}
impl Table {
pub fn compose(lua: &Lua) -> mlua::Result<mlua::Table> {
let new = lua.create_function(|_, (_, area, rows): (mlua::Table, Rect, Vec<TableRow>)| {
Ok(Self { area, rows: rows.into_iter().map(Into::into).collect(), ..Default::default() })
let new = lua.create_function(|_, (_, rows): (mlua::Table, Vec<Row>)| {
Ok(Self { rows, ..Default::default() })
})?;
let table = lua.create_table()?;
@ -35,35 +40,24 @@ impl Table {
Ok(table)
}
}
impl UserData for Table {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
crate::impl_area_method!(methods);
methods.add_function_mut("header", |_, (ud, row): (AnyUserData, TableRow)| {
ud.borrow_mut::<Self>()?.header = Some(row.into());
Ok(ud)
});
methods.add_function_mut("footer", |_, (ud, row): (AnyUserData, TableRow)| {
ud.borrow_mut::<Self>()?.footer = Some(row.into());
Ok(ud)
});
methods.add_function_mut("widths", |_, (ud, widths): (AnyUserData, Vec<Constraint>)| {
ud.borrow_mut::<Self>()?.widths = widths.into_iter().map(Into::into).collect();
Ok(ud)
});
pub fn selected_cell(&self) -> Option<&ratatui::text::Text> {
let row = &self.rows[self.selected()?];
let col = self.state.selected_column()?;
if row.cells.is_empty() { None } else { Some(&row.cells[col.min(row.cells.len() - 1)].text) }
}
}
impl Renderable for Table {
fn area(&self) -> ratatui::layout::Rect { *self.area }
fn render(mut self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
pub(super) fn render(
mut self,
buf: &mut ratatui::buffer::Buffer,
trans: impl FnOnce(yazi_config::popup::Position) -> ratatui::layout::Rect,
) {
let mut table = ratatui::widgets::Table::new(self.rows, self.widths)
.column_spacing(self.column_spacing)
.style(self.style)
.row_highlight_style(self.row_highlight_style)
.column_highlight_style(self.column_highlight_style)
.cell_highlight_style(self.cell_highlight_style)
.highlight_symbol(self.highlight_symbol)
.highlight_spacing(self.highlight_spacing)
.flex(self.flex);
@ -78,59 +72,70 @@ impl Renderable for Table {
table = table.block(block);
}
table.render(*self.area, buf, &mut self.state);
table.render(self.area.transform(trans), buf, &mut self.state);
}
fn clone_render(&self, buf: &mut ratatui::buffer::Buffer) { Box::new(self.clone()).render(buf) }
}
pub(crate) fn select(&mut self, idx: Option<usize>) {
self
.state
.select(idx.map(|i| if self.rows.is_empty() { 0 } else { i.min(self.rows.len() - 1) }));
}
// --- TableRow
#[derive(Clone, Default, FromLua)]
pub struct TableRow {
cells: Vec<ratatui::widgets::Cell<'static>>,
height: u16,
top_margin: u16,
bottom_margin: u16,
style: ratatui::style::Style,
}
impl TableRow {
pub fn compose(lua: &Lua) -> mlua::Result<mlua::Table> {
let new = lua.create_function(|_, (_, cols): (mlua::Table, Vec<Text>)| {
Ok(Self { cells: cols.into_iter().map(Into::into).collect(), ..Default::default() })
})?;
let row = lua.create_table()?;
row.set_metatable(Some(lua.create_table_from([("__call", new)])?));
Ok(row)
pub(crate) fn selected(&self) -> Option<usize> {
if self.rows.is_empty() { None } else { Some(self.state.selected()?.min(self.rows.len() - 1)) }
}
}
impl From<TableRow> for ratatui::widgets::Row<'static> {
fn from(value: TableRow) -> Self {
Self::new(value.cells)
.height(value.height)
.top_margin(value.top_margin)
.bottom_margin(value.bottom_margin)
.style(value.style)
}
impl TryFrom<AnyUserData> for Table {
type Error = mlua::Error;
fn try_from(value: AnyUserData) -> Result<Self, Self::Error> { value.take() }
}
impl UserData for TableRow {
impl UserData for Table {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
crate::impl_style_method!(methods, style);
crate::impl_area_method!(methods);
methods.add_function_mut("height", |_, (ud, value): (AnyUserData, u16)| {
ud.borrow_mut::<Self>()?.height = value;
methods.add_function_mut("header", |_, (ud, row): (AnyUserData, Row)| {
ud.borrow_mut::<Self>()?.header = Some(row.into());
Ok(ud)
});
methods.add_function_mut("margin_t", |_, (ud, value): (AnyUserData, u16)| {
ud.borrow_mut::<Self>()?.top_margin = value;
methods.add_function_mut("footer", |_, (ud, row): (AnyUserData, Row)| {
ud.borrow_mut::<Self>()?.footer = Some(row.into());
Ok(ud)
});
methods.add_function_mut("margin_b", |_, (ud, value): (AnyUserData, u16)| {
ud.borrow_mut::<Self>()?.bottom_margin = value;
methods.add_function_mut("widths", |_, (ud, widths): (AnyUserData, Vec<Constraint>)| {
ud.borrow_mut::<Self>()?.widths = widths.into_iter().map(Into::into).collect();
Ok(ud)
});
methods.add_function_mut("spacing", |_, (ud, spacing): (AnyUserData, u16)| {
ud.borrow_mut::<Self>()?.column_spacing = spacing;
Ok(ud)
});
methods.add_function_mut("row", |_, (ud, idx): (AnyUserData, Option<usize>)| {
ud.borrow_mut::<Self>()?.state.select(idx);
Ok(ud)
});
methods.add_function_mut("col", |_, (ud, idx): (AnyUserData, Option<usize>)| {
ud.borrow_mut::<Self>()?.state.select_column(idx);
Ok(ud)
});
methods.add_function_mut("style", |_, (ud, value): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.style = Style::try_from(value)?.0;
Ok(ud)
});
methods.add_function_mut("row_style", |_, (ud, value): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.row_highlight_style = Style::try_from(value)?.0;
Ok(ud)
});
methods.add_function_mut("col_style", |_, (ud, value): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.column_highlight_style = Style::try_from(value)?.0;
Ok(ud)
});
methods.add_function_mut("cell_style", |_, (ud, value): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.cell_highlight_style = Style::try_from(value)?.0;
Ok(ud)
});
}

View file

@ -1,8 +1,8 @@
use ansi_to_tui::IntoText;
use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, IntoLua, Lua, Table, UserData, Value};
use mlua::{ExternalError, ExternalResult, FromLua, IntoLua, Lua, Table, UserData, Value};
use ratatui::widgets::Widget;
use super::{Line, Rect, Renderable, Span};
use super::{Area, Line, Span};
// Alignment
pub(super) const LEFT: u8 = 0;
@ -14,11 +14,11 @@ pub const WRAP_NO: u8 = 0;
pub const WRAP: u8 = 1;
pub const WRAP_TRIM: u8 = 2;
const EXPECTED: &str = "expected a string, ui.Line, ui.Span or a table of them";
const EXPECTED: &str = "expected a string, Line, Span, or a table of them";
#[derive(Clone, Default, FromLua)]
pub struct Text {
pub area: Rect,
pub area: Area,
// TODO: block
pub inner: ratatui::text::Text<'static>,
@ -49,6 +49,16 @@ impl Text {
text.set_metatable(Some(lua.create_table_from([("__call", new)])?));
Ok(text)
}
pub(super) fn render(
self,
buf: &mut ratatui::buffer::Buffer,
trans: impl Fn(yazi_config::popup::Position) -> ratatui::layout::Rect,
) {
let rect = self.area.transform(trans);
let p: ratatui::widgets::Paragraph = self.into();
p.render(rect, buf);
}
}
impl TryFrom<Value> for Text {
@ -63,6 +73,8 @@ impl TryFrom<Value> for Text {
line.into()
} else if let Ok(Span(span)) = ud.take() {
span.into()
} else if let Ok(text) = ud.take() {
return Ok(text);
} else {
Err(EXPECTED.into_lua_err())?
}
@ -131,24 +143,12 @@ impl UserData for Text {
methods.add_function_mut("wrap", |_, (ud, wrap): (AnyUserData, u8)| {
ud.borrow_mut::<Self>()?.wrap = match wrap {
w @ (WRAP | WRAP_TRIM | WRAP_NO) => w,
_ => return Err("expected a WRAP or WRAP_TRIM or WRAP_OFF".into_lua_err()),
_ => return Err("expected a WRAP, WRAP_TRIM or WRAP_NO".into_lua_err()),
};
Ok(ud)
});
methods.add_method("max_width", |_, me, ()| {
Ok(me.inner.lines.iter().take(me.area.height as usize).map(|l| l.width()).max())
Ok(me.inner.lines.iter().take(me.area.size().height as usize).map(|l| l.width()).max())
});
}
}
impl Renderable for Text {
fn area(&self) -> ratatui::layout::Rect { *self.area }
fn render(self: Box<Self>, buf: &mut ratatui::buffer::Buffer) {
let area = *self.area;
let p: ratatui::widgets::Paragraph = (*self).into();
p.render(area, buf);
}
fn clone_render(&self, buf: &mut ratatui::buffer::Buffer) { Box::new(self.clone()).render(buf) }
}

View file

@ -1,7 +1,7 @@
use std::{borrow::Cow, io::Cursor, mem, path::{Path, PathBuf}, sync::OnceLock};
use anyhow::{Result, anyhow};
use ratatui::{layout::Rect, text::{Line, Span, Text}};
use ratatui::{layout::Size, text::{Line, Span, Text}};
use syntect::{LoadingError, dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}};
use tokio::{fs::File, io::{AsyncBufReadExt, BufReader}};
use yazi_config::{PREVIEW, THEME, preview::PreviewWrap};
@ -36,14 +36,14 @@ impl Highlighter {
#[inline]
pub fn abort() { INCR.next(); }
pub async fn highlight(&self, skip: usize, area: Rect) -> Result<Text<'static>, PeekError> {
pub async fn highlight(&self, skip: usize, size: Size) -> Result<Text<'static>, PeekError> {
let mut reader = BufReader::new(File::open(&self.path).await?);
let syntax = Self::find_syntax(&self.path).await;
let mut plain = syntax.is_err();
let mut before = Vec::with_capacity(if plain { 0 } else { skip });
let mut after = Vec::with_capacity(area.height as _);
let mut after = Vec::with_capacity(size.height as _);
let mut i = 0;
let mut buf = vec![];
@ -67,24 +67,24 @@ impl Highlighter {
i += if i >= skip {
buf.iter_mut().for_each(Self::carriage_return_to_line_feed);
after.push(String::from_utf8_lossy(&buf).into_owned());
Self::line_height(&after[after.len() - 1], area.width)
Self::line_height(&after[after.len() - 1], size.width)
} else if !plain {
before.push(String::from_utf8_lossy(&buf).into_owned());
Self::line_height(&before[before.len() - 1], area.width)
Self::line_height(&before[before.len() - 1], size.width)
} else if PREVIEW.wrap == PreviewWrap::Yes {
Self::line_height(&String::from_utf8_lossy(&buf), area.width)
Self::line_height(&String::from_utf8_lossy(&buf), size.width)
} else {
1
};
buf.clear();
if i > skip + area.height as usize {
if i > skip + size.height as usize {
break;
}
}
if skip > 0 && i < skip + area.height as usize {
return Err(PeekError::Exceed(i.saturating_sub(area.height as _)));
if skip > 0 && i < skip + size.height as usize {
return Err(PeekError::Exceed(i.saturating_sub(size.height as _)));
}
Ok(if plain {

View file

@ -1,4 +1,4 @@
use mlua::{ExternalError, ExternalResult, ObjectLike, Table};
use mlua::{ExternalError, ExternalResult, IntoLua, ObjectLike, Table};
use tokio::runtime::Handle;
use yazi_config::LAYOUT;
@ -20,16 +20,20 @@ pub async fn fetch(name: &str, files: Vec<yazi_shared::fs::File>) -> mlua::Resul
return Err("unloaded plugin".into_lua_err());
};
let files = files.into_iter().filter_map(|f| File::cast(&lua, f).ok()).collect::<Vec<_>>();
if files.is_empty() {
return Err("no files".into_lua_err());
let files =
lua.create_sequence_from(files.into_iter().filter_map(|f| File::cast(&lua, f).ok()))?;
if files.raw_len() == 0 {
return Ok(1);
}
plugin.raw_set("skip", 0)?;
plugin.raw_set("area", Rect::from(LAYOUT.get().preview))?;
plugin.raw_set("files", files)?;
Handle::current().block_on(plugin.call_async_method("fetch", ()))
Handle::current().block_on(plugin.call_async_method(
"fetch",
lua.create_table_from([
("area", Rect::from(LAYOUT.get().preview).into_lua(&lua)?),
("files", files.into_lua(&lua)?),
])?,
))
})
.await
.into_lua_err()?

View file

@ -6,7 +6,7 @@ use crate::runtime::Runtime;
pub fn slim_lua(name: &str) -> mlua::Result<Lua> {
let lua = Lua::new();
lua.set_named_registry_value("rt", Runtime::new(name))?;
crate::Config::new(&lua).install_preview()?;
crate::config::Config::new(&lua).install_preview()?.install_plugin()?;
// Base
let globals = lua.globals();

View file

@ -1,3 +1,3 @@
#![allow(clippy::module_inception)]
yazi_macro::mod_flat!(entry fetch isolate peek preload seek);
yazi_macro::mod_flat!(entry fetch isolate peek preload seek spot);

View file

@ -18,9 +18,9 @@ pub fn peek(
skip: usize,
) -> CancellationToken {
let ct = CancellationToken::new();
let (ct1, ct2) = (ct.clone(), ct.clone());
let name = cmd.name.to_owned();
let (ct1, ct2) = (ct.clone(), ct.clone());
tokio::task::spawn_blocking(move || {
let future = async {
LOADER.ensure(&name).await.into_lua_err()?;
@ -43,10 +43,12 @@ pub fn peek(
return Err("unloaded plugin".into_lua_err());
};
plugin.raw_set("file", File::cast(&lua, file)?)?;
plugin.raw_set("_mime", mime)?;
plugin.raw_set("mime", mime)?;
plugin.raw_set("skip", skip)?;
plugin.raw_set("area", Rect::from(LAYOUT.get().preview))?;
plugin.raw_set("window", Window::default())?;
// TODO: remove this as it's not safe in async context,
// there may be race conditions between the `window` and `area`
plugin.raw_set("window", Window::get())?;
if ct2.is_cancelled() { Ok(()) } else { plugin.call_async_method("peek", ()).await }
};
@ -71,10 +73,12 @@ pub fn peek(
pub fn peek_sync(cmd: &Cmd, file: yazi_shared::fs::File, mime: Cow<'static, str>, skip: usize) {
let cb: PluginCallback = Box::new(move |_, plugin| {
plugin.raw_set("file", File::cast(&LUA, file)?)?;
plugin.raw_set("_mime", mime)?;
plugin.raw_set("mime", mime)?;
plugin.raw_set("skip", skip)?;
plugin.raw_set("area", Rect::from(LAYOUT.get().preview))?;
plugin.raw_set("window", Window::default())?;
// TODO: remove this as it's not safe in async context,
// there may be race conditions between the `window` and `area`
plugin.raw_set("window", Window::get())?;
plugin.call_method("peek", ())
});

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