feat: terminal drag and drop protocol (#3999)

This commit is contained in:
三咲雅 misaki masa 2026-05-25 10:30:04 +08:00 committed by GitHub
parent f7fea2b19a
commit cf8b54179e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 474 additions and 43 deletions

37
Cargo.lock generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
View 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() }
}

View file

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

View file

@ -1 +1 @@
yazi_macro::mod_flat!(event keyboard modifiers mouse);
yazi_macro::mod_flat!(dnd event keyboard modifiers mouse);

View file

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

View 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(())
}
}

View file

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

View file

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

View 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(())
}
}

View file

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

View file

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