diff --git a/Cargo.lock b/Cargo.lock index 363c3c2b..2496ee06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,9 +239,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "av-scenechange" @@ -489,9 +489,9 @@ checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "by_address" @@ -2047,9 +2047,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.98" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -3590,9 +3590,9 @@ dependencies = [ [[package]] name = "russh" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fba341d1fce5e094392f088499c5808d2a60d3a658f58fe480c26ac77b1c7dd3" +checksum = "f67013f080c226e5a34db1c71f2567f44d95a6300005bb6cd4e2c8fe3c326d1b" dependencies = [ "aes", "bitflags 2.11.1", @@ -4934,9 +4934,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -4947,9 +4947,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.71" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -4957,9 +4957,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4967,9 +4967,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -4980,9 +4980,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -6159,6 +6159,7 @@ dependencies = [ "ratatui", "rustix 1.1.4", "signal-hook 0.4.4", + "strum 0.28.0", "thiserror 2.0.18", "tokio", "windows-sys 0.61.2", diff --git a/Cargo.toml b/Cargo.toml index d7ff624f..516ac590 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ percent-encoding = "2.3.2" rand = { version = "0.10.1", default-features = false, features = [ "std", "sys_rng" ] } ratatui = { version = "0.30.0", default-features = false, features = [ "layout-cache", "serde", "underline-color", "unstable-rendered-line-info", "unstable-widget-ref" ] } regex = "1.12.3" -russh = { version = "0.61.0", default-features = false, features = [ "ring", "rsa" ] } +russh = { version = "0.61.1", default-features = false, features = [ "ring", "rsa" ] } scopeguard = "1.2.0" serde = { version = "1.0.228", features = [ "derive" ] } serde_json = "1.0.150" diff --git a/cspell.json b/cspell.json index 289f3fd0..ae46bfbf 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"language":"en","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"],"version":"0.2","flagWords":[]} \ No newline at end of file +{"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 diff --git a/yazi-actor/src/tasks/inspect.rs b/yazi-actor/src/tasks/inspect.rs index 22150212..a7824471 100644 --- a/yazi-actor/src/tasks/inspect.rs +++ b/yazi-actor/src/tasks/inspect.rs @@ -4,12 +4,11 @@ use anyhow::Result; use scopeguard::defer; use tokio::{io::{AsyncReadExt, stdin}, select, sync::mpsc, time}; use yazi_binding::Permit; -use yazi_term::TERM; use yazi_macro::{succ, writef}; use yazi_parser::VoidForm; use yazi_scheduler::AppProxy; use yazi_shared::data::Data; -use yazi_term::{YIELD_TO_SUBPROCESS, sequence::EraseScreen}; +use yazi_term::{TERM, YIELD_TO_SUBPROCESS, sequence::EraseScreen}; use yazi_tty::TTY; use crate::{Actor, Ctx}; diff --git a/yazi-fm/src/dispatcher.rs b/yazi-fm/src/dispatcher.rs index 53b525dd..34e4057d 100644 --- a/yazi-fm/src/dispatcher.rs +++ b/yazi-fm/src/dispatcher.rs @@ -4,9 +4,10 @@ use anyhow::Result; use tracing::warn; use yazi_actor::Ctx; use yazi_config::keymap::Key; -use yazi_macro::{act, emit}; +use yazi_macro::{act, emit, writef}; use yazi_shared::event::{ActionCow, Event, NEED_RENDER}; -use yazi_term::event::{Event as TermEvent, KeyEvent, MouseEvent}; +use yazi_term::{event::{Event as TermEvent, KeyEvent, MouseEvent}, sequence::{ConfirmDrag, ConfirmDrop, FinishDrop, PresentDrag, PresentDragIcon, StartDrag, StartDrop}}; +use yazi_tty::TTY; use yazi_widgets::input::InputMode; use crate::{Executor, Router, app::App}; @@ -29,6 +30,53 @@ 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(()) + } }; if let Err(e) = &result { diff --git a/yazi-term/Cargo.toml b/yazi-term/Cargo.toml index 7d2a506d..f3977dbe 100644 --- a/yazi-term/Cargo.toml +++ b/yazi-term/Cargo.toml @@ -24,6 +24,7 @@ bitflags = { workspace = true } futures = { workspace = true } parking_lot = { workspace = true } ratatui = { workspace = true } +strum = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } diff --git a/yazi-term/src/error.rs b/yazi-term/src/error.rs index 3d376b36..8d9cf715 100644 --- a/yazi-term/src/error.rs +++ b/yazi-term/src/error.rs @@ -1,4 +1,4 @@ -use std::str; +use std::{num, str}; pub type Result = std::result::Result; @@ -19,3 +19,7 @@ pub enum ParseError { impl From for ParseError { fn from(_: str::Utf8Error) -> Self { Self::Invalid } } + +impl From for ParseError { + fn from(_: num::ParseIntError) -> Self { Self::Invalid } +} diff --git a/yazi-term/src/event/dnd.rs b/yazi-term/src/event/dnd.rs new file mode 100644 index 00000000..763140ac --- /dev/null +++ b/yazi-term/src/event/dnd.rs @@ -0,0 +1,121 @@ +use std::str::SplitWhitespace; + +use strum::FromRepr; + +use crate::parser::StateOsc72; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DndEvent { + // Drag + DragOffer(DndDragOffer), + DragAccept(DndDragAccept), + DragChange(DndDragChange), + DragLand, + DragEnd(DndDragEnd), + DragSend(DndDragSend), + + // Drop + DropEnter(DndDropEnter), + DropLeave, + DropReady(DndDropReady), + DropData(DndDropData), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DndDragOffer { + pub x: u32, + pub y: u32, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DndDragAccept { + pub idx: u8, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DndDragChange { + pub op: DndOp, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DndDragEnd { + pub canceled: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DndDragSend { + pub idx: u8, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DndDropEnter { + pub x: u32, + pub y: u32, + pub op: DndOp, + pub mimes: DndMimeList, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DndDropReady { + pub x: u32, + pub y: u32, + pub op: DndOp, + pub mimes: DndMimeList, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DndDropData { + pub idx: u8, + pub payload: Vec, +} + +impl DndEvent { + pub(crate) fn from_state(s: StateOsc72) -> Option { + Some(match s.r#type.unwrap_or_default() { + // Drag + b'o' => Self::DragOffer(DndDragOffer { x: s.x?.try_into().ok()?, y: s.y?.try_into().ok()? }), + b'e' if s.x? == 1 => Self::DragAccept(DndDragAccept { idx: s.y?.try_into().ok()? }), + b'e' if s.x? == 2 => Self::DragChange(DndDragChange { op: DndOp::from_repr(s.op?)? }), + 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()? }), + + // Drop + b'm' if s.x == Some(-1) && s.y == Some(-1) => Self::DropLeave, + b'm' => Self::DropEnter(DndDropEnter { + x: s.x?.try_into().ok()?, + y: s.y?.try_into().ok()?, + op: DndOp::from_repr(s.op?)?, + mimes: DndMimeList::new(s.payload)?, + }), + b'M' => Self::DropReady(DndDropReady { + x: s.x?.try_into().ok()?, + y: s.y?.try_into().ok()?, + 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 }), + + _ => return None, + }) + } +} + +// --- Operation +#[derive(Clone, Copy, Debug, Eq, FromRepr, PartialEq)] +#[repr(u8)] +pub enum DndOp { + Copy = 1, + Move = 2, + Either = 3, +} + +// --- MIME list +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DndMimeList(String); + +impl DndMimeList { + pub fn new(b: Vec) -> Option { Some(Self(String::from_utf8(b).ok()?)) } + + pub fn iter(&self) -> SplitWhitespace<'_> { self.0.split_whitespace() } +} diff --git a/yazi-term/src/event/event.rs b/yazi-term/src/event/event.rs index f83e0f05..0df95509 100644 --- a/yazi-term/src/event/event.rs +++ b/yazi-term/src/event/event.rs @@ -1,4 +1,4 @@ -use crate::{Dimension, event::{KeyEvent, MouseEvent}}; +use crate::{Dimension, event::{DndEvent, KeyEvent, MouseEvent}}; #[derive(Clone, Debug, Eq, PartialEq)] pub enum Event { @@ -8,4 +8,5 @@ pub enum Event { FocusIn, FocusOut, Paste(String), + Dnd(DndEvent), } diff --git a/yazi-term/src/event/mod.rs b/yazi-term/src/event/mod.rs index 6a7bbaf9..247ca94b 100644 --- a/yazi-term/src/event/mod.rs +++ b/yazi-term/src/event/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(event keyboard modifiers mouse); +yazi_macro::mod_flat!(dnd event keyboard modifiers mouse); diff --git a/yazi-term/src/parser/mod.rs b/yazi-term/src/parser/mod.rs index cc7f5c59..72b7cbef 100644 --- a/yazi-term/src/parser/mod.rs +++ b/yazi-term/src/parser/mod.rs @@ -1,4 +1,4 @@ -yazi_macro::mod_flat!(csi ground parser state); +yazi_macro::mod_flat!(csi ground osc parser state); #[cfg(windows)] yazi_macro::mod_flat!(windows); diff --git a/yazi-term/src/parser/osc.rs b/yazi-term/src/parser/osc.rs new file mode 100644 index 00000000..268dae2b --- /dev/null +++ b/yazi-term/src/parser/osc.rs @@ -0,0 +1,40 @@ +use crate::{ParseError, Result, parser::{Parser, State}}; + +impl Parser { + pub(super) fn parse_osc72(&mut self) -> Result<()> { + debug_assert!(self.seq.starts_with(b"\x1b]72;")); + debug_assert!(self.seq.ends_with(b"\x1b\\")); + + let mut it = self.seq[5..self.seq.len() - 2].splitn(2, |&b| b == b';'); + let meta = str::from_utf8(it.next().ok_or(ParseError::Invalid)?)?; + let payload = it.next().unwrap_or(&[]); + + let State::Osc72(state) = &mut self.state else { unreachable!() }; + state.has_more = false; // reset has_more for this new sequence + + for part in meta.split(':') { + match part.split_once('=').ok_or(ParseError::Invalid)? { + ("t", v) if let Some(b) = v.bytes().next() => state.r#type = Some(b), + ("x", v) => state.x = Some(v.parse()?), + ("y", v) => state.y = Some(v.parse()?), + ("o", v) => state.op = Some(v.parse()?), + ("m", "1") => state.has_more = true, + _ => {} + } + } + + // For `t=r`, the presence of a payload indicates more data is coming, + // even if `m=1` is not set. + if state.r#type == Some(b'r') && !payload.is_empty() { + state.has_more = true; + } + + // Limit payload size to 1MiB to prevent potential DoS + if state.payload.len() + payload.len() > 1 << 20 { + return Err(ParseError::Invalid); + } + + state.payload.extend(payload); + Ok(()) + } +} diff --git a/yazi-term/src/parser/parser.rs b/yazi-term/src/parser/parser.rs index 826a3c94..79f2a6f2 100644 --- a/yazi-term/src/parser/parser.rs +++ b/yazi-term/src/parser/parser.rs @@ -1,13 +1,13 @@ -use std::{collections::VecDeque, num::NonZeroU8, str}; +use std::{collections::VecDeque, mem, num::NonZeroU8, str}; use yazi_shim::utf8_char_width; use super::state::State; -use crate::event::{Event, KeyCode, KeyEvent, Modifiers}; +use crate::event::{DndEvent, Event, KeyCode, KeyEvent, Modifiers}; #[derive(Debug)] pub struct Parser { - state: State, + pub(super) state: State, pub(super) seq: Vec, pub(crate) events: VecDeque, } @@ -30,7 +30,7 @@ impl Parser { } fn step(&mut self, b: u8) { - match self.state { + match &self.state { State::Ground => self.on_ground(b), State::Esc => self.on_esc(b), State::EscO => self.on_esco(b), @@ -38,9 +38,10 @@ impl Parser { State::NormalMouse => self.on_normal_mouse(b), State::BracketedPaste => self.on_bracketed_paste(b), State::Osc | State::OscSt => self.on_osc(b), + State::Osc72(_) => self.on_osc72(b), State::Dcs | State::DcsSt => self.on_dcs(b), - State::Utf8(n) => self.on_utf8(b, n), - State::AltUtf8(n) => self.on_alt_utf8(b, n), + State::Utf8(n) => self.on_utf8(b, *n), + State::AltUtf8(n) => self.on_alt_utf8(b, *n), } } @@ -51,9 +52,12 @@ impl Parser { /// been seen but no follow-up bytes arrived), this emits a bare /// [`KeyCode::Escape`] event and resets to [`State::Ground`]. pub fn flush(&mut self) { - if self.state == State::Esc { - self.emit_key(KeyCode::Escape); + match &self.state { + State::Esc => self.emit_key(KeyCode::Escape), + State::Osc72(s) if s.has_more => return, + _ => {} } + self.reset(); } @@ -193,8 +197,11 @@ impl Parser { fn on_osc(&mut self, b: u8) { self.seq.push(b); - match (self.state, b) { + match (&self.state, b) { (State::Osc, b'\x07') => self.reset(), // BEL — OSC complete (discard) + (State::Osc, _) if self.seq.starts_with(b"\x1b]72;") => { + self.state = State::Osc72(Default::default()); + } (State::Osc, b'\x1B') => self.state = State::OscSt, (State::Osc, _) => {} // keep accumulating (State::OscSt, b'\\') => self.reset(), // ST (`\x1B\\`) — OSC complete (discard) @@ -204,10 +211,34 @@ impl Parser { } } + fn on_osc72(&mut self, b: u8) { + self.seq.push(b); + + if !self.seq.ends_with(b"\x1b\\") { + return; + } else if self.parse_osc72().is_err() { + return self.reset(); + } + + match mem::take(&mut self.state) { + State::Osc72(s) if s.has_more => { + self.seq.clear(); + self.state = State::Osc72(s); + } + State::Osc72(s) => { + if let Some(e) = DndEvent::from_state(s) { + self.emit(Event::Dnd(e)); + } + self.reset(); + } + _ => unreachable!(), + } + } + fn on_dcs(&mut self, b: u8) { self.seq.push(b); - match (self.state, b) { + match (&self.state, b) { (State::Dcs, b'\x1B') => self.state = State::DcsSt, (State::Dcs, _) => {} (State::DcsSt, b'\\') => self.reset(), // ST — DCS complete (discard) diff --git a/yazi-term/src/parser/state.rs b/yazi-term/src/parser/state.rs index 49780ccb..b2934f53 100644 --- a/yazi-term/src/parser/state.rs +++ b/yazi-term/src/parser/state.rs @@ -1,8 +1,9 @@ use std::num::NonZeroU8; -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Default, PartialEq)] pub(crate) enum State { /// Normal ground state. + #[default] Ground, /// Saw `\x1B`, waiting for the next byte. Esc, @@ -16,6 +17,8 @@ pub(crate) enum State { BracketedPaste, /// Inside an OSC sequence (`\x1B]` … BEL or ST). Osc, + /// Inside an OSC 72 (DnD) sequence (`\x1B]72;` … ST). + Osc72(StateOsc72), /// Inside OSC, just saw `\x1B` (potential start of ST = `\x1B\\`). OscSt, /// Inside a DCS sequence (`\x1BP` … ST). @@ -27,3 +30,13 @@ pub(crate) enum State { /// `\x1B` + mid-UTF-8 character: `n` continuation bytes still needed. AltUtf8(NonZeroU8), } + +#[derive(Debug, Default, PartialEq)] +pub(crate) struct StateOsc72 { + pub(crate) r#type: Option, + pub(crate) x: Option, + pub(crate) y: Option, + pub(crate) op: Option, + pub(crate) payload: Vec, + pub(crate) has_more: bool, +} diff --git a/yazi-term/src/sequence/dnd.rs b/yazi-term/src/sequence/dnd.rs new file mode 100644 index 00000000..57f31e68 --- /dev/null +++ b/yazi-term/src/sequence/dnd.rs @@ -0,0 +1,170 @@ +use std::{fmt::{self, Display}, str}; + +use base64::{Engine, engine::general_purpose}; + +/// Enable drag support: `OSC 72 ; t=o:x=1 ; machine id ST` +pub struct EnableDrag<'a>(pub &'a str); + +impl Display for EnableDrag<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "\x1b]72;t=o:x=1;{}\x1b\\", self.0) + } +} + +/// Enable drop support: `OSC 72 ; t=a ; MIME list ST` +pub struct EnableDrop<'a>(pub &'a [&'a str]); + +impl Display for EnableDrop<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "\x1b]72;t=a;{}\x1b\\", ListDndMimes(self.0)) + } +} + +/// Disable drag support: `OSC 72 ; t=o:x=2 ST` +pub struct DisableDrag; + +impl Display for DisableDrag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b]72;t=o:x=2\x1b\\") } +} + +/// Disable drop support: `OSC 72 ; t=A ST` +pub struct DisableDrop; + +impl Display for DisableDrop { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b]72;t=A\x1b\\") } +} + +/// 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]), +} + +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)), + } + } +} + +/// Confirm dropped data: `OSC 72 ; t=m:o=O ; MIME list ST` +pub enum ConfirmDrop<'a> { + Reject, + Copy(&'a [&'a str]), + Move(&'a [&'a str]), +} + +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)), + } + } +} + +/// Start dragging: `OSC 72 ; t=P:x=-1 ST` +pub struct StartDrag; + +impl Display for StartDrag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b]72;t=P:x=-1\x1b\\") } +} + +/// Start requesting dropped data: `OSC 72 ; t=r:x=idx ST` +pub struct StartDrop(pub u8); + +impl Display for StartDrop { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "\x1b]72;t=r:x={}\x1b\\", self.0) + } +} + +/// Present drag data: `OSC 72 ; t=p:x=idx ; base64 encoded data ST` +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 chunks = b64.len().div_ceil(4096); + + for (i, chunk) in b64.chunks(4096).enumerate() { + let s = unsafe { str::from_utf8_unchecked(chunk) }; + if i == 0 { + write!(f, "\x1b]72;t=p:x={}:m={};{s}\x1b\\", self.0, (chunks > 1) as u8)?; + } else { + write!(f, "\x1b]72;m={};{s}\x1b\\", (i + 1 < chunks) as u8)?; + } + } + + write!(f, "\x1b]72;t=p:x={}\x1b\\", self.0) + } +} + +/// Present drag icon data: +/// `OSC 72 ; t=p:x=-1:y=fmt:X=width:Y=height:o=opacity ; base64 payload ST` +pub struct PresentDragIcon<'a> { + pub format: u8, + pub opacity: u16, + pub width: u32, + pub height: u32, + pub payload: &'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 chunks = b64.len().div_ceil(4096); + + for (i, chunk) in b64.chunks(4096).enumerate() { + let s = unsafe { str::from_utf8_unchecked(chunk) }; + if i == 0 { + write!( + f, + "\x1b]72;t=p:x=-1:y={}:X={}:Y={}:o={}:m={};{s}\x1b\\", + self.format, + self.width, + self.height, + self.opacity, + (chunks > 1) as u8 + )?; + } else { + write!(f, "\x1b]72;m={};{s}\x1b\\", (i + 1 < chunks) as u8)?; + } + } + + Ok(()) + } +} + +/// Finish requesting dropped data: `OSC 72 ; t=r:o=operation ST` +#[derive(Clone, Copy)] +pub enum FinishDrop { + Copy = 1, + Move = 2, +} + +impl Display for FinishDrop { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "\x1b]72;t=r:o={}\x1b\\", *self as u8) + } +} + +/// Write MIME types separated by spaces. +struct ListDndMimes<'a>(&'a [&'a str]); + +impl Display for ListDndMimes<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (i, &m) in self.0.iter().enumerate() { + if i != 0 { + write!(f, " ")?; + } + write!(f, "{m}")?; + } + Ok(()) + } +} diff --git a/yazi-term/src/sequence/mod.rs b/yazi-term/src/sequence/mod.rs index 5d166474..401188ba 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 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); diff --git a/yazi-tui/src/raterm.rs b/yazi-tui/src/raterm.rs index 1fd82217..3b528821 100644 --- a/yazi-tui/src/raterm.rs +++ b/yazi-tui/src/raterm.rs @@ -6,7 +6,7 @@ use yazi_config::YAZI; use yazi_emulator::{Emulator, Mux, TMUX}; use yazi_macro::writef; use yazi_shim::cell::SyncCell; -use yazi_term::{TERM, event::{Event, KeyEventKind}, sequence::{DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, EnableFocusChange, EnableMouseCapture, EnterAlternateScreen, If, LeaveAlternateScreen, PopKeyboardFlags, PushKeyboardFlags, RequestCursorBlink, RequestCursorStyle, RequestDA1, RequestKeyboardFlags, RestoreBackground, RestoreCursorStyle, SetBackground, SetTitle, ShowCursor}, stream::EventStream}; +use yazi_term::{TERM, event::{Event, KeyEventKind}, sequence::{DisableBracketedPaste, DisableDrag, DisableDrop, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, EnableDrag, EnableDrop, EnableFocusChange, EnableMouseCapture, EnterAlternateScreen, If, LeaveAlternateScreen, PopKeyboardFlags, PushKeyboardFlags, RequestCursorBlink, RequestCursorStyle, RequestDA1, RequestKeyboardFlags, RestoreBackground, RestoreCursorStyle, SetBackground, SetTitle, ShowCursor}, stream::EventStream}; use yazi_tty::{TTY, TtyWriter}; use crate::{RatermBackend, RatermOption, RatermState}; @@ -42,10 +42,12 @@ impl Raterm { let opt = RatermOption::default(); writef!( TTY.writer(), - "{}{RequestCursorStyle}{RequestCursorBlink}{RequestKeyboardFlags}{RequestDA1}{}{}{EnableBracketedPaste}{EnableFocusChange}{}", + "{}{RequestCursorStyle}{RequestCursorBlink}{RequestKeyboardFlags}{RequestDA1}{}{}{EnableBracketedPaste}{EnableFocusChange}{}{}{}", If(!TMUX.get(), EnterAlternateScreen), If(TMUX.get(), EnterAlternateScreen), SetBackground(&opt.bg), + EnableDrag(""), + EnableDrop(&["text/uri-list"]), If(opt.mouse, EnableMouseCapture), )?; @@ -80,7 +82,7 @@ impl Raterm { _ = writef!( TTY.writer(), - "{}{}{}{}{}{DisableFocusChange}{DisableBracketedPaste}{LeaveAlternateScreen}{ShowCursor}", + "{}{DisableDrop}{DisableDrag}{}{}{}{}{DisableFocusChange}{DisableBracketedPaste}{LeaveAlternateScreen}{ShowCursor}", If(state.mouse, DisableMouseCapture), If(state.bg, RestoreBackground), If(state.csi_u, PopKeyboardFlags),