From ae5f84ee65f4d8c57d76dbde277b5bb62b10c6d8 Mon Sep 17 00:00:00 2001 From: UnnaturalTwilight <107954129+UnnaturalTwilight@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:38:02 -0400 Subject: [PATCH] feat: osc 5522 clipboard --- .gitignore | 1 + Cargo.lock | 1 + yazi-actor/Cargo.toml | 27 ++-- yazi-actor/src/app/clipboard.rs | 45 +++++++ yazi-actor/src/app/mod.rs | 1 + yazi-actor/src/mgr/copy.rs | 40 +++++- yazi-actor/src/mgr/paste.rs | 2 +- yazi-actor/src/tasks/spawn.rs | 1 + yazi-cli/src/env/env.rs | 1 + yazi-config/preset/keymap-default.toml | 1 + yazi-core/src/tasks/file.rs | 6 +- yazi-core/src/tasks/option.rs | 7 +- yazi-emulator/src/emulator.rs | 12 +- yazi-fm/src/dispatcher.rs | 15 ++- yazi-parser/src/app/clipboard.rs | 19 +++ yazi-parser/src/app/mod.rs | 2 +- yazi-parser/src/spark/spark.rs | 3 + yazi-plugin/preset/components/root.lua | 28 ++++ yazi-plugin/preset/plugins/clipboard.lua | 32 +++++ yazi-plugin/src/runtime/term.rs | 1 + yazi-plugin/src/utils/tasks.rs | 1 + yazi-runner/src/loader/loader.rs | 1 + yazi-scheduler/src/file/in.rs | 23 +++- yazi-scheduler/src/scheduler.rs | 8 +- yazi-shim/src/base64.rs | 7 + yazi-term/src/event/clipboard.rs | 163 +++++++++++++++++++++++ yazi-term/src/event/event.rs | 3 +- yazi-term/src/event/lua.rs | 34 ++++- yazi-term/src/event/mod.rs | 2 +- yazi-term/src/parser/osc.rs | 69 +++++++++- yazi-term/src/parser/parser.rs | 31 ++++- yazi-term/src/parser/state.rs | 27 ++++ yazi-tty/src/lua.rs | 17 ++- yazi-tty/src/sequence/clipboard.rs | 98 ++++++++++++++ yazi-tui/src/raterm.rs | 6 +- yazi-widgets/src/clipboard.rs | 46 +++++++ 36 files changed, 743 insertions(+), 38 deletions(-) create mode 100644 yazi-actor/src/app/clipboard.rs create mode 100644 yazi-parser/src/app/clipboard.rs create mode 100644 yazi-plugin/preset/plugins/clipboard.lua create mode 100644 yazi-term/src/event/clipboard.rs diff --git a/.gitignore b/.gitignore index 246b034d..dd4f815e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,6 @@ result-* .idea/ .vscode/ +.zed/ *.snap diff --git a/Cargo.lock b/Cargo.lock index cd76337b..5f7beb49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4745,6 +4745,7 @@ dependencies = [ "libc", "mlua", "paste", + "percent-encoding", "ratatui-core", "scopeguard", "tokio", diff --git a/yazi-actor/Cargo.toml b/yazi-actor/Cargo.toml index 8c05e59e..e4daff42 100644 --- a/yazi-actor/Cargo.toml +++ b/yazi-actor/Cargo.toml @@ -40,19 +40,20 @@ yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" } yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" } # External dependencies -anyhow = { workspace = true } -either = { workspace = true } -futures = { workspace = true } -hashbrown = { workspace = true } -indexmap = { workspace = true } -inventory = { workspace = true } -mlua = { workspace = true } -paste = { workspace = true } -ratatui-core = { workspace = true } -scopeguard = { workspace = true } -tokio = { workspace = true } -tokio-stream = { workspace = true } -tracing = { workspace = true } +anyhow = { workspace = true } +either = { workspace = true } +futures = { workspace = true } +hashbrown = { workspace = true } +indexmap = { workspace = true } +inventory = { workspace = true } +mlua = { workspace = true } +paste = { workspace = true } +ratatui-core = { workspace = true } +scopeguard = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +tracing = { workspace = true } +percent-encoding = { workspace = true } [target."cfg(unix)".dependencies] libc = { workspace = true } diff --git a/yazi-actor/src/app/clipboard.rs b/yazi-actor/src/app/clipboard.rs new file mode 100644 index 00000000..56b2f8ae --- /dev/null +++ b/yazi-actor/src/app/clipboard.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::ClipboardForm; +use yazi_plugin::LUA; +use yazi_shared::data::Data; + +use crate::{Actor, Ctx}; + +pub struct Clipboard; + +impl Actor for Clipboard { + type Form = ClipboardForm; + + const NAME: &str = "clipboard"; + + fn act(cx: &mut Ctx, form: Self::Form) -> Result { + 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 form.event.is_paste_offer() { + root.call_method::<()>("paste_offer", form.event)?; + } else if form.event.is_read() { + root.call_method::<()>("paste_data", form.event)?; + } else { + root.call_method::<()>("write_result", form.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 b6c0bd1a..c95c54c2 100644 --- a/yazi-actor/src/app/mod.rs +++ b/yazi-actor/src/app/mod.rs @@ -16,4 +16,5 @@ yazi_macro::mod_flat!( theme title update_progress + clipboard ); diff --git a/yazi-actor/src/mgr/copy.rs b/yazi-actor/src/mgr/copy.rs index 90e5beb2..b6ecdc54 100644 --- a/yazi-actor/src/mgr/copy.rs +++ b/yazi-actor/src/mgr/copy.rs @@ -2,7 +2,8 @@ use anyhow::{Result, bail}; use yazi_macro::{act, succ}; use yazi_parser::mgr::CopyForm; use yazi_shared::{data::Data, strand::ToStrand, url::UrlLike}; -use yazi_widgets::CLIPBOARD; +use yazi_shim::RFC_3986; +use yazi_widgets::{CLIPBOARD, ClipboardData}; use crate::{Actor, Ctx}; @@ -41,6 +42,16 @@ impl Actor for Copy { "name_without_ext" => { s.extend_from_slice(&form.separator.transform(&u.stem().unwrap_or_default())); } + "uri_list" => { + // Per the spec this should be CRLF line endings but everything i've tested on + // linux works with just LF + s.extend_from_slice(b"file://"); + s.extend_from_slice( + percent_encoding::percent_encode(&form.separator.transform(&u.to_strand()), RFC_3986) + .to_string() + .as_bytes(), + ); + } _ => bail!("Unknown copy type: {}", form.r#type), }; if it.peek().is_some() { @@ -53,7 +64,32 @@ impl Actor for Copy { s.extend_from_slice(&form.separator.transform(&cx.cwd().to_strand())); } - futures::executor::block_on(CLIPBOARD.set(s)); + if yazi_emulator::EMULATOR.osc_5522 { + let mut data = Vec::::new(); + match form.r#type.as_ref() { + "uri_list" => { + data.push(ClipboardData { + mime: b"text/uri-list".to_vec(), + payload: s.clone(), + alias: b"text/plain".to_vec(), + }); + #[cfg(target_os = "linux")] + // Because Thunar (and likely others) won't reconize `text/uri-list` + data.push(ClipboardData { + mime: b"x-special/gnome-copied-files".to_vec(), + payload: [b"copy\n".to_vec(), s].concat(), + alias: vec![], + }); + } + _ => { + data.push(ClipboardData { mime: b"text/plain".to_vec(), payload: s, alias: vec![] }); + } + } + + futures::executor::block_on(CLIPBOARD.write(data)); + } else { + futures::executor::block_on(CLIPBOARD.set(s)); + } succ!(); } } diff --git a/yazi-actor/src/mgr/paste.rs b/yazi-actor/src/mgr/paste.rs index 63037efe..3a1c2ceb 100644 --- a/yazi-actor/src/mgr/paste.rs +++ b/yazi-actor/src/mgr/paste.rs @@ -23,7 +23,7 @@ impl Actor for Paste { mgr.tabs.iter_mut().for_each(|t| _ = t.selected.remove_many(mgr.yanked.urls())); act!(mgr:unyank, cx) } else { - succ!(cx.core.tasks.file_copy(&mgr.yanked, dest, form.force, form.follow)); + succ!(cx.core.tasks.file_copy(&mgr.yanked, dest, form.force, Some(form.follow))); } } } diff --git a/yazi-actor/src/tasks/spawn.rs b/yazi-actor/src/tasks/spawn.rs index efa4b535..f725ac96 100644 --- a/yazi-actor/src/tasks/spawn.rs +++ b/yazi-actor/src/tasks/spawn.rs @@ -16,6 +16,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::Copy(r#in) => cx.tasks.scheduler.file_copy(r#in), TaskOpt::Plugin(r#in) => cx.tasks.scheduler.plugin_entry(r#in), }) diff --git a/yazi-cli/src/env/env.rs b/yazi-cli/src/env/env.rs index 1cc8e981..21c06fbb 100644 --- a/yazi-cli/src/env/env.rs +++ b/yazi-cli/src/env/env.rs @@ -112,6 +112,7 @@ impl Env { writeln!(s, " wl-copy/paste: {} / {}", Self::dep_version("wl-copy", "--version"), Self::dep_version("wl-paste", "--version"))?; writeln!(s, " xclip : {}", Self::dep_version("xclip", "-version"))?; writeln!(s, " xsel : {}", Self::dep_version("xsel", "--version"))?; + writeln!(s, " OSC 5522 : {:?}", yazi_emulator::EMULATOR.osc_5522)?; writeln!(s, "\nRoutine")?; writeln!(s, " `file -bL --mime-type`: {}", Self::file1_output())?; diff --git a/yazi-config/preset/keymap-default.toml b/yazi-config/preset/keymap-default.toml index db1fa4ff..8b8f35fa 100644 --- a/yazi-config/preset/keymap-default.toml +++ b/yazi-config/preset/keymap-default.toml @@ -101,6 +101,7 @@ keymap = [ { on = [ "c", "d" ], run = "copy dirname", desc = "Copy directory URL" }, { on = [ "c", "f" ], run = "copy filename", desc = "Copy filename" }, { on = [ "c", "n" ], run = "copy name_without_ext", desc = "Copy filename without extension" }, + { on = [ "c", "l" ], run = "copy uri_list", desc = "Copy URI list" }, # Filter { on = "f", run = "filter --smart", desc = "Filter files" }, diff --git a/yazi-core/src/tasks/file.rs b/yazi-core/src/tasks/file.rs index c92f9cc2..276ad051 100644 --- a/yazi-core/src/tasks/file.rs +++ b/yazi-core/src/tasks/file.rs @@ -1,5 +1,5 @@ use tracing::debug; -use yazi_scheduler::file::FileInCut; +use yazi_scheduler::file::{FileInCopy, FileInCut}; use yazi_shared::url::{UrlBuf, UrlLike}; use super::Tasks; @@ -22,7 +22,7 @@ impl Tasks { } } - pub fn file_copy(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: bool) { + pub fn file_copy(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: Option) { self.scheduler.behavior.reset(); for u in src.urls() { @@ -33,7 +33,7 @@ impl Tasks { if force && u == to { debug!("file_copy: same file, skip {to:?}"); } else { - self.scheduler.file_copy(u.clone(), to, force, follow); + self.scheduler.file_copy(FileInCopy::new(u.clone(), to, force, follow)); } } } diff --git a/yazi-core/src/tasks/option.rs b/yazi-core/src/tasks/option.rs index e3c14931..87070da8 100644 --- a/yazi-core/src/tasks/option.rs +++ b/yazi-core/src/tasks/option.rs @@ -1,13 +1,14 @@ use std::borrow::Cow; use yazi_macro::impl_data_any; -use yazi_scheduler::{TaskIn, file::FileInCut, plugin::PluginInEntry}; +use yazi_scheduler::{TaskIn, file::{FileInCopy, FileInCut}, plugin::PluginInEntry}; use yazi_shared::id::Id; use yazi_shim::SStr; #[derive(Clone, Debug)] pub enum TaskOpt { Cut(FileInCut), + Copy(FileInCopy), Plugin(PluginInEntry), } @@ -20,6 +21,7 @@ impl TaskIn for TaskOpt { fn id(&self) -> Id { match self { Self::Cut(r#in) => r#in.id(), + Self::Copy(r#in) => r#in.id(), Self::Plugin(r#in) => r#in.id(), } @@ -28,6 +30,7 @@ impl TaskIn for TaskOpt { fn set_id(&mut self, id: Id) -> &mut Self { match self { Self::Cut(r#in) => _ = r#in.set_id(id), + Self::Copy(r#in) => _ = r#in.set_id(id), Self::Plugin(r#in) => _ = r#in.set_id(id), } @@ -37,6 +40,7 @@ impl TaskIn for TaskOpt { fn title(&self) -> Cow<'_, str> { match self { Self::Cut(r#in) => r#in.title(), + Self::Copy(r#in) => r#in.title(), Self::Plugin(r#in) => r#in.title(), } @@ -45,6 +49,7 @@ impl TaskIn for TaskOpt { fn set_title(&mut self, title: impl Into) -> &mut Self { match self { Self::Cut(r#in) => _ = r#in.set_title(title), + Self::Copy(r#in) => _ = r#in.set_title(title), Self::Plugin(r#in) => _ = r#in.set_title(title), } diff --git a/yazi-emulator/src/emulator.rs b/yazi-emulator/src/emulator.rs index a87f0a13..72e5f5e9 100644 --- a/yazi-emulator/src/emulator.rs +++ b/yazi-emulator/src/emulator.rs @@ -9,7 +9,7 @@ use tracing::{debug, error, warn}; use yazi_macro::writef; use yazi_shim::cell::RoCell; use yazi_term::TERM; -use yazi_tty::{Handle, TTY, sequence::{HideCursor, If, KittyGraphicsQuery, MoveTo, RequestBgColor, RequestCellPixelSize, RequestDA1, RequestXtVersion, RestoreCursorPos, SaveCursorPos, SetFg, SetSgr, ShowCursor}}; +use yazi_tty::{Handle, TTY, sequence::{HideCursor, If, KittyGraphicsQuery, MoveTo, QueryOSC5522, RequestBgColor, RequestCellPixelSize, RequestDA1, RequestXtVersion, RestoreCursorPos, SaveCursorPos, SetFg, SetSgr, ShowCursor}}; use crate::{Brand, Mux, TMUX, Unknown}; @@ -22,6 +22,7 @@ pub struct Emulator { pub light: bool, pub csi_16t: (u16, u16), pub force_16t: bool, + pub osc_5522: bool, } impl Default for Emulator { @@ -32,6 +33,7 @@ impl Default for Emulator { light: false, csi_16t: (0, 0), force_16t: false, + osc_5522: false, } } } @@ -44,11 +46,12 @@ impl Emulator { let resort = Brand::from_env(); writef!( TTY.writer(), - "{SaveCursorPos}{}{}{}{}{}{RestoreCursorPos}", + "{SaveCursorPos}{}{}{}{}{}{}{RestoreCursorPos}", If(resort.is_none(), Mux::wrap(KittyGraphicsQuery)), Mux::wrap(RequestXtVersion), RequestCellPixelSize, RequestBgColor, + QueryOSC5522, Mux::wrap(RequestDA1), )?; @@ -65,12 +68,17 @@ impl Emulator { }; let csi_16t = Self::csi_16t(&resp).unwrap_or_default(); + + let osc_5522 = + ["\x1b[?5522;1$y", "\x1b[?5522;2$y", "\x1b[?5522;3$y"].iter().any(|s| resp.contains(s)); + Ok(Self { kind, version: Self::csi_gt_q(&resp).unwrap_or_default(), light: Self::light_bg(&resp).unwrap_or_default(), csi_16t, force_16t: Self::force_16t(csi_16t), + osc_5522, }) } diff --git a/yazi-fm/src/dispatcher.rs b/yazi-fm/src/dispatcher.rs index 63c7e0d5..96e742fb 100644 --- a/yazi-fm/src/dispatcher.rs +++ b/yazi-fm/src/dispatcher.rs @@ -5,7 +5,7 @@ use tracing::warn; use yazi_actor::Ctx; use yazi_macro::{act, emit}; use yazi_shared::event::{ActionCow, Event, NEED_RENDER}; -use yazi_term::event::{DndEvent, Event as TermEvent, KeyEvent, MouseEvent}; +use yazi_term::event::{ClipboardEvent, DndEvent, Event as TermEvent, KeyEvent, MouseEvent}; use yazi_widgets::input::InputMode; use crate::{Executor, Router, app::App}; @@ -29,6 +29,7 @@ impl<'a> Dispatcher<'a> { Event::Term(TermEvent::FocusOut) => Ok(()), Event::Term(TermEvent::Paste(str)) => self.dispatch_paste(str), Event::Term(TermEvent::Dnd(dnd)) => self.dispatch_dnd(dnd), + Event::Term(TermEvent::Clipboard(clip)) => self.dispatch_clipboard(clip), }; if let Err(e) = &result { @@ -101,4 +102,16 @@ impl<'a> Dispatcher<'a> { let cx = &mut Ctx::active(&mut self.app.core, &mut self.app.term); act!(app:dnd, cx, dnd).map(|_| ()) } + + fn dispatch_clipboard(&mut self, clip: ClipboardEvent) -> Result<()> { + if self.app.core.input.main.visible && clip.is_read() { + if let Some(text) = clip.text() { + self.dispatch_paste(text)?; + } + Ok(()) + } else { + let cx = &mut Ctx::active(&mut self.app.core, &mut self.app.term); + act!(app:clipboard, cx, clip).map(|_| ()) + } + } } diff --git a/yazi-parser/src/app/clipboard.rs b/yazi-parser/src/app/clipboard.rs new file mode 100644 index 00000000..d5e6fc51 --- /dev/null +++ b/yazi-parser/src/app/clipboard.rs @@ -0,0 +1,19 @@ +use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; +use yazi_term::event::ClipboardEvent; + +#[derive(Debug)] +pub struct ClipboardForm { + pub event: ClipboardEvent, +} + +impl From for ClipboardForm { + fn from(event: ClipboardEvent) -> Self { Self { event } } +} + +impl FromLua for ClipboardForm { + fn from_lua(_: Value, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} + +impl IntoLua for ClipboardForm { + 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 095335e8..a1adb43d 100644 --- a/yazi-parser/src/app/mod.rs +++ b/yazi-parser/src/app/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(deprecate dnd lua mouse plugin quit reflow title update_progress); +yazi_macro::mod_flat!(deprecate dnd lua mouse plugin quit reflow title update_progress clipboard); diff --git a/yazi-parser/src/spark/spark.rs b/yazi-parser/src/spark/spark.rs index 4f88962d..5c1bb13a 100644 --- a/yazi-parser/src/spark/spark.rs +++ b/yazi-parser/src/spark/spark.rs @@ -25,6 +25,7 @@ pub enum Spark<'a> { AppTheme(crate::VoidForm), AppTitle(crate::app::TitleForm), AppUpdateProgress(crate::app::UpdateProgressForm), + AppClipboard(crate::app::ClipboardForm), // Mgr Arrow(crate::ArrowForm), @@ -220,6 +221,7 @@ impl<'a> IntoLua for Spark<'a> { Self::AppTheme(b) => b.into_lua(lua), Self::AppTitle(b) => b.into_lua(lua), Self::AppUpdateProgress(b) => b.into_lua(lua), + Self::AppClipboard(b) => b.into_lua(lua), // Mgr Self::Arrow(b) => b.into_lua(lua), @@ -395,6 +397,7 @@ try_from_spark!(crate::app::QuitForm, app:quit, mgr:quit); try_from_spark!(crate::app::ReflowForm, app:reflow, app:resize, app:resume); try_from_spark!(crate::app::TitleForm, app:title); try_from_spark!(crate::app::UpdateProgressForm, app:update_progress); +try_from_spark!(crate::app::ClipboardForm, app:clipboard); try_from_spark!(crate::cmp::CloseForm, cmp:close); try_from_spark!(crate::cmp::ShowForm, cmp:show); try_from_spark!(crate::cmp::TriggerForm, cmp:trigger); diff --git a/yazi-plugin/preset/components/root.lua b/yazi-plugin/preset/components/root.lua index 466994a1..01453468 100644 --- a/yazi-plugin/preset/components/root.lua +++ b/yazi-plugin/preset/components/root.lua @@ -99,3 +99,31 @@ function Root:drop(event) return c and c.drop and c:drop(event) end end + +-- Clipboard events +function Root:paste_offer(event) + if event and event.pw then + -- No harm in asking for unavailable types + local mimetypes = "text/plain text/uri-list" + ya.dbg("Requesting ReadClipboard") + rt.tty:queue("ReadClipboard", { mimes = mimetypes, pw = event.pw, name = "Paste Event", primary = event.primary }) + rt.tty:flush() + end +end + +function Root:paste_data(event) + if event.data["text/uri-list"] ~= nil then + local list = event.data["text/uri-list"] + ya.dbg("Pasting URI list:", list) + require("clipboard").copy_uri_list(list) + end + -- TODO !!5522!! Suport non text formats + -- if event.data["image/png"] ~= nil then + -- local type = "image/png" + -- local data = event.data["image/png"] + -- ya.dbg("Pasting image/png:") + -- require("clipboard").paste_image(type, data) + -- end +end + +function Root:write_result(event) end diff --git a/yazi-plugin/preset/plugins/clipboard.lua b/yazi-plugin/preset/plugins/clipboard.lua new file mode 100644 index 00000000..32c9d732 --- /dev/null +++ b/yazi-plugin/preset/plugins/clipboard.lua @@ -0,0 +1,32 @@ +local M = {} + +function M.copy_uri_list(list) + cx.tasks.behavior:reset() + 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("copy", { from = from, to = to }):spawn() end) + end + + ::continue:: + end +end + +function M.paste_image(mime, data) + local type = mime:match("image/([^;]+)") + local dir = cx.active.current.cwd + local url = Url(dir .. "/pasted_image." .. type) + ya.async(function() + local file = fs.unique("file", url) + if file then + fs.write(file, data) + end + end) +end + +return M diff --git a/yazi-plugin/src/runtime/term.rs b/yazi-plugin/src/runtime/term.rs index 06e47e9d..e07ba439 100644 --- a/yazi-plugin/src/runtime/term.rs +++ b/yazi-plugin/src/runtime/term.rs @@ -7,6 +7,7 @@ pub(super) fn term() -> Composer { match key { b"light" => EMULATOR.light.into_lua(lua), b"cell_size" => cell_size(lua)?.into_lua(lua), + b"osc5522_clipboard" => EMULATOR.osc_5522.into_lua(lua), _ => Ok(Value::Nil), } } diff --git a/yazi-plugin/src/utils/tasks.rs b/yazi-plugin/src/utils/tasks.rs index fcf0d108..e1a0f181 100644 --- a/yazi-plugin/src/utils/tasks.rs +++ b/yazi-plugin/src/utils/tasks.rs @@ -9,6 +9,7 @@ impl Utils { lua.create_function(|lua, (kind, value): (LuaString, Value)| { Ok(TaskOpt(match &*kind.as_bytes() { b"cut" => tasks::TaskOpt::Cut(<_>::from_lua(value, lua)?), + b"copy" => tasks::TaskOpt::Copy(<_>::from_lua(value, lua)?), b"plugin" => tasks::TaskOpt::Plugin(<_>::from_lua(value, lua)?), diff --git a/yazi-runner/src/loader/loader.rs b/yazi-runner/src/loader/loader.rs index e4593b46..87d5d298 100644 --- a/yazi-runner/src/loader/loader.rs +++ b/yazi-runner/src/loader/loader.rs @@ -28,6 +28,7 @@ impl Default for Loader { let cache = HashMap::from_iter([ // Plugins ("archive".to_owned(), preset!("plugins/archive").into()), + ("clipboard".to_owned(), preset!("plugins/clipboard").into()), ("code".to_owned(), preset!("plugins/code").into()), ("dds".to_owned(), preset!("plugins/dds").into()), ("dnd".to_owned(), preset!("plugins/dnd").into()), diff --git a/yazi-scheduler/src/file/in.rs b/yazi-scheduler/src/file/in.rs index 17cd8e47..792a9f64 100644 --- a/yazi-scheduler/src/file/in.rs +++ b/yazi-scheduler/src/file/in.rs @@ -131,7 +131,7 @@ impl FileIn { // --- Copy #[derive(Clone, Debug)] -pub(crate) struct FileInCopy { +pub struct FileInCopy { pub(crate) id: Id, pub(crate) from: UrlBuf, pub(crate) to: UrlBuf, @@ -157,6 +157,18 @@ impl TaskIn for FileInCopy { } impl FileInCopy { + pub fn new(from: UrlBuf, to: UrlBuf, force: bool, follow: Option) -> Self { + Self { + follow: follow.unwrap_or(!from.auth().covariant(to.auth())), + id: Id::ZERO, + from, + to, + force, + cha: None, + retry: 0, + } + } + pub(super) fn into_link(self) -> FileInLink { FileInLink { id: self.id, @@ -171,6 +183,15 @@ impl FileInCopy { } } +impl FromLua for FileInCopy { + fn from_lua(value: Value, _: &Lua) -> mlua::Result { + let Value::Table(t) = value else { + return Err("constructing FileInCopy from non-table value".into_lua_err()); + }; + + Ok(Self::new(t.raw_get("from")?, t.raw_get("to")?, t.raw_get("force")?, None)) + } +} // --- Cut #[derive(Clone, Debug)] pub struct FileInCut { diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 67e5ac9b..03794437 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -77,16 +77,16 @@ impl Scheduler { id } - pub fn file_copy(&self, from: UrlBuf, to: UrlBuf, force: bool, follow: bool) { - let follow = follow || !from.auth().covariant(to.auth()); - let mut r#in = FileInCopy { id: Id::ZERO, from, to, force, cha: None, follow, retry: 0 }; + pub fn file_copy(&self, mut r#in: FileInCopy) -> 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, FileOutCopy::Fail("Cannot copy directory into itself".to_owned())); } else { self.file.submit(r#in, LOW); } + + id } pub fn file_link(&self, from: UrlBuf, to: UrlBuf, relative: bool, force: bool) { diff --git a/yazi-shim/src/base64.rs b/yazi-shim/src/base64.rs index c3f6dae0..90be5cb3 100644 --- a/yazi-shim/src/base64.rs +++ b/yazi-shim/src/base64.rs @@ -6,3 +6,10 @@ pub const BASE64_SANE: GeneralPurpose = GeneralPurpose::new( .with_encode_padding(false) .with_decode_padding_mode(DecodePaddingMode::Indifferent), ); + +pub const BASE64_PAD: GeneralPurpose = GeneralPurpose::new( + &STANDARD, + GeneralPurposeConfig::new() + .with_encode_padding(true) + .with_decode_padding_mode(DecodePaddingMode::Indifferent), +); diff --git a/yazi-term/src/event/clipboard.rs b/yazi-term/src/event/clipboard.rs new file mode 100644 index 00000000..bd00f4a8 --- /dev/null +++ b/yazi-term/src/event/clipboard.rs @@ -0,0 +1,163 @@ +use std::str::SplitWhitespace; + +use strum::{FromRepr, IntoStaticStr}; + +use crate::parser::{Osc5522Status, StateOsc5522}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ClipboardEvent { + ReadMimetypes(ClipboardPaste), + ReadData(ClipboardRead), + ReadError(ClipboardError), + WriteSuccess, + WriteError(ClipboardError), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClipboardPaste { + pub mimes: ClipboardMimeList, + pub primary: bool, + pub pw: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClipboardData { + pub mime: Vec, + pub data: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClipboardRead { + pub mimes: ClipboardMimeList, + pub primary: bool, + pub data: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClipboardError { + pub name: String, +} + +impl ClipboardEvent { + pub fn r#type(&self) -> &'static str { + match self { + Self::ReadMimetypes(_) => "read_mimetypes", + Self::ReadData(_) => "read_data", + Self::ReadError(_) => "read_error", + Self::WriteSuccess => "write_success", + Self::WriteError(_) => "write_error", + } + } + + pub fn mimes(&self) -> Option<&ClipboardMimeList> { + match self { + Self::ReadMimetypes(e) => Some(&e.mimes), + Self::ReadData(e) => Some(&e.mimes), + _ => None, + } + } + + pub fn primary(&self) -> Option { + match self { + Self::ReadMimetypes(e) => Some(e.primary), + _ => None, + } + } + + pub fn pw(&self) -> Option { + match self { + Self::ReadMimetypes(e) => Some(String::from_utf8_lossy(&e.pw).into_owned()), + _ => None, + } + } + + pub fn text(&self) -> Option { + match self { + Self::ReadData(e) if let Some(t) = e.data.iter().find(|e| e.mime == b"text/plain") => { + Some(String::from_utf8_lossy(&t.data).into_owned()) + } + _ => None, + } + } + + pub fn is_paste_offer(&self) -> bool { + match self { + Self::ReadMimetypes(_) => true, + _ => false, + } + } + + pub fn is_read(&self) -> bool { + match self { + Self::ReadError(_) | Self::ReadData(_) => true, + _ => false, + } + } + + pub(crate) fn from_state(s: StateOsc5522) -> Option { + Some(match s { + StateOsc5522 { read: true, status: Some(Osc5522Status::DONE), idx: 0, mime, .. } + if mime.first()? == b"." => + { + ClipboardEvent::ReadMimetypes(ClipboardPaste { + mimes: ClipboardMimeList::new(s.payload.first()?.to_owned())?, + primary: s.primary, + pw: s.pw, + }) + } + StateOsc5522 { read: true, status: Some(Osc5522Status::DONE), .. } => { + let mut mimes = Vec::new(); + let mut data = Vec::new(); + for (mime, payload) in s.mime.iter().zip(s.payload.iter()) { + data.push(ClipboardData { mime: mime.to_owned(), data: payload.to_owned() }); + mimes.extend(mime); + mimes.push(b' '); + } + ClipboardEvent::ReadData(ClipboardRead { + mimes: ClipboardMimeList::new(mimes)?, + primary: s.primary, + data, + }) + } + StateOsc5522 { read: true, .. } => { + Self::ReadError(ClipboardError { name: parse_error(s.status)? }) + } + StateOsc5522 { read: false, status: Some(Osc5522Status::DONE), .. } => { + ClipboardEvent::WriteSuccess + } + StateOsc5522 { read: false, .. } => { + Self::WriteError(ClipboardError { name: parse_error(s.status)? }) + } + }) + } +} + +// --- Operation +#[derive(Clone, Copy, Debug, Eq, FromRepr, IntoStaticStr, PartialEq)] +#[repr(u8)] +pub enum ClipboardType { + Read = 1, + Write = 2, +} + +// --- MIME list +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClipboardMimeList(String); + +impl ClipboardMimeList { + pub fn new(b: Vec) -> Option { Some(Self(String::from_utf8(b).ok()?)) } + + pub fn iter(&self) -> SplitWhitespace<'_> { self.0.split_whitespace() } +} + +// --- Error payload parsing +fn parse_error(status: Option) -> Option { + match status { + Some(Osc5522Status::ENOSYS) => Some("ENOSYS".to_string()), + Some(Osc5522Status::EPERM) => Some("EPERM".to_string()), + Some(Osc5522Status::EBUSY) => Some("EBUSY".to_string()), + Some(Osc5522Status::EIO) => Some("EIO".to_string()), + Some(Osc5522Status::EINVAL) => Some("EINVAL".to_string()), + _ => None, + } +} diff --git a/yazi-term/src/event/event.rs b/yazi-term/src/event/event.rs index 0df95509..36988a5b 100644 --- a/yazi-term/src/event/event.rs +++ b/yazi-term/src/event/event.rs @@ -1,4 +1,4 @@ -use crate::{Dimension, event::{DndEvent, KeyEvent, MouseEvent}}; +use crate::{Dimension, event::{ClipboardEvent, DndEvent, KeyEvent, MouseEvent}}; #[derive(Clone, Debug, Eq, PartialEq)] pub enum Event { @@ -9,4 +9,5 @@ pub enum Event { FocusOut, Paste(String), Dnd(DndEvent), + Clipboard(ClipboardEvent), } diff --git a/yazi-term/src/event/lua.rs b/yazi-term/src/event/lua.rs index d6c4a8a1..2080bacb 100644 --- a/yazi-term/src/event/lua.rs +++ b/yazi-term/src/event/lua.rs @@ -3,7 +3,7 @@ use std::mem; use mlua::{IntoLua, Lua, LuaSerdeExt, UserData, UserDataFields, Value}; use yazi_shim::{mlua::{SER_OPT, UserDataFieldsExt}, strum::IntoStr}; -use crate::event::{DndDropArrive, DndEvent, KeyEvent, MouseButton, MouseEvent, MouseEventKind}; +use crate::event::{ClipboardEvent, ClipboardRead, DndDropArrive, DndEvent, KeyEvent, MouseButton, MouseEvent, MouseEventKind}; // --- DndEvent impl UserData for DndEvent { @@ -35,6 +35,38 @@ impl UserData for DndEvent { } } +// --- ClipboardEvent +impl UserData for ClipboardEvent { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("type", |_, me| Ok(me.r#type())); + + fields.add_field_method_get("pw", |_, me| Ok(me.pw())); + + fields.add_field_method_get("primary", |_, me| Ok(me.primary())); + + fields.add_cached_field("mimes", |lua, me| { + if let Some(mimes) = me.mimes() { + lua.create_sequence_from(mimes.iter())?.into_lua(lua) + } else { + Ok(Value::Nil) + } + }); + + fields.add_cached_field_mut("data", |lua, me| match me { + Self::ReadData(ClipboardRead { data, .. }) => lua + .create_table_from(data.iter().map(|d| { + ( + lua.create_string(&d.mime).ok(), + // TODO !!5522!! is this the best way + lua.create_external_string(&*d.data).ok(), + ) + }))? + .into_lua(lua), + _ => Ok(Value::Nil), + }); + } +} + // --- MouseEvent impl UserData for MouseEvent { fn add_fields>(fields: &mut F) { diff --git a/yazi-term/src/event/mod.rs b/yazi-term/src/event/mod.rs index 54533203..a330dbd7 100644 --- a/yazi-term/src/event/mod.rs +++ b/yazi-term/src/event/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(dnd event keyboard lua modifiers mouse); +yazi_macro::mod_flat!(clipboard dnd event keyboard lua modifiers mouse); diff --git a/yazi-term/src/parser/osc.rs b/yazi-term/src/parser/osc.rs index 268dae2b..08dc8f12 100644 --- a/yazi-term/src/parser/osc.rs +++ b/yazi-term/src/parser/osc.rs @@ -1,4 +1,7 @@ -use crate::{ParseError, Result, parser::{Parser, State}}; +use base64::Engine; +use yazi_shim::BASE64_PAD; + +use crate::{ParseError, Result, parser::{Osc5522Status, Parser, State}}; impl Parser { pub(super) fn parse_osc72(&mut self) -> Result<()> { @@ -37,4 +40,68 @@ impl Parser { state.payload.extend(payload); Ok(()) } + + pub(super) fn parse_osc5522(&mut self) -> Result<()> { + debug_assert!(self.seq.starts_with(b"\x1b]5522;")); + debug_assert!(self.seq.ends_with(b"\x1b\\")); + + let mut it = self.seq[7..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::Osc5522(state) = &mut self.state else { unreachable!() }; + state.has_more = false; + + for part in meta.split(':') { + match part.split_once('=').ok_or(ParseError::Invalid)? { + ("status", v) => match v { + "OK" => { + state.status = Some(Osc5522Status::OK); + state.has_more = true; + } + "DATA" => { + state.status = Some(Osc5522Status::DATA); + state.has_more = true; + } + "DONE" => state.status = Some(Osc5522Status::DONE), + "ENOSYS" => state.status = Some(Osc5522Status::ENOSYS), + "EPERM" => state.status = Some(Osc5522Status::EPERM), + "EBUSY" => state.status = Some(Osc5522Status::EBUSY), + "EIO" => state.status = Some(Osc5522Status::EIO), + "EINVAL" => state.status = Some(Osc5522Status::EINVAL), + _ => return Err(ParseError::Invalid), + }, + ("type", v) => state.read = v == "read", + ("loc", v) => state.primary = v == "primary", + ("mime", v) => { + let bytes = BASE64_PAD.decode(v.as_bytes()).or(Err(ParseError::Invalid))?; + if state.mime.len() == 0 { + state.mime.push(bytes); + } else if state.mime[state.idx] != bytes { + state.mime.push(bytes); + state.idx += 1; + } + } + ("pw", v) => state.pw = BASE64_PAD.decode(v.as_bytes()).unwrap_or_default(), + _ => {} + } + } + + // decode now since each payload may have its own padding + let payload = BASE64_PAD.decode(&payload).or(Err(ParseError::Invalid))?; + + // Limit payload size to 1MiB to prevent potential DoS + // TODO A larger size would be required for directly pasting images/large files + if state.payload.len() + payload.len() > 1 << 20 { + return Err(ParseError::Invalid); + } + + if state.idx >= state.payload.len() { + state.payload.push(payload.to_vec()); + } else { + state.payload[state.idx].extend(payload); + } + + Ok(()) + } } diff --git a/yazi-term/src/parser/parser.rs b/yazi-term/src/parser/parser.rs index 79f2a6f2..70d9f574 100644 --- a/yazi-term/src/parser/parser.rs +++ b/yazi-term/src/parser/parser.rs @@ -3,7 +3,7 @@ use std::{collections::VecDeque, mem, num::NonZeroU8, str}; use yazi_shim::utf8_char_width; use super::state::State; -use crate::event::{DndEvent, Event, KeyCode, KeyEvent, Modifiers}; +use crate::event::{ClipboardEvent, DndEvent, Event, KeyCode, KeyEvent, Modifiers}; #[derive(Debug)] pub struct Parser { @@ -39,6 +39,7 @@ impl Parser { State::BracketedPaste => self.on_bracketed_paste(b), State::Osc | State::OscSt => self.on_osc(b), State::Osc72(_) => self.on_osc72(b), + State::Osc5522(_) => self.on_osc5522(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), @@ -55,6 +56,7 @@ impl Parser { match &self.state { State::Esc => self.emit_key(KeyCode::Escape), State::Osc72(s) if s.has_more => return, + State::Osc5522(s) if s.has_more => return, _ => {} } @@ -202,6 +204,9 @@ impl Parser { (State::Osc, _) if self.seq.starts_with(b"\x1b]72;") => { self.state = State::Osc72(Default::default()); } + (State::Osc, _) if self.seq.starts_with(b"\x1b]5522;") => { + self.state = State::Osc5522(Default::default()); + } (State::Osc, b'\x1B') => self.state = State::OscSt, (State::Osc, _) => {} // keep accumulating (State::OscSt, b'\\') => self.reset(), // ST (`\x1B\\`) — OSC complete (discard) @@ -235,6 +240,30 @@ impl Parser { } } + fn on_osc5522(&mut self, b: u8) { + self.seq.push(b); + + if !self.seq.ends_with(b"\x1b\\") { + return; + } else if self.parse_osc5522().is_err() { + return self.reset(); + } + + match mem::take(&mut self.state) { + State::Osc5522(s) if s.has_more => { + self.seq.clear(); + self.state = State::Osc5522(s); + } + State::Osc5522(s) => { + if let Some(e) = ClipboardEvent::from_state(s) { + self.emit(Event::Clipboard(e)); + } + self.reset(); + } + _ => unreachable!(), + } + } + fn on_dcs(&mut self, b: u8) { self.seq.push(b); diff --git a/yazi-term/src/parser/state.rs b/yazi-term/src/parser/state.rs index b2934f53..8a827fc5 100644 --- a/yazi-term/src/parser/state.rs +++ b/yazi-term/src/parser/state.rs @@ -19,6 +19,8 @@ pub(crate) enum State { Osc, /// Inside an OSC 72 (DnD) sequence (`\x1B]72;` … ST). Osc72(StateOsc72), + /// Inside an OSC 5522 (Clipboard) sequence (`\x1B]5522;` … ST). + Osc5522(StateOsc5522), /// Inside OSC, just saw `\x1B` (potential start of ST = `\x1B\\`). OscSt, /// Inside a DCS sequence (`\x1BP` … ST). @@ -40,3 +42,28 @@ pub(crate) struct StateOsc72 { pub(crate) payload: Vec, pub(crate) has_more: bool, } + +#[derive(Debug, Default, PartialEq)] +pub(crate) struct StateOsc5522 { + pub(crate) status: Option, + pub(crate) read: bool, + pub(crate) primary: bool, + pub(crate) mime: Vec>, + pub(crate) payload: Vec>, + pub(crate) pw: Vec, + pub(crate) idx: usize, + pub(crate) has_more: bool, +} + +#[derive(Debug, Default, PartialEq)] +pub(crate) enum Osc5522Status { + #[default] + OK, + DATA, + DONE, + ENOSYS, + EPERM, + EBUSY, + EIO, + EINVAL, +} diff --git a/yazi-tty/src/lua.rs b/yazi-tty/src/lua.rs index 70ae40ca..f1a15144 100644 --- a/yazi-tty/src/lua.rs +++ b/yazi-tty/src/lua.rs @@ -4,7 +4,7 @@ use mlua::{BorrowedBytes, ExternalError, IntoLuaMulti, Lua, MultiValue, Table, U use yazi_binding::Error; use yazi_shim::mlua::{ByteString, LuaTableExt}; -use crate::{Tty, sequence::{AgreeDrag, AgreeDrop, FinishDrop, PresentDrag, PresentDragIcon, StartDrag, StartDrop}}; +use crate::{Tty, sequence::{AgreeDrag, AgreeDrop, FinishDrop, PresentDrag, PresentDragIcon, ReadClipboard, StartDrag, StartDrop, WriteClipboard, WriteClipboardData}}; impl Tty { fn queue(&self, lua: &Lua, kind: &[u8], t: &Table) -> mlua::Result { @@ -54,6 +54,21 @@ impl Tty { _ => return Err("invalid FinishDrop type".into_lua_err()), }, + b"ReadClipboard" => { + let esc_seq = ReadClipboard { + mime: &t.raw_get::("mimes")?, + pw: &t.raw_get::("pw")?, + name: &t.raw_get::("name")?, + primary: t.raw_get("primary")?, + }; + write!(w, "{}", esc_seq) + } + b"WriteClipboard" => { + let mime = &t.raw_get::("mime")?; + let payload = &t.raw_get::("data")?; + let alias = &t.raw_get::("alias")?; + write!(w, "{}", WriteClipboard { data: vec![WriteClipboardData { mime, payload, alias }] }) + } _ => return Err("invalid sequence kind".into_lua_err()), }; diff --git a/yazi-tty/src/sequence/clipboard.rs b/yazi-tty/src/sequence/clipboard.rs index 9ef6be39..dfdaf4e6 100644 --- a/yazi-tty/src/sequence/clipboard.rs +++ b/yazi-tty/src/sequence/clipboard.rs @@ -1,6 +1,7 @@ use std::fmt::{self, Display}; use base64::{Engine, engine::general_purpose}; +use yazi_shim::BASE64_PAD; /// Set clipboard content via OSC 52 pub struct SetClipboard { @@ -18,3 +19,100 @@ impl Display for SetClipboard { write!(f, "\x1b]52;c;{}\x1b\\", self.content) } } + +/// Query OSC 5522 via DECRQM +pub struct QueryOSC5522; + +impl Display for QueryOSC5522 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "\x1b[?5522$p") } +} + +/// Enable receiving unsolicited paste events via OSC 5522: `CSI ? 5522 h` +pub struct EnablePasteEvents; + +impl Display for EnablePasteEvents { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "\x1b[?5522h") } +} + +/// Disable receiving unsolicited paste events via OSC 5522: `CSI ? 5522 l` +pub struct DisablePasteEvents; + +impl Display for DisablePasteEvents { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "\x1b[?5522l") } +} + +/// Read data from clipboard: +/// `OSC 5522 ; type=read : ; ST` +pub struct ReadClipboard<'a> { + pub mime: &'a [u8], + pub pw: &'a [u8], + pub name: &'a [u8], + pub primary: bool, +} + +impl Display for ReadClipboard<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let b64_mime = BASE64_PAD.encode(self.mime).into_bytes(); + let mime_str = unsafe { String::from_utf8_unchecked(b64_mime) }; + let mut metadata = String::new(); + if self.pw.len() > 0 { + let b64_pw = BASE64_PAD.encode(self.pw).into_bytes(); + let pw_str = unsafe { String::from_utf8_unchecked(b64_pw) }; + let b64_name = BASE64_PAD.encode(self.name).into_bytes(); + let name_str = unsafe { String::from_utf8_unchecked(b64_name) }; + metadata.push_str(&format!(":pw={}:name={}", pw_str, name_str)); + } + if self.primary { + metadata.push_str(":loc=primary"); + } + write!(f, "\x1b]5522;type=read{};{}\x1b\\", metadata, mime_str) + } +} + +/// Read available MIME types from clipboard: +/// `OSC 5522 ; type=read ; ST` +pub struct ReadClipboardMimes; + +impl Display for ReadClipboardMimes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "\x1b]5522;type=read;{}\x1b\\", BASE64_PAD.encode(b".")) + } +} + +/// Write data to clipboard: +/// `OSC 5522 ; type=write ST` +/// `OSC 5522 ; type=wdata : mime= ; ST` +/// `OSC 5522 ; type=wdata ST` +pub struct WriteClipboard<'a> { + pub data: Vec>, +} + +impl Display for WriteClipboard<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "\x1b]5522;type=write\x1b\\")?; + for item in &self.data { + let b64_mime = BASE64_PAD.encode(item.mime).into_bytes(); + let mime_str = unsafe { String::from_utf8_unchecked(b64_mime) }; + let data = item.payload; + + for (_, chunk) in data.chunks(4096).enumerate() { + let b64_chunk = BASE64_PAD.encode(chunk).into_bytes(); + let s = unsafe { String::from_utf8_unchecked(b64_chunk) }; + write!(f, "\x1b]5522;type=wdata:mime={};{s}\x1b\\", mime_str)?; + } + + if item.alias.len() > 0 { + let b64_alias = BASE64_PAD.encode(item.alias).into_bytes(); + let s = unsafe { String::from_utf8_unchecked(b64_alias) }; + write!(f, "\x1b]5522;type=walias:mime={};{s}\x1b\\", mime_str)?; + } + } + write!(f, "\x1b]5522;type=wdata\x1b\\") + } +} + +pub struct WriteClipboardData<'a> { + pub mime: &'a [u8], + pub payload: &'a [u8], + pub alias: &'a [u8], +} diff --git a/yazi-tui/src/raterm.rs b/yazi-tui/src/raterm.rs index 4908d59c..7266fb90 100644 --- a/yazi-tui/src/raterm.rs +++ b/yazi-tui/src/raterm.rs @@ -8,7 +8,7 @@ use yazi_macro::writef; use yazi_proxy::AppProxy; use yazi_shim::cell::SyncCell; use yazi_term::{TERM, event::{Event, KeyEventKind}, stream::EventStream}; -use yazi_tty::{TTY, TtyWriter, sequence::{DisableBracketedPaste, DisableDrag, DisableDrop, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, EnableDrag, EnableDrop, EnableFocusChange, EnableMouseCapture, EnterAlternateScreen, If, LeaveAlternateScreen, PopKeyboardFlags, PushKeyboardFlags, RequestCursorBlink, RequestCursorStyle, RequestDA1, RestoreCursorStyle, SetTitle, ShowCursor}}; +use yazi_tty::{TTY, TtyWriter, sequence::{DisableBracketedPaste, DisableDrag, DisableDrop, DisableFocusChange, DisableMouseCapture, DisablePasteEvents, EnableBracketedPaste, EnableDrag, EnableDrop, EnableFocusChange, EnableMouseCapture, EnablePasteEvents, EnterAlternateScreen, If, LeaveAlternateScreen, PopKeyboardFlags, PushKeyboardFlags, RequestCursorBlink, RequestCursorStyle, RequestDA1, RestoreCursorStyle, SetTitle, ShowCursor}}; use crate::{RatermBackend, RatermOption, RatermState}; @@ -43,7 +43,7 @@ impl Raterm { let opt = RatermOption::default(); writef!( TTY.writer(), - "{}{RequestCursorStyle}{RequestCursorBlink}{RequestDA1}{}{EnableBracketedPaste}{EnableFocusChange}{}{}{}{}", + "{}{RequestCursorStyle}{RequestCursorBlink}{RequestDA1}{}{EnableBracketedPaste}{EnableFocusChange}{}{}{}{}{EnablePasteEvents}", If(!TMUX.get(), EnterAlternateScreen), If(TMUX.get(), EnterAlternateScreen), PushKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES @@ -77,7 +77,7 @@ impl Raterm { _ = writef!( TTY.writer(), - "{}{PopKeyboardFlags}{DisableDrop}{DisableDrag}{}{}{DisableFocusChange}{DisableBracketedPaste}{LeaveAlternateScreen}{ShowCursor}", + "{}{PopKeyboardFlags}{DisableDrop}{DisablePasteEvents}{DisableDrag}{}{}{DisableFocusChange}{DisableBracketedPaste}{LeaveAlternateScreen}{ShowCursor}", If(state.mouse, DisableMouseCapture), RestoreCursorStyle { shape: state.cursor_shape, blink: state.cursor_blink }, If(state.title, SetTitle("")), diff --git a/yazi-widgets/src/clipboard.rs b/yazi-widgets/src/clipboard.rs index a6f11d96..47f10b28 100644 --- a/yazi-widgets/src/clipboard.rs +++ b/yazi-widgets/src/clipboard.rs @@ -8,6 +8,12 @@ pub struct Clipboard { content: Mutex>, } +pub struct ClipboardData { + pub mime: Vec, + pub payload: Vec, + pub alias: Vec, +} + impl Clipboard { #[cfg(unix)] pub async fn get(&self) -> Vec { @@ -102,4 +108,44 @@ impl Clipboard { .await .ok(); } + + /// OSC 5522 Query MIME types + pub async fn query_mime_types(&self) { + use yazi_macro::writef; + use yazi_tty::{TTY, sequence::ReadClipboardMimes}; + + writef!(TTY.writer(), "{}", ReadClipboardMimes {}).ok(); + } + + /// OSC 5522 Clipboard read + pub async fn read(&self, mime: impl AsRef<[u8]>, pw: impl AsRef<[u8]>) { + use yazi_macro::writef; + use yazi_tty::{TTY, sequence::ReadClipboard}; + + writef!(TTY.writer(), "{}", ReadClipboard { + mime: mime.as_ref(), + pw: pw.as_ref(), + name: b"yazi", + primary: false, + }) + .ok(); + } + + /// OSC 5522 Clipboard write + pub async fn write(&self, data: impl AsRef<[ClipboardData]>) { + use yazi_macro::writef; + use yazi_tty::{TTY, sequence::{WriteClipboard, WriteClipboardData}}; + + let items = data + .as_ref() + .iter() + .map(|d| WriteClipboardData { + mime: d.mime.as_ref(), + payload: d.payload.as_ref(), + alias: d.alias.as_ref(), + }) + .collect::>(); + + writef!(TTY.writer(), "{}", WriteClipboard { data: items }).ok(); + } }