mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 14:51:03 +00:00
feat: terminal drag and drop protocol (#3999)
This commit is contained in:
parent
f7fea2b19a
commit
cf8b54179e
17 changed files with 474 additions and 43 deletions
37
Cargo.lock
generated
37
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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":[]}
|
||||
{"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"}
|
||||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::str;
|
||||
use std::{num, str};
|
||||
|
||||
pub type Result<T, E = ParseError> = std::result::Result<T, E>;
|
||||
|
||||
|
|
@ -19,3 +19,7 @@ pub enum ParseError {
|
|||
impl From<str::Utf8Error> for ParseError {
|
||||
fn from(_: str::Utf8Error) -> Self { Self::Invalid }
|
||||
}
|
||||
|
||||
impl From<num::ParseIntError> for ParseError {
|
||||
fn from(_: num::ParseIntError) -> Self { Self::Invalid }
|
||||
}
|
||||
|
|
|
|||
121
yazi-term/src/event/dnd.rs
Normal file
121
yazi-term/src/event/dnd.rs
Normal file
|
|
@ -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<u8>,
|
||||
}
|
||||
|
||||
impl DndEvent {
|
||||
pub(crate) fn from_state(s: StateOsc72) -> Option<Self> {
|
||||
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<u8>) -> Option<Self> { Some(Self(String::from_utf8(b).ok()?)) }
|
||||
|
||||
pub fn iter(&self) -> SplitWhitespace<'_> { self.0.split_whitespace() }
|
||||
}
|
||||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(event keyboard modifiers mouse);
|
||||
yazi_macro::mod_flat!(dnd event keyboard modifiers mouse);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
40
yazi-term/src/parser/osc.rs
Normal file
40
yazi-term/src/parser/osc.rs
Normal file
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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<u8>,
|
||||
pub(crate) events: VecDeque<Event>,
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<u8>,
|
||||
pub(crate) x: Option<i32>,
|
||||
pub(crate) y: Option<i32>,
|
||||
pub(crate) op: Option<u8>,
|
||||
pub(crate) payload: Vec<u8>,
|
||||
pub(crate) has_more: bool,
|
||||
}
|
||||
|
|
|
|||
170
yazi-term/src/sequence/dnd.rs
Normal file
170
yazi-term/src/sequence/dnd.rs
Normal file
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue