diff --git a/CHANGELOG.md b/CHANGELOG.md index ccb14fab..0ba1e102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): ### Added +- Drag and drop ([#4005]) - Bulk create ([#3793]) - Image preview with Überzug++ on Niri ([#3990]) @@ -1729,3 +1730,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3943]: https://github.com/sxyazi/yazi/pull/3943 [#3989]: https://github.com/sxyazi/yazi/pull/3989 [#3990]: https://github.com/sxyazi/yazi/pull/3990 +[#4005]: https://github.com/sxyazi/yazi/pull/4005 diff --git a/Cargo.lock b/Cargo.lock index 2496ee06..e2c3f2dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,9 +29,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" dependencies = [ "cipher", "cpubits", @@ -41,9 +41,9 @@ dependencies = [ [[package]] name = "aes-gcm" -version = "0.11.0-rc.3" +version = "0.11.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22c0c90bbe8d4f77c3ca9ddabe41a1f8382d6fc1f7cea89459d0f320371f972" +checksum = "da8c919c118108f144adecad74b425b804ad075580d605d9b33c2d6d1c62a2f8" dependencies = [ "aead", "aes", @@ -716,9 +716,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "compact_str" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" dependencies = [ "castaway", "cfg-if", @@ -2156,9 +2156,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ "libc", ] @@ -2201,9 +2201,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "loop9" @@ -2287,9 +2287,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "memmem" @@ -5685,6 +5685,7 @@ dependencies = [ "yazi-shared", "yazi-shim", "yazi-term", + "yazi-tty", "yazi-vfs", "yazi-widgets", ] @@ -5992,6 +5993,7 @@ dependencies = [ "libc", "mlua", "paste", + "percent-encoding", "ratatui", "serde_json", "tokio", @@ -6135,8 +6137,10 @@ name = "yazi-shim" version = "26.5.6" dependencies = [ "arc-swap", + "base64", "hashbrown 0.17.1", "mlua", + "percent-encoding", "ratatui", "serde", "thiserror 2.0.18", @@ -6264,18 +6268,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" dependencies = [ "proc-macro2", "quote", diff --git a/README.md b/README.md index 1cd2e097..831a8ae7 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Yazi (means "duck") is a terminal file manager written in Rust, based on non-blo - 💫 Vim-like input/pick/confirm/which/notify component, auto-completion for cd paths - 🏷️ Multi-Tab Support, Cross-directory selection, Scrollable Preview (for videos, PDFs, archives, code, directories, etc.) - 🔄 Bulk Rename/Create, Archive Extraction, Visual Mode, File Chooser, [Git Integration](https://github.com/yazi-rs/plugins/tree/main/git.yazi), [Mount Manager](https://github.com/yazi-rs/plugins/tree/main/mount.yazi) -- 🎨 Theme System, Mouse Support, Trash Bin, Custom Layouts, CSI u, OSC 52 +- 🎨 Theme System, Mouse Support, Drag and Drop, Trash Bin, Custom Layouts, CSI u, OSC 52 - ... and more! https://github.com/sxyazi/yazi/assets/17523360/92ff23fa-0cd5-4f04-b387-894c12265cc7 diff --git a/cspell.json b/cspell.json index ae46bfbf..41af766c 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike","sstr","pointee","writef","wakeup","nonblocking","sigwinch","timespec","termios","tcgetattr","tcsetattr","tcgetwinsize","rustix","codepoint","codepoints","Raterm","mimetypes"],"language":"en","version":"0.2"} \ No newline at end of file +{"version":"0.2","language":"en","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike","sstr","pointee","writef","wakeup","nonblocking","sigwinch","timespec","termios","tcgetattr","tcsetattr","tcgetwinsize","rustix","codepoint","codepoints","Raterm","mimetypes","Mimelist"]} \ No newline at end of file diff --git a/flake.lock b/flake.lock index d485f39e..052f6d36 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1776329215, - "narHash": "sha256-a8BYi3mzoJ/AcJP8UldOx8emoPRLeWqALZWu4ZvjPXw=", + "lastModified": 1779877693, + "narHash": "sha256-NOF9NAREhxr50bbBfVcVOq+ArCMSoe8dP79Pk2uyARk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b86751bc4085f48661017fa226dee99fab6c651b", + "rev": "4100e830e085863741bc69b156ec4ccd53ab5be0", "type": "github" }, "original": { @@ -48,11 +48,11 @@ ] }, "locked": { - "lastModified": 1776741231, - "narHash": "sha256-k9G98qzn+7npROUaks8VqCFm7cFtEG8ulQLBBo5lItg=", + "lastModified": 1779851998, + "narHash": "sha256-UkkMh3bX9QW4Luqkm98nUaOqKWrU6i65mUnph3WeSSw=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "02061303f7c4c964f7b4584dabd9e985b4cd442b", + "rev": "6cddd512fa2bf7231f098d3a2f92f6e4cff71e0a", "type": "github" }, "original": { diff --git a/nix/yazi-unwrapped.nix b/nix/yazi-unwrapped.nix index 3601b485..bba98fbf 100644 --- a/nix/yazi-unwrapped.nix +++ b/nix/yazi-unwrapped.nix @@ -28,9 +28,6 @@ rustPlatform.buildRustPackage (finalAttrs: { cargoLock = { lockFile = "${src}/Cargo.lock"; - #outputHashes = { - # "mlua-0.10.0" = "sha256-Xg6/jc+UP8tbJJ6x1sbAgt8ZHt051xEBBcjmikQqYlw="; - #}; }; env = { diff --git a/yazi-actor/src/app/dnd.rs b/yazi-actor/src/app/dnd.rs new file mode 100644 index 00000000..10f357a5 --- /dev/null +++ b/yazi-actor/src/app/dnd.rs @@ -0,0 +1,45 @@ +use anyhow::Result; +use mlua::{ObjectLike, Table}; +use tracing::error; +use yazi_actor::lives::Lives; +use yazi_binding::runtime_scope; +use yazi_macro::succ; +use yazi_parser::app::DndForm; +use yazi_plugin::LUA; +use yazi_shared::data::Data; + +use crate::{Actor, Ctx}; + +pub struct Dnd; + +impl Actor for Dnd { + type Form = DndForm; + + const NAME: &str = "dnd"; + + fn act(cx: &mut Ctx, form: Self::Form) -> Result { + let event = yazi_binding::DndEvent::from(form.event); + + let Some(size) = cx.term.as_ref().and_then(|t| t.size().ok()) else { succ!() }; + let area = yazi_binding::elements::Rect::from(size); + + let result = Lives::scope(cx.core, move || { + runtime_scope!(LUA, "root", { + let root = LUA.globals().raw_get::("Root")?.call_method::
("new", area)?; + + if event.is_drag() { + root.call_method::<()>("drag", event)?; + } else { + root.call_method::<()>("drop", event)?; + } + + Ok(()) + }) + }); + + if let Err(ref e) = result { + error!("{e}"); + } + succ!(result?); + } +} diff --git a/yazi-actor/src/app/mod.rs b/yazi-actor/src/app/mod.rs index 0b0a6086..b6c0bd1a 100644 --- a/yazi-actor/src/app/mod.rs +++ b/yazi-actor/src/app/mod.rs @@ -2,6 +2,7 @@ yazi_macro::mod_flat!( accept_payload bootstrap deprecate + dnd focus lua mouse diff --git a/yazi-actor/src/app/mouse.rs b/yazi-actor/src/app/mouse.rs index 7e121168..3692cf00 100644 --- a/yazi-actor/src/app/mouse.rs +++ b/yazi-actor/src/app/mouse.rs @@ -25,25 +25,25 @@ impl Actor for Mouse { let area = yazi_binding::elements::Rect::from(size); let result = Lives::scope(cx.core, move || { - let root = runtime_scope!(LUA, "root", { - LUA.globals().raw_get::
("Root")?.call_method::
("new", area) - })?; + runtime_scope!(LUA, "root", { + let root = LUA.globals().raw_get::
("Root")?.call_method::
("new", area)?; - match event.kind { - MouseEventKind::Down(_) => root.call_method("click", (event, false))?, - MouseEventKind::Up(_) => root.call_method("click", (event, true))?, + match event.kind { + MouseEventKind::Down(_) => root.call_method("click", (event, false))?, + MouseEventKind::Up(_) => root.call_method("click", (event, true))?, - MouseEventKind::ScrollDown => root.call_method("scroll", (event, 1))?, - MouseEventKind::ScrollUp => root.call_method("scroll", (event, -1))?, + MouseEventKind::ScrollDown => root.call_method("scroll", (event, 1))?, + MouseEventKind::ScrollUp => root.call_method("scroll", (event, -1))?, - MouseEventKind::ScrollRight => root.call_method("touch", (event, 1))?, - MouseEventKind::ScrollLeft => root.call_method("touch", (event, -1))?, + MouseEventKind::ScrollRight => root.call_method("touch", (event, 1))?, + MouseEventKind::ScrollLeft => root.call_method("touch", (event, -1))?, - MouseEventKind::Moved => root.call_method("move", event)?, - MouseEventKind::Drag(_) => root.call_method("drag", event)?, - } + MouseEventKind::Moved => root.call_method("move", event)?, + MouseEventKind::Drag(_) => root.call_method("drag", event)?, + } - Ok(()) + Ok(()) + }) }); if let Err(ref e) = result { diff --git a/yazi-actor/src/tasks/spawn.rs b/yazi-actor/src/tasks/spawn.rs index 0928fe3c..963bd757 100644 --- a/yazi-actor/src/tasks/spawn.rs +++ b/yazi-actor/src/tasks/spawn.rs @@ -15,6 +15,7 @@ impl Actor for Spawn { fn act(cx: &mut Ctx, form: Self::Form) -> Result { succ!(match form.opt { + TaskOpt::Cut(r#in) => cx.tasks.scheduler.file_cut(r#in), TaskOpt::Plugin(r#in) => cx.tasks.scheduler.plugin_entry(r#in), }) } diff --git a/yazi-adapter/src/drivers/iip.rs b/yazi-adapter/src/drivers/iip.rs index 8859626b..e8174e8f 100644 --- a/yazi-adapter/src/drivers/iip.rs +++ b/yazi-adapter/src/drivers/iip.rs @@ -1,4 +1,4 @@ -use std::{fmt::Write, io::Write as ioWrite, path::PathBuf}; +use std::{fmt::Write as _, io::Write as _, path::PathBuf}; use anyhow::Result; use base64::{Engine, engine::{Config, general_purpose::STANDARD}}; diff --git a/yazi-binding/Cargo.toml b/yazi-binding/Cargo.toml index 1eb4fcc1..ccd2385a 100644 --- a/yazi-binding/Cargo.toml +++ b/yazi-binding/Cargo.toml @@ -25,6 +25,7 @@ yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" } yazi-term = { path = "../yazi-term", version = "26.5.6" } +yazi-tty = { path = "../yazi-tty", version = "26.5.9" } yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" } yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" } diff --git a/yazi-binding/src/dnd.rs b/yazi-binding/src/dnd.rs new file mode 100644 index 00000000..6689acd8 --- /dev/null +++ b/yazi-binding/src/dnd.rs @@ -0,0 +1,55 @@ +use std::{mem, ops::Deref}; + +use mlua::{UserData, UserDataFields, Value}; +use yazi_shim::strum::IntoStr; +use yazi_term::event::{DndDropArrive, DndEvent as Inner}; + +use crate::{cached_field, cached_field_mut}; + +pub struct DndEvent { + inner: Inner, + + v_mimes: Option, + v_data: Option>, +} + +impl Deref for DndEvent { + type Target = Inner; + + fn deref(&self) -> &Self::Target { &self.inner } +} + +impl From for DndEvent { + fn from(inner: Inner) -> Self { Self { inner, v_mimes: None, v_data: None } } +} + +impl UserData for DndEvent { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("type", |_, me| Ok(me.inner.r#type())); + + fields.add_field_method_get("x", |_, me| Ok(me.inner.x())); + + fields.add_field_method_get("y", |_, me| Ok(me.inner.y())); + + fields.add_field_method_get("idx", |_, me| Ok(me.inner.idx())); + + fields.add_field_method_get("op", |_, me| Ok(me.inner.op().map(IntoStr::into_str))); + + cached_field!(fields, mimes, |lua, me| { + if let Some(mimes) = me.inner.mimes() { + lua.create_sequence_from(mimes.iter())?.into_lua(lua) + } else { + Ok(Value::Nil) + } + }); + + cached_field_mut!(fields, data, |lua, me| { + match &mut me.inner { + Inner::DropArrive(DndDropArrive { data, .. }) => { + lua.create_external_string(mem::take(data))?.into_lua(lua) + } + _ => Ok(Value::Nil), + } + }); + } +} diff --git a/yazi-binding/src/lib.rs b/yazi-binding/src/lib.rs index b1cfd06e..548e58aa 100644 --- a/yazi-binding/src/lib.rs +++ b/yazi-binding/src/lib.rs @@ -2,4 +2,4 @@ mod macros; yazi_macro::mod_pub!(config elements process theme); -yazi_macro::mod_flat!(access calculator cha chan chord_cow composer error fd file handle icon id image input iter layer mouse path permit range runtime scheme selector stage style url utils); +yazi_macro::mod_flat!(access calculator cha chan chord_cow composer dnd error fd file handle icon id image input iter layer mouse path permit range runtime scheme selector stage style tty url utils); diff --git a/yazi-binding/src/mouse.rs b/yazi-binding/src/mouse.rs index bc959bf5..8d837b7d 100644 --- a/yazi-binding/src/mouse.rs +++ b/yazi-binding/src/mouse.rs @@ -18,6 +18,7 @@ impl From for MouseEvent { impl UserData for MouseEvent { fn add_fields>(fields: &mut F) { + fields.add_field("type", "legacy"); fields.add_field_method_get("x", |_, me| Ok(me.column)); fields.add_field_method_get("y", |_, me| Ok(me.row)); fields.add_field_method_get("is_left", |_, me| { diff --git a/yazi-binding/src/tty.rs b/yazi-binding/src/tty.rs new file mode 100644 index 00000000..39f52e74 --- /dev/null +++ b/yazi-binding/src/tty.rs @@ -0,0 +1,74 @@ +use std::io::Write; + +use mlua::{BorrowedBytes, ExternalError, IntoLuaMulti, Lua, MultiValue, Table, UserData, UserDataMethods}; +use yazi_shim::mlua::{ByteString, LuaTableExt}; +use yazi_term::sequence::{ConfirmDrag, ConfirmDrop, FinishDrop, PresentDrag, PresentDragIcon, StartDrag, StartDrop}; +use yazi_tty::TTY; + +use crate::Error; + +pub struct Tty; + +impl Tty { + fn queue(lua: &Lua, kind: &[u8], t: &Table) -> mlua::Result { + let mut w = TTY.writer(); + + let result = match kind { + b"ConfirmDrag" => { + let it = t.raw_get::
("mimes")?.sequence_iter::(lua).flatten(); + write!(w, "{}", match &*t.raw_get::("type")? { + b"copy" => ConfirmDrag::Copy(it), + b"move" => ConfirmDrag::Move(it), + b"either" => ConfirmDrag::Either(it), + _ => return Err("invalid ConfirmDrag type".into_lua_err()), + }) + } + b"ConfirmDrop" => { + let it = t.raw_get::
("mimes")?.sequence_iter::(lua).flatten(); + write!(w, "{}", match &*t.raw_get::("type")? { + b"reject" => ConfirmDrop::Reject, + b"copy" => ConfirmDrop::Copy(it), + b"move" => ConfirmDrop::Move(it), + _ => return Err("invalid ConfirmDrop type".into_lua_err()), + }) + } + b"StartDrag" => write!(w, "{StartDrag}"), + b"StartDrop" => write!(w, "{}", StartDrop(t.raw_get("idx")?)), + b"PresentDrag" => { + write!(w, "{}", PresentDrag(t.raw_get("idx")?, &t.raw_get::("data")?)) + } + b"PresentDragIcon" => { + write!(w, "{}", PresentDragIcon { + format: t.raw_get("format")?, + opacity: t.raw_get("opacity")?, + width: t.raw_get("width")?, + height: t.raw_get("height")?, + data: &t.raw_get::("data")?, + }) + } + b"FinishDrop" => match &*t.raw_get::("type")? { + b"copy" => write!(w, "{}", FinishDrop::Copy), + b"move" => write!(w, "{}", FinishDrop::Move), + _ => return Err("invalid FinishDrop type".into_lua_err()), + }, + _ => return Err("invalid sequence kind".into_lua_err()), + }; + + match result { + Ok(()) => true.into_lua_multi(&lua), + Err(e) => (false, Error::Io(e)).into_lua_multi(&lua), + } + } +} + +impl UserData for Tty { + fn add_methods>(methods: &mut M) { + methods + .add_method("queue", |lua, _, (kind, t): (BorrowedBytes, Table)| Self::queue(lua, &kind, &t)); + + methods.add_method("flush", |lua, _, ()| match TTY.writer().flush() { + Ok(()) => true.into_lua_multi(lua), + Err(e) => (false, Error::Io(e)).into_lua_multi(lua), + }); + } +} diff --git a/yazi-core/src/tasks/file.rs b/yazi-core/src/tasks/file.rs index a737e4d3..c956c76e 100644 --- a/yazi-core/src/tasks/file.rs +++ b/yazi-core/src/tasks/file.rs @@ -1,5 +1,6 @@ use indexmap::IndexSet; use tracing::debug; +use yazi_scheduler::file::FileInCut; use yazi_shared::url::{UrlBuf, UrlBufCov, UrlLike}; use super::Tasks; @@ -17,7 +18,7 @@ impl Tasks { if force && *u == to { debug!("file_cut: same file, skip {to:?}"); } else { - self.scheduler.file_cut(u.0.clone(), to, force); + self.scheduler.file_cut(FileInCut::new(u.0.clone(), to, force)); } } } diff --git a/yazi-core/src/tasks/option.rs b/yazi-core/src/tasks/option.rs index 30269059..fe861cd7 100644 --- a/yazi-core/src/tasks/option.rs +++ b/yazi-core/src/tasks/option.rs @@ -1,10 +1,12 @@ use std::borrow::Cow; -use yazi_scheduler::{TaskIn, plugin::PluginInEntry}; +use yazi_scheduler::{TaskIn, file::FileInCut, plugin::PluginInEntry}; use yazi_shared::{Id, SStr}; #[derive(Clone, Debug)] pub enum TaskOpt { + Cut(FileInCut), + Plugin(PluginInEntry), } @@ -13,12 +15,16 @@ impl TaskIn for TaskOpt { fn id(&self) -> Id { match self { + Self::Cut(r#in) => r#in.id(), + Self::Plugin(r#in) => r#in.id(), } } fn set_id(&mut self, id: Id) -> &mut Self { match self { + Self::Cut(r#in) => _ = r#in.set_id(id), + Self::Plugin(r#in) => _ = r#in.set_id(id), } self @@ -26,12 +32,16 @@ impl TaskIn for TaskOpt { fn title(&self) -> Cow<'_, str> { match self { + Self::Cut(r#in) => r#in.title(), + Self::Plugin(r#in) => r#in.title(), } } fn set_title(&mut self, title: impl Into) -> &mut Self { match self { + Self::Cut(r#in) => _ = r#in.set_title(title), + Self::Plugin(r#in) => _ = r#in.set_title(title), } self diff --git a/yazi-fm/src/dispatcher.rs b/yazi-fm/src/dispatcher.rs index 34e4057d..b25fd32e 100644 --- a/yazi-fm/src/dispatcher.rs +++ b/yazi-fm/src/dispatcher.rs @@ -4,10 +4,9 @@ use anyhow::Result; use tracing::warn; use yazi_actor::Ctx; use yazi_config::keymap::Key; -use yazi_macro::{act, emit, writef}; +use yazi_macro::{act, emit}; use yazi_shared::event::{ActionCow, Event, NEED_RENDER}; -use yazi_term::{event::{Event as TermEvent, KeyEvent, MouseEvent}, sequence::{ConfirmDrag, ConfirmDrop, FinishDrop, PresentDrag, PresentDragIcon, StartDrag, StartDrop}}; -use yazi_tty::TTY; +use yazi_term::event::{DndEvent, Event as TermEvent, KeyEvent, MouseEvent}; use yazi_widgets::input::InputMode; use crate::{Executor, Router, app::App}; @@ -30,53 +29,7 @@ impl<'a> Dispatcher<'a> { Event::Term(TermEvent::FocusIn) => self.dispatch_focus(), Event::Term(TermEvent::FocusOut) => Ok(()), Event::Term(TermEvent::Paste(str)) => self.dispatch_paste(str), - Event::Term(TermEvent::Dnd(dnd)) => { - match dnd { - yazi_term::event::DndEvent::DragOffer(event) => { - tracing::debug!("DragOffer: {event:?}"); - // writef!( - // TTY.writer(), - // "{}{}{}{StartDrag}", - // ConfirmDrag::Either(&["text/uri-list"]), - // PresentDrag(0, b"file:///tmp/cspell.json\r\n"), - // PresentDragIcon { format: 0, width: 6, height: 1, opacity: 0, - // payload: b"drag" }, ) - // .ok(); - } - yazi_term::event::DndEvent::DragAccept(event) => { - tracing::debug!("DragAccept: {event:?}"); - } - yazi_term::event::DndEvent::DragChange(event) => { - tracing::debug!("DragChange: {event:?}"); - } - yazi_term::event::DndEvent::DragLand => { - tracing::debug!("DragLand"); - } - yazi_term::event::DndEvent::DragEnd(event) => { - tracing::debug!("DragEnd: {event:?}"); - } - yazi_term::event::DndEvent::DragSend(event) => { - tracing::debug!("DragSend: {event:?}"); - } - - yazi_term::event::DndEvent::DropEnter(event) => { - tracing::debug!("DropEnter: {event:?}"); - writef!(TTY.writer(), "{}", ConfirmDrop::Copy(&["text/uri-list"])).ok(); - } - yazi_term::event::DndEvent::DropLeave => { - tracing::debug!("DropLeave"); - } - yazi_term::event::DndEvent::DropReady(event) => { - tracing::debug!("DropReady: {event:?}"); - writef!(TTY.writer(), "{}", StartDrop(1)).ok(); - } - yazi_term::event::DndEvent::DropData(event) => { - tracing::debug!("DropData: {event:?}"); - writef!(TTY.writer(), "{}", FinishDrop::Copy).ok(); - } - } - Ok(()) - } + Event::Term(TermEvent::Dnd(dnd)) => self.dispatch_dnd(dnd), }; if let Err(e) = &result { @@ -145,4 +98,9 @@ impl<'a> Dispatcher<'a> { } Ok(()) } + + fn dispatch_dnd(&mut self, dnd: DndEvent) -> Result<()> { + let cx = &mut Ctx::active(&mut self.app.core, &mut self.app.term); + act!(app:dnd, cx, dnd).map(|_| ()) + } } diff --git a/yazi-parser/src/app/dnd.rs b/yazi-parser/src/app/dnd.rs new file mode 100644 index 00000000..b879ca51 --- /dev/null +++ b/yazi-parser/src/app/dnd.rs @@ -0,0 +1,19 @@ +use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; +use yazi_term::event::DndEvent; + +#[derive(Debug)] +pub struct DndForm { + pub event: DndEvent, +} + +impl From for DndForm { + fn from(event: DndEvent) -> Self { Self { event } } +} + +impl FromLua for DndForm { + fn from_lua(_: Value, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} + +impl IntoLua for DndForm { + fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} diff --git a/yazi-parser/src/app/mod.rs b/yazi-parser/src/app/mod.rs index 881d723f..095335e8 100644 --- a/yazi-parser/src/app/mod.rs +++ b/yazi-parser/src/app/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(deprecate lua mouse plugin quit reflow title update_progress); +yazi_macro::mod_flat!(deprecate dnd lua mouse plugin quit reflow title update_progress); diff --git a/yazi-parser/src/spark/spark.rs b/yazi-parser/src/spark/spark.rs index a2f6234f..48f2c080 100644 --- a/yazi-parser/src/spark/spark.rs +++ b/yazi-parser/src/spark/spark.rs @@ -11,6 +11,7 @@ pub enum Spark<'a> { AppAcceptPayload(yazi_dds::Payload<'a>), AppBootstrap(crate::VoidForm), AppDeprecate(crate::app::DeprecateForm), + AppDnd(crate::app::DndForm), AppFocus(crate::VoidForm), AppLua(crate::app::LuaForm), AppMouse(crate::app::MouseForm), @@ -199,6 +200,7 @@ impl<'a> IntoLua for Spark<'a> { Self::AppAcceptPayload(b) => b.into_lua(lua), Self::AppBootstrap(b) => b.into_lua(lua), Self::AppDeprecate(b) => b.into_lua(lua), + Self::AppDnd(b) => b.into_lua(lua), Self::AppFocus(b) => b.into_lua(lua), Self::AppLua(b) => b.into_lua(lua), Self::AppMouse(b) => b.into_lua(lua), @@ -376,6 +378,7 @@ try_from_spark!( // App try_from_spark!(crate::ArrowForm, mgr:arrow, mgr:tab_swap); try_from_spark!(crate::app::DeprecateForm, app:deprecate); +try_from_spark!(crate::app::DndForm, app:dnd); try_from_spark!(crate::app::LuaForm, app:lua); try_from_spark!(crate::app::MouseForm, app:mouse); try_from_spark!(crate::app::PluginForm, app:plugin, app:plugin_do); diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index 882a6de3..60cbf122 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -36,18 +36,19 @@ yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" } yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" } # External dependencies -ansi-to-tui = { workspace = true } -anyhow = { workspace = true } -futures = { workspace = true } -mlua = { workspace = true } -paste = { workspace = true } -ratatui = { workspace = true } -serde_json = { workspace = true } -tokio = { workspace = true } -tokio-stream = { workspace = true } -tracing = { workspace = true } -twox-hash = { workspace = true } -unicode-width = { workspace = true } +ansi-to-tui = { workspace = true } +anyhow = { workspace = true } +futures = { workspace = true } +mlua = { workspace = true } +paste = { workspace = true } +percent-encoding = { workspace = true } +ratatui = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +tracing = { workspace = true } +twox-hash = { workspace = true } +unicode-width = { workspace = true } [target."cfg(unix)".dependencies] libc = { workspace = true } diff --git a/yazi-plugin/preset/components/current.lua b/yazi-plugin/preset/components/current.lua index 74f73fe4..01ea28c6 100644 --- a/yazi-plugin/preset/components/current.lua +++ b/yazi-plugin/preset/components/current.lua @@ -1,5 +1,7 @@ Current = { _id = "current", + _dragging = false, + _dropping = false, } function Current:new(area, tab) @@ -24,6 +26,14 @@ function Current:empty() } end +function Current:dropping() + if Current._dropping then + return Tip:new(self._area, "Drop to move here…"):redraw() + else + return {} + end +end + function Current:reflow() return { self } end function Current:redraw() @@ -44,6 +54,7 @@ function Current:redraw() return { ui.List(left):area(self._area), ui.Text(right):area(self._area):align(ui.Align.RIGHT), + table.unpack(self:dropping()), } end @@ -62,3 +73,31 @@ end function Current:scroll(event, step) ya.emit("arrow", { step }) end function Current:touch(event, step) end + +function Current:drag(event) + if event.type == "offer" then + Current._dragging = require("dnd").offer_uri_list() + elseif event.type == "end" or event.type == "error" then + Current._dragging = false + end +end + +function Current:drop(event) + if Current._dragging then + return + elseif event.type == "enter" then + rt.tty:queue("ConfirmDrop", { type = "move", mimes = { "text/uri-list" } }) + elseif event.type == "ready" then + rt.tty:queue("StartDrop", { idx = 1 }) + elseif event.type == "arrive" then + rt.tty:queue("FinishDrop", { type = "move" }) + require("dnd").cut_uri_list(event.data) + end + rt.tty:flush() + + local d = event.type == "enter" + if Current._dropping ~= d then + Current._dropping = d + ui.render() + end +end diff --git a/yazi-plugin/preset/components/rail.lua b/yazi-plugin/preset/components/rail.lua index 40d938d8..305f9694 100644 --- a/yazi-plugin/preset/components/rail.lua +++ b/yazi-plugin/preset/components/rail.lua @@ -24,6 +24,10 @@ function Rail:scroll(event, step) end function Rail:touch(event, step) end function Rail:drag(event) + if event.type ~= "legacy" then + return + end + local c, x, parent, current, preview = self._chunks, 0, 0, 0, 0 if self._id == "rail-left" then x = math.min(event.x, c[2].right - 2) diff --git a/yazi-plugin/preset/components/root.lua b/yazi-plugin/preset/components/root.lua index 201d37fa..02e00858 100644 --- a/yazi-plugin/preset/components/root.lua +++ b/yazi-plugin/preset/components/root.lua @@ -1,6 +1,7 @@ Root = { _id = "root", _dragging = nil, + _dropping = nil, } function Root:new(area) @@ -50,7 +51,7 @@ end -- Mouse events function Root:click(event, up) - local c = Root._dragging or ya.child_at(ui.Rect { x = event.x, y = event.y }, self:reflow()) + local c = ya.child_at(ui.Rect { x = event.x, y = event.y }, self:reflow()) Root._dragging = not up and c or nil if tostring(cx.layer) == "mgr" then @@ -84,3 +85,16 @@ function Root:drag(event) local c = Root._dragging return c and c.drag and c:drag(event) end + +function Root:drop(event) + local d = Root._dropping + local c = event.x and ya.child_at(ui.Rect { x = event.x, y = event.y }, self:reflow()) or d + if d and d.drop and d._id ~= c._id then + d:drop { type = "leave" } + end + + Root._dropping = c + if tostring(cx.layer) == "mgr" then + return c and c.drop and c:drop(event) + end +end diff --git a/yazi-plugin/preset/components/tip.lua b/yazi-plugin/preset/components/tip.lua new file mode 100644 index 00000000..e4c0ed36 --- /dev/null +++ b/yazi-plugin/preset/components/tip.lua @@ -0,0 +1,29 @@ +Tip = { + _id = "tip", +} + +function Tip:new(area, text) + local me = setmetatable({ _area = area, _text = text }, { __index = self }) + me:layout() + return me +end + +function Tip:layout() + self._chunks = ui.Layout() + :direction(ui.Layout.VERTICAL) + :constraints({ + ui.Constraint.Fill(1), + ui.Constraint.Length(1), + ui.Constraint.Fill(1), + }) + :split(self._area) +end + +function Tip:reflow() return {} end + +function Tip:redraw() + return { + ui.Clear(self._chunks[2]), + ui.Text(self._text):area(self._chunks[2]):align(ui.Align.CENTER):fg("black"):bg("yellow"):bold(), + } +end diff --git a/yazi-plugin/preset/plugins/dnd.lua b/yazi-plugin/preset/plugins/dnd.lua new file mode 100644 index 00000000..039cf90b --- /dev/null +++ b/yazi-plugin/preset/plugins/dnd.lua @@ -0,0 +1,45 @@ +local M = {} + +function M.selected_uri_list() + local paths = {} + for _, u in pairs(cx.active.selected) do + paths[#paths + 1] = "file://" .. ya.percent_encode(tostring(u.path)) + end + if #paths == 0 and cx.active.current.hovered then + paths[1] = "file://" .. ya.percent_encode(tostring(cx.active.current.hovered.path)) + end + return paths +end + +function M.cut_uri_list(list) + for line in list:gmatch("[^\r\n]+") do + if line:sub(1, 7) ~= "file://" then + goto continue + end + + local from = Url(ya.percent_decode(line:sub(8))) + if from.name then + local to = cx.active.current.cwd:join(from.name) + ya.async(function() ya.task("cut", { from = from, to = to }):spawn() end) + end + + ::continue:: + end +end + +function M.offer_uri_list() + local list = M.selected_uri_list() + if #list == 0 then + return false + end + + local icon = string.format("%d selected file(s)", #list) + rt.tty:queue("ConfirmDrag", { type = "either", mimes = { "text/uri-list" } }) + rt.tty:queue("PresentDrag", { idx = 0, data = table.concat(list, "\r\n") }) + rt.tty:queue("PresentDragIcon", { format = 0, opacity = 0, width = 6, height = 4, data = icon }) + rt.tty:queue("StartDrag", {}) + rt.tty:flush() + return true +end + +return M diff --git a/yazi-plugin/src/runtime/runtime.rs b/yazi-plugin/src/runtime/runtime.rs index 757ec015..2e2cc163 100644 --- a/yazi-plugin/src/runtime/runtime.rs +++ b/yazi-plugin/src/runtime/runtime.rs @@ -1,5 +1,5 @@ use mlua::{IntoLua, Lua, LuaSerdeExt, Value}; -use yazi_binding::{Composer, ComposerGet, ComposerSet, SER_OPT, Url, config::{OpenRules, Opener}, elements::Wrap}; +use yazi_binding::{Composer, ComposerGet, ComposerSet, SER_OPT, Tty, Url, config::{OpenRules, Opener}, elements::Wrap}; use yazi_boot::ARGS; use yazi_config::YAZI; @@ -7,6 +7,7 @@ pub fn compose() -> Composer { fn get(lua: &Lua, key: &[u8]) -> mlua::Result { match key { b"args" => args().into_lua(lua)?, + b"tty" => Tty.into_lua(lua)?, b"term" => super::term().into_lua(lua)?, b"mgr" => mgr().into_lua(lua)?, b"open" => open().into_lua(lua)?, diff --git a/yazi-plugin/src/standard.rs b/yazi-plugin/src/standard.rs index 811b1673..0dbbb8e5 100644 --- a/yazi-plugin/src/standard.rs +++ b/yazi-plugin/src/standard.rs @@ -59,6 +59,7 @@ fn stage_1(lua: &Lua) -> Result<()> { lua.load(preset!("components/tab")).set_name("tab.lua").exec()?; lua.load(preset!("components/tabs")).set_name("tabs.lua").exec()?; lua.load(preset!("components/tasks")).set_name("tasks.lua").exec()?; + lua.load(preset!("components/tip")).set_name("tip.lua").exec()?; Ok(()) } diff --git a/yazi-plugin/src/utils/tasks.rs b/yazi-plugin/src/utils/tasks.rs index 302fe1b6..4c5c63e7 100644 --- a/yazi-plugin/src/utils/tasks.rs +++ b/yazi-plugin/src/utils/tasks.rs @@ -8,7 +8,10 @@ impl Utils { pub(super) fn task(lua: &Lua) -> mlua::Result { lua.create_function(|lua, (kind, value): (mlua::String, Value)| { Ok(TaskOpt(match &*kind.as_bytes() { + b"cut" => tasks::TaskOpt::Cut(<_>::from_lua(value, lua)?), + b"plugin" => tasks::TaskOpt::Plugin(<_>::from_lua(value, lua)?), + _ => Err(format!("unsupported spawn kind: {}", kind.display()).into_lua_err())?, })) }) diff --git a/yazi-plugin/src/utils/text.rs b/yazi-plugin/src/utils/text.rs index 8e3e8e95..f5522195 100644 --- a/yazi-plugin/src/utils/text.rs +++ b/yazi-plugin/src/utils/text.rs @@ -1,5 +1,8 @@ +use std::borrow::Cow; + use mlua::{Function, Lua}; use twox_hash::XxHash3_128; +use yazi_shim::RFC_3986; use yazi_widgets::CLIPBOARD; use super::Utils; @@ -33,4 +36,24 @@ impl Utils { } }) } + + pub(super) fn percent_decode(lua: &Lua) -> mlua::Result { + lua.create_function(|lua, s: mlua::String| { + let b = s.as_bytes(); + match percent_encoding::percent_decode(&b).into() { + Cow::Borrowed(_) => Ok(s), + Cow::Owned(b) => lua.create_external_string(b), + } + }) + } + + pub(super) fn percent_encode(lua: &Lua) -> mlua::Result { + lua.create_function(|lua, s: mlua::String| { + let b = s.as_bytes(); + match percent_encoding::percent_encode(&b, RFC_3986).into() { + Cow::Borrowed(_) => Ok(s), + Cow::Owned(s) => lua.create_external_string(s), + } + }) + } } diff --git a/yazi-plugin/src/utils/utils.rs b/yazi-plugin/src/utils/utils.rs index 10245716..3bcbb23a 100644 --- a/yazi-plugin/src/utils/utils.rs +++ b/yazi-plugin/src/utils/utils.rs @@ -66,6 +66,8 @@ pub fn compose( b"hash" => Utils::hash(lua)?, b"quote" => Utils::quote(lua)?, b"clipboard" => Utils::clipboard(lua)?, + b"percent_encode" => Utils::percent_encode(lua)?, + b"percent_decode" => Utils::percent_decode(lua)?, // Time b"time" => Utils::time(lua)?, diff --git a/yazi-runner/src/loader/loader.rs b/yazi-runner/src/loader/loader.rs index d5c99e18..5dc41e14 100644 --- a/yazi-runner/src/loader/loader.rs +++ b/yazi-runner/src/loader/loader.rs @@ -30,6 +30,7 @@ impl Default for Loader { ("archive".to_owned(), preset!("plugins/archive").into()), ("code".to_owned(), preset!("plugins/code").into()), ("dds".to_owned(), preset!("plugins/dds").into()), + ("dnd".to_owned(), preset!("plugins/dnd").into()), ("empty".to_owned(), preset!("plugins/empty").into()), ("extract".to_owned(), preset!("plugins/extract").into()), ("file".to_owned(), preset!("plugins/file").into()), @@ -71,6 +72,7 @@ impl Default for Loader { ("tab".to_owned(), [][..].into()), ("tabs".to_owned(), [][..].into()), ("tasks".to_owned(), [][..].into()), + ("tip".to_owned(), [][..].into()), // Reserved ("inline".to_owned(), [][..].into()), ]); diff --git a/yazi-scheduler/src/file/in.rs b/yazi-scheduler/src/file/in.rs index 09101ef8..2f6b486f 100644 --- a/yazi-scheduler/src/file/in.rs +++ b/yazi-scheduler/src/file/in.rs @@ -1,5 +1,6 @@ use std::{borrow::Cow, mem, path::PathBuf}; +use mlua::{ExternalError, FromLua, Lua, Value}; use tokio::sync::mpsc; use yazi_fs::cha::Cha; use yazi_shared::{Id, url::{UrlBuf, UrlLike}}; @@ -172,7 +173,7 @@ impl FileInCopy { // --- Cut #[derive(Clone, Debug)] -pub(crate) struct FileInCut { +pub struct FileInCut { pub(crate) id: Id, pub(crate) from: UrlBuf, pub(crate) to: UrlBuf, @@ -203,6 +204,19 @@ impl Drop for FileInCut { } impl FileInCut { + pub fn new(from: UrlBuf, to: UrlBuf, force: bool) -> Self { + Self { + follow: !from.scheme().covariant(to.scheme()), + id: Id::ZERO, + from, + to, + force, + cha: None, + retry: 0, + drop: None, + } + } + pub(super) fn into_link(mut self) -> FileInLink { FileInLink { id: self.id, @@ -222,6 +236,20 @@ impl FileInCut { } } +impl FromLua for FileInCut { + fn from_lua(value: Value, _: &Lua) -> mlua::Result { + let Value::Table(t) = value else { + return Err("constructing FileInCut from non-table value".into_lua_err()); + }; + + Ok(Self::new( + t.raw_get::("from")?.into(), + t.raw_get::("to")?.into(), + t.raw_get("force")?, + )) + } +} + // --- Link #[derive(Clone, Debug)] pub(crate) struct FileInLink { diff --git a/yazi-scheduler/src/plugin/in.rs b/yazi-scheduler/src/plugin/in.rs index 0d0dc423..371c60f4 100644 --- a/yazi-scheduler/src/plugin/in.rs +++ b/yazi-scheduler/src/plugin/in.rs @@ -72,9 +72,9 @@ impl TaskIn for PluginInEntry { } } -impl PluginInEntry { - pub(crate) fn into_job(self) -> EntryJob { - EntryJob { id: self.id, args: self.args, plugin: self.plugin } +impl From for EntryJob { + fn from(value: PluginInEntry) -> Self { + Self { id: value.id, args: value.args, plugin: value.plugin } } } diff --git a/yazi-scheduler/src/plugin/plugin.rs b/yazi-scheduler/src/plugin/plugin.rs index 43ee7f1b..876fd68e 100644 --- a/yazi-scheduler/src/plugin/plugin.rs +++ b/yazi-scheduler/src/plugin/plugin.rs @@ -20,7 +20,7 @@ impl Plugin { pub(crate) async fn entry(&self, task: PluginInEntry) -> Result<(), PluginOutEntry> { let id = task.id; - RUNNER.entry(task.into_job()).await?; + RUNNER.entry(task.into()).await?; Ok(self.ops.out(id, PluginOutEntry::Succ)) } } diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index f569a4bc..35bb3693 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -65,17 +65,16 @@ impl Scheduler { } } - pub fn file_cut(&self, from: UrlBuf, to: UrlBuf, force: bool) { - let follow = !from.scheme().covariant(to.scheme()); - let mut r#in = - FileInCut { id: Id::ZERO, from, to, force, cha: None, follow, retry: 0, drop: None }; + pub fn file_cut(&self, mut r#in: FileInCut) -> Id { + let id = self.add(&mut r#in, |t| t.id); - self.add(&mut r#in, |_| ()); if r#in.to.try_starts_with(&r#in.from).unwrap_or(false) && !r#in.to.covariant(&r#in.from) { self.ops.out(r#in.id, FileOutCut::Fail("Cannot cut directory into itself".to_owned())); } else { self.file.submit(r#in, LOW); } + + id } pub fn file_copy(&self, from: UrlBuf, to: UrlBuf, force: bool, follow: bool) { diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 7dc674df..cdc99fba 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -23,7 +23,7 @@ dyn-clone = { workspace = true } foldhash = { workspace = true } futures = { workspace = true } hashbrown = { workspace = true } -memchr = "2.8.0" +memchr = "2.8.1" ordered-float = { workspace = true } parking_lot = { workspace = true } paste = { workspace = true } diff --git a/yazi-shim/Cargo.toml b/yazi-shim/Cargo.toml index b6154dea..ad580071 100644 --- a/yazi-shim/Cargo.toml +++ b/yazi-shim/Cargo.toml @@ -20,15 +20,17 @@ vendored-lua = [ "mlua/vendored" ] yazi-macro = { path = "../yazi-macro", version = "26.5.6" } # External dependencies -arc-swap = { workspace = true } -hashbrown = { workspace = true } -mlua = { workspace = true } -ratatui = { workspace = true } -serde = { workspace = true } -thiserror = { workspace = true } -toml = { workspace = true } -twox-hash = { workspace = true } -unicode-width = { workspace = true } +arc-swap = { workspace = true } +base64 = { workspace = true } +hashbrown = { workspace = true } +mlua = { workspace = true } +percent-encoding = { workspace = true } +ratatui = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +toml = { workspace = true } +twox-hash = { workspace = true } +unicode-width = { workspace = true } [dependencies.unicode-segmentation] version = "1.13.2" diff --git a/yazi-shim/src/base64.rs b/yazi-shim/src/base64.rs new file mode 100644 index 00000000..c3f6dae0 --- /dev/null +++ b/yazi-shim/src/base64.rs @@ -0,0 +1,8 @@ +use base64::{alphabet::STANDARD, engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig}}; + +pub const BASE64_SANE: GeneralPurpose = GeneralPurpose::new( + &STANDARD, + GeneralPurposeConfig::new() + .with_encode_padding(false) + .with_decode_padding_mode(DecodePaddingMode::Indifferent), +); diff --git a/yazi-shim/src/lib.rs b/yazi-shim/src/lib.rs index ad4cb83b..4264d4e1 100644 --- a/yazi-shim/src/lib.rs +++ b/yazi-shim/src/lib.rs @@ -1,6 +1,6 @@ yazi_macro::mod_pub!(arc_swap cell mlua path ratatui serde strum toml vec); -yazi_macro::mod_flat!(twox utf8); +yazi_macro::mod_flat!(base64 percent_encoding twox utf8); #[cfg(windows)] yazi_macro::mod_flat!(win32); diff --git a/yazi-shim/src/mlua/mod.rs b/yazi-shim/src/mlua/mod.rs index ff9b6f21..65220acf 100644 --- a/yazi-shim/src/mlua/mod.rs +++ b/yazi-shim/src/mlua/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(traits); +yazi_macro::mod_flat!(sequence string traits); diff --git a/yazi-shim/src/mlua/sequence.rs b/yazi-shim/src/mlua/sequence.rs new file mode 100644 index 00000000..d6d0f108 --- /dev/null +++ b/yazi-shim/src/mlua/sequence.rs @@ -0,0 +1,49 @@ +use std::marker::PhantomData; + +use mlua::{FromLua, Integer, IntoLua, Lua, Table, ffi}; + +// mlua's `TableSequence<'_, V>` holds `table: &'a Table` and cannot be Clone. +// By owning both `Lua` and `Table` (both are reference-counted; Clone = two +// atomic increments) we get a Clone-able sequence iterator with zero heap +// allocation. +// +// `next()` acquires the Lua lock once per element (same as `raw_get`), but uses +// `lua_rawgeti` to skip the separate integer key push that `raw_get` requires. +// Holding the lock across all elements (as `TableSequence` does) is not +// possible here because `LuaGuard` is `pub(crate)` and cannot be stored in +// Clone types outside mlua. +#[derive(Clone)] +pub struct SequenceIter { + pub(super) lua: Lua, + pub(super) table: Table, + pub(super) index: Integer, + pub(super) _phantom: PhantomData, +} + +impl Iterator for SequenceIter { + type Item = mlua::Result; + + fn next(&mut self) -> Option> { + self.index += 1; + self.lua.exec_raw_lua(|lua| { + unsafe { + let state = lua.state(); + if ffi::lua_checkstack(state, 2) == 0 { + return Some(Err(mlua::Error::StackError)); + } + + if let Err(e) = <&Table as IntoLua>::push_into_stack(&self.table, lua) { + return Some(Err(e)); + }; + + let result = match ffi::lua_rawgeti(state, -1, self.index) { + ffi::LUA_TNIL => None, + _ => Some(::from_stack(-1, lua)), + }; + + ffi::lua_pop(state, 2); // table + value + result + } + }) + } +} diff --git a/yazi-shim/src/mlua/string.rs b/yazi-shim/src/mlua/string.rs new file mode 100644 index 00000000..861e9d84 --- /dev/null +++ b/yazi-shim/src/mlua/string.rs @@ -0,0 +1,26 @@ +use std::{fmt, ops::Deref}; + +use mlua::{FromLua, IntoLua, Lua, Value}; + +#[derive(Clone)] +pub struct ByteString(mlua::String); + +impl Deref for ByteString { + type Target = mlua::String; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl fmt::Display for ByteString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.display().fmt(f) } +} + +impl FromLua for ByteString { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + Ok(ByteString(mlua::String::from_lua(value, lua)?)) + } +} + +impl IntoLua for ByteString { + fn into_lua(self, lua: &Lua) -> mlua::Result { self.0.into_lua(lua) } +} diff --git a/yazi-shim/src/mlua/traits.rs b/yazi-shim/src/mlua/traits.rs index 992430b0..35b98e79 100644 --- a/yazi-shim/src/mlua/traits.rs +++ b/yazi-shim/src/mlua/traits.rs @@ -1,7 +1,10 @@ -use mlua::{ExternalError, Table, Value}; +use std::marker::PhantomData; -use crate::toml::DeserializeOverWith; +use mlua::{ExternalError, FromLua, Lua, Table, Value}; +use crate::{mlua::SequenceIter, toml::DeserializeOverWith}; + +// --- DeserializeOverLua pub trait DeserializeOverLua: DeserializeOverWith { fn deserialize_over_lua(self, table: &Table) -> mlua::Result { let de = mlua::serde::Deserializer::new(Value::Table(table.clone())); @@ -10,3 +13,19 @@ pub trait DeserializeOverLua: DeserializeOverWith { } impl DeserializeOverLua for T {} + +// --- IntoLua +pub trait LuaTableExt { + fn sequence_iter(&self, lua: &Lua) -> SequenceIter; +} + +impl LuaTableExt for Table { + fn sequence_iter(&self, lua: &Lua) -> SequenceIter { + SequenceIter { + lua: lua.clone(), + table: self.clone(), + index: 0, + _phantom: PhantomData, + } + } +} diff --git a/yazi-shim/src/percent_encoding.rs b/yazi-shim/src/percent_encoding.rs new file mode 100644 index 00000000..24e9ce54 --- /dev/null +++ b/yazi-shim/src/percent_encoding.rs @@ -0,0 +1,20 @@ +use percent_encoding::{AsciiSet, CONTROLS}; + +// RFC 3986 path component: encode everything that is not a safe path character. +// Safe chars: unreserved (A-Za-z0-9 -._~) + sub-delims (!$&'()*+,;=) + : @ / +pub const RFC_3986: &AsciiSet = &CONTROLS + .add(b' ') + .add(b'"') + .add(b'#') + .add(b'%') + .add(b'<') + .add(b'>') + .add(b'?') + .add(b'[') + .add(b'\\') + .add(b']') + .add(b'^') + .add(b'`') + .add(b'{') + .add(b'|') + .add(b'}'); diff --git a/yazi-term/src/event/dnd.rs b/yazi-term/src/event/dnd.rs index 763140ac..d3c81b6a 100644 --- a/yazi-term/src/event/dnd.rs +++ b/yazi-term/src/event/dnd.rs @@ -1,6 +1,8 @@ use std::str::SplitWhitespace; -use strum::FromRepr; +use base64::Engine; +use strum::{FromRepr, IntoStaticStr}; +use yazi_shim::BASE64_SANE; use crate::parser::StateOsc72; @@ -13,12 +15,14 @@ pub enum DndEvent { DragLand, DragEnd(DndDragEnd), DragSend(DndDragSend), + DragError(DndDragError), // Drop DropEnter(DndDropEnter), DropLeave, DropReady(DndDropReady), - DropData(DndDropData), + DropArrive(DndDropArrive), + DropError(DndDropError), } #[derive(Clone, Debug, Eq, PartialEq)] @@ -47,6 +51,13 @@ pub struct DndDragSend { pub idx: u8, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DndDragError { + pub idx: u8, + pub name: String, + pub desc: String, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct DndDropEnter { pub x: u32, @@ -64,12 +75,96 @@ pub struct DndDropReady { } #[derive(Clone, Debug, Eq, PartialEq)] -pub struct DndDropData { - pub idx: u8, - pub payload: Vec, +pub struct DndDropArrive { + pub idx: u8, + pub data: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DndDropError { + pub idx: u8, + pub name: String, + pub desc: String, } impl DndEvent { + pub fn r#type(&self) -> &'static str { + match self { + Self::DragOffer(_) => "offer", + Self::DragAccept(_) => "accept", + Self::DragChange(_) => "change", + Self::DragLand => "land", + Self::DragEnd(_) => "end", + Self::DragSend(_) => "send", + Self::DragError(_) => "error", + + Self::DropEnter(_) => "enter", + Self::DropLeave => "leave", + Self::DropReady(_) => "ready", + Self::DropArrive(_) => "arrive", + Self::DropError(_) => "error", + } + } + + pub fn x(&self) -> Option { + match self { + Self::DragOffer(e) => Some(e.x), + Self::DropEnter(e) => Some(e.x), + Self::DropReady(e) => Some(e.x), + _ => None, + } + } + + pub fn y(&self) -> Option { + match self { + Self::DragOffer(e) => Some(e.y), + Self::DropEnter(e) => Some(e.y), + Self::DropReady(e) => Some(e.y), + _ => None, + } + } + + pub fn idx(&self) -> Option { + match self { + Self::DragAccept(e) => Some(e.idx), + Self::DragSend(e) => Some(e.idx), + Self::DragError(e) => Some(e.idx), + Self::DropArrive(e) => Some(e.idx), + Self::DropError(e) => Some(e.idx), + _ => None, + } + } + + pub fn op(&self) -> Option { + match self { + Self::DragChange(e) => Some(e.op), + Self::DropEnter(e) => Some(e.op), + Self::DropReady(e) => Some(e.op), + _ => None, + } + } + + pub fn mimes(&self) -> Option<&DndMimeList> { + match self { + Self::DropEnter(e) => Some(&e.mimes), + Self::DropReady(e) => Some(&e.mimes), + _ => None, + } + } + + pub fn is_drag(&self) -> bool { + matches!( + self, + Self::DragOffer(_) + | Self::DragAccept(_) + | Self::DragChange(_) + | Self::DragLand + | Self::DragEnd(_) + | Self::DragSend(_) + | Self::DragError(_) + ) + } + pub(crate) fn from_state(s: StateOsc72) -> Option { Some(match s.r#type.unwrap_or_default() { // Drag @@ -79,6 +174,10 @@ impl DndEvent { b'e' if s.x? == 3 => Self::DragLand, b'e' if s.x? == 4 => Self::DragEnd(DndDragEnd { canceled: s.y? != 0 }), b'e' if s.x? == 5 => Self::DragSend(DndDragSend { idx: s.y?.try_into().ok()? }), + b'E' => { + let (name, desc) = parse_error(s.payload)?; + Self::DragError(DndDragError { idx: s.y?.try_into().ok()?, name, desc }) + } // Drop b'm' if s.x == Some(-1) && s.y == Some(-1) => Self::DropLeave, @@ -94,7 +193,14 @@ impl DndEvent { op: DndOp::from_repr(s.op?)?, mimes: DndMimeList::new(s.payload)?, }), - b'r' => Self::DropData(DndDropData { idx: s.x?.try_into().ok()?, payload: s.payload }), + b'r' => Self::DropArrive(DndDropArrive { + idx: s.x?.try_into().ok()?, + data: BASE64_SANE.decode(&s.payload).ok()?, + }), + b'R' => { + let (name, desc) = parse_error(s.payload)?; + Self::DropError(DndDropError { idx: s.x?.try_into().ok()?, name, desc }) + } _ => return None, }) @@ -102,7 +208,7 @@ impl DndEvent { } // --- Operation -#[derive(Clone, Copy, Debug, Eq, FromRepr, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, FromRepr, IntoStaticStr, PartialEq)] #[repr(u8)] pub enum DndOp { Copy = 1, @@ -119,3 +225,13 @@ impl DndMimeList { pub fn iter(&self) -> SplitWhitespace<'_> { self.0.split_whitespace() } } + +// --- Error payload parsing +fn parse_error(payload: Vec) -> Option<(String, String)> { + let s = String::from_utf8(payload).ok()?; + Some(match s.split_once(':') { + Some((name, desc)) => (name.to_owned(), desc.to_owned()), + None if s.is_empty() => return None, + None => (s, String::new()), + }) +} diff --git a/yazi-term/src/sequence/dnd.rs b/yazi-term/src/sequence/dnd.rs index 57f31e68..681cf612 100644 --- a/yazi-term/src/sequence/dnd.rs +++ b/yazi-term/src/sequence/dnd.rs @@ -1,6 +1,9 @@ use std::{fmt::{self, Display}, str}; -use base64::{Engine, engine::general_purpose}; +use base64::Engine; +use yazi_shim::BASE64_SANE; + +use super::traits::Mimelist; /// Enable drag support: `OSC 72 ; t=o:x=1 ; machine id ST` pub struct EnableDrag<'a>(pub &'a str); @@ -12,11 +15,11 @@ impl Display for EnableDrag<'_> { } /// Enable drop support: `OSC 72 ; t=a ; MIME list ST` -pub struct EnableDrop<'a>(pub &'a [&'a str]); +pub struct EnableDrop(pub M); -impl Display for EnableDrop<'_> { +impl Display for EnableDrop { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "\x1b]72;t=a;{}\x1b\\", ListDndMimes(self.0)) + write!(f, "\x1b]72;t=a;{}\x1b\\", ListDndMimes(self.0.clone())) } } @@ -35,35 +38,35 @@ impl Display for DisableDrop { } /// Confirm drag: `OSC 72 ; t=o:o=operation ST` -pub enum ConfirmDrag<'a> { - Copy(&'a [&'a str]), - Move(&'a [&'a str]), - Either(&'a [&'a str]), +pub enum ConfirmDrag { + Copy(M), + Move(M), + Either(M), } -impl Display for ConfirmDrag<'_> { +impl Display for ConfirmDrag { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Copy(mimes) => write!(f, "\x1b]72;t=o:o=1;{}\x1b\\", ListDndMimes(mimes)), - Self::Move(mimes) => write!(f, "\x1b]72;t=o:o=2;{}\x1b\\", ListDndMimes(mimes)), - Self::Either(mimes) => write!(f, "\x1b]72;t=o:o=3;{}\x1b\\", ListDndMimes(mimes)), + Self::Copy(mimes) => write!(f, "\x1b]72;t=o:o=1;{}\x1b\\", ListDndMimes(mimes.clone())), + Self::Move(mimes) => write!(f, "\x1b]72;t=o:o=2;{}\x1b\\", ListDndMimes(mimes.clone())), + Self::Either(mimes) => write!(f, "\x1b]72;t=o:o=3;{}\x1b\\", ListDndMimes(mimes.clone())), } } } /// Confirm dropped data: `OSC 72 ; t=m:o=O ; MIME list ST` -pub enum ConfirmDrop<'a> { +pub enum ConfirmDrop { Reject, - Copy(&'a [&'a str]), - Move(&'a [&'a str]), + Copy(M), + Move(M), } -impl Display for ConfirmDrop<'_> { +impl Display for ConfirmDrop { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Reject => write!(f, "\x1b]72;t=m:o=0\x1b\\"), - Self::Copy(mimes) => write!(f, "\x1b]72;t=m:o=1;{}\x1b\\", ListDndMimes(mimes)), - Self::Move(mimes) => write!(f, "\x1b]72;t=m:o=2;{}\x1b\\", ListDndMimes(mimes)), + Self::Copy(mimes) => write!(f, "\x1b]72;t=m:o=1;{}\x1b\\", ListDndMimes(mimes.clone())), + Self::Move(mimes) => write!(f, "\x1b]72;t=m:o=2;{}\x1b\\", ListDndMimes(mimes.clone())), } } } @@ -89,7 +92,7 @@ pub struct PresentDrag<'a>(pub u8, pub &'a [u8]); impl Display for PresentDrag<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let b64 = general_purpose::STANDARD_NO_PAD.encode(self.1).into_bytes(); + let b64 = BASE64_SANE.encode(self.1).into_bytes(); let chunks = b64.len().div_ceil(4096); for (i, chunk) in b64.chunks(4096).enumerate() { @@ -112,12 +115,12 @@ pub struct PresentDragIcon<'a> { pub opacity: u16, pub width: u32, pub height: u32, - pub payload: &'a [u8], + pub data: &'a [u8], } impl Display for PresentDragIcon<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let b64 = general_purpose::STANDARD_NO_PAD.encode(self.payload).into_bytes(); + let b64 = BASE64_SANE.encode(self.data).into_bytes(); let chunks = b64.len().div_ceil(4096); for (i, chunk) in b64.chunks(4096).enumerate() { @@ -155,11 +158,11 @@ impl Display for FinishDrop { } /// Write MIME types separated by spaces. -struct ListDndMimes<'a>(&'a [&'a str]); +struct ListDndMimes(pub M); -impl Display for ListDndMimes<'_> { +impl Display for ListDndMimes { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for (i, &m) in self.0.iter().enumerate() { + for (i, m) in self.0.clone().into_iter().enumerate() { if i != 0 { write!(f, " ")?; } diff --git a/yazi-term/src/sequence/mod.rs b/yazi-term/src/sequence/mod.rs index 401188ba..defec32c 100644 --- a/yazi-term/src/sequence/mod.rs +++ b/yazi-term/src/sequence/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(clipboard csi_u cursor dnd erase mode query r#if restore_background set_background style sync); +yazi_macro::mod_flat!(clipboard csi_u cursor dnd erase mode query r#if restore_background set_background style sync traits); diff --git a/yazi-term/src/sequence/traits.rs b/yazi-term/src/sequence/traits.rs new file mode 100644 index 00000000..9911d60b --- /dev/null +++ b/yazi-term/src/sequence/traits.rs @@ -0,0 +1,11 @@ +use std::fmt::Display; + +/// Types that can be iterated to produce a list of MIME types. +pub(super) trait Mimelist: IntoIterator + Clone {} + +impl Mimelist for T +where + T: IntoIterator + Clone, + T::Item: Display, +{ +} diff --git a/yazi-tty/src/writer.rs b/yazi-tty/src/writer.rs index ca997d63..e69d7f9f 100644 --- a/yazi-tty/src/writer.rs +++ b/yazi-tty/src/writer.rs @@ -1,4 +1,4 @@ -use std::{fmt::{self, Debug}, io::{self, BufWriter, Write}, ops::Deref}; +use std::{fmt::{self, Debug}, io::{self, BufWriter}, ops::Deref}; use parking_lot::Mutex; @@ -23,12 +23,6 @@ impl io::Write for TtyWriter<'_> { fn flush(&mut self) -> io::Result<()> { self.0.lock().flush() } } -impl fmt::Write for TtyWriter<'_> { - fn write_str(&mut self, s: &str) -> fmt::Result { - self.0.lock().write_all(s.as_bytes()).map_err(|_| fmt::Error) - } -} - #[cfg(unix)] impl std::os::fd::AsFd for TtyWriter<'_> { fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {