diff --git a/.gitignore b/.gitignore index 246b034d8..dd4f815ec 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,6 @@ result-* .idea/ .vscode/ +.zed/ *.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index dc2923f5c..87761fc24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Bulk create ([#3793]) - Dynamic keymap Lua API ([#4031]) - New `ui.Input` element ([#4040]) +- Copying and pasting files with the system clipboard ([#4035]) - Image preview with Überzug++ on Niri ([#3990]) - New gait for input `backward` and `forward` actions ([#4012]) @@ -1746,3 +1747,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#4022]: https://github.com/sxyazi/yazi/pull/4022 [#4031]: https://github.com/sxyazi/yazi/pull/4031 [#4040]: https://github.com/sxyazi/yazi/pull/4040 +[#4035]: https://github.com/sxyazi/yazi/pull/4035 diff --git a/Cargo.lock b/Cargo.lock index ac4b38baf..f8e576d7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5567,6 +5567,7 @@ dependencies = [ "mlua", "parking_lot", "paste", + "percent-encoding", "ratatui", "scopeguard", "tokio", diff --git a/yazi-actor/Cargo.toml b/yazi-actor/Cargo.toml index e39c009fd..eb6a8f190 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 } -mlua = { workspace = true } -parking_lot = { workspace = true } -paste = { workspace = true } -ratatui = { 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 } +mlua = { workspace = true } +parking_lot = { workspace = true } +paste = { workspace = true } +ratatui = { 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 000000000..6878434a3 --- /dev/null +++ b/yazi-actor/src/app/clipboard.rs @@ -0,0 +1,41 @@ +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 event = yazi_binding::ClipboardEvent::from(form.event); + + let Some(size) = cx.term.as_ref().and_then(|t| t.size().ok()) else { succ!() }; + let area = yazi_binding::elements::Rect::from(size); + + let result = Lives::scope(cx.core, move |_| { + runtime_scope!(LUA, "root", { + let root = LUA.globals().raw_get::("Root")?.call_method::
("new", area)?; + + root.call_method::<()>("clipboard", 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 b6c0bd1a7..0e5d148e4 100644 --- a/yazi-actor/src/app/mod.rs +++ b/yazi-actor/src/app/mod.rs @@ -1,6 +1,7 @@ yazi_macro::mod_flat!( accept_payload bootstrap + clipboard deprecate dnd focus diff --git a/yazi-actor/src/mgr/copy.rs b/yazi-actor/src/mgr/copy.rs index f2b06391f..0ab6aa6a6 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,25 @@ 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, + alias: b"text/plain".to_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 b030474ad..5e812199b 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)); 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)); } } } diff --git a/yazi-actor/src/tasks/spawn.rs b/yazi-actor/src/tasks/spawn.rs index efa4b535a..f725ac962 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-binding/src/clipboard.rs b/yazi-binding/src/clipboard.rs new file mode 100644 index 000000000..8944965cc --- /dev/null +++ b/yazi-binding/src/clipboard.rs @@ -0,0 +1,49 @@ +use std::ops::Deref; + +use mlua::{IntoLua, UserData, UserDataFields, Value}; +use yazi_shim::mlua::UserDataFieldsExt; +use yazi_term::event::{ClipboardEvent as Inner, ClipboardRead}; + +pub struct ClipboardEvent { + inner: Inner, +} + +impl Deref for ClipboardEvent { + type Target = Inner; + + fn deref(&self) -> &Self::Target { &self.inner } +} + +impl From for ClipboardEvent { + fn from(inner: Inner) -> Self { Self { inner } } +} + +impl UserData for ClipboardEvent { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("type", |_, me| Ok(me.inner.r#type())); + + fields.add_field_method_get("pw", |_, me| Ok(me.inner.pw())); + + fields.add_field_method_get("primary", |_, me| Ok(me.inner.primary())); + + fields.add_cached_field("mimes", |lua, me| { + if let Some(mimes) = me.inner.mimes() { + lua.create_sequence_from(mimes.iter())?.into_lua(lua) + } else { + Ok(Value::Nil) + } + }); + + fields.add_cached_field_mut("data", |lua, me| match &mut me.inner { + Inner::ReadData(ClipboardRead { data, .. }) => lua + .create_table_from( + data + .iter() + .map(|d| Ok((lua.create_string(&d.mime)?, lua.create_external_string(&*d.data)?))) + .collect::, mlua::Error>>()?, + )? + .into_lua(lua), + _ => Ok(Value::Nil), + }); + } +} diff --git a/yazi-binding/src/lib.rs b/yazi-binding/src/lib.rs index 8c52be9e4..d845e98fb 100644 --- a/yazi-binding/src/lib.rs +++ b/yazi-binding/src/lib.rs @@ -2,4 +2,4 @@ mod macros; yazi_macro::mod_pub!(config elements event keymap process theme); -yazi_macro::mod_flat!(access calculator cha chan composer dnd error fd file handle icon id image input iter layer mouse path permit range runtime scheme selector sendable stage style tty url utils); +yazi_macro::mod_flat!(access calculator cha chan clipboard composer dnd error fd file handle icon id image input iter layer mouse path permit range runtime scheme selector sendable stage style tty url utils); diff --git a/yazi-binding/src/tty.rs b/yazi-binding/src/tty.rs index 2713716c4..06367a9bc 100644 --- a/yazi-binding/src/tty.rs +++ b/yazi-binding/src/tty.rs @@ -2,7 +2,7 @@ use std::io::Write; use mlua::{BorrowedBytes, ExternalError, IntoLuaMulti, Lua, MultiValue, Table, UserData, UserDataMethods}; use yazi_shim::mlua::{ByteString, LuaTableExt}; -use yazi_term::sequence::{AgreeDrag, AgreeDrop, FinishDrop, PresentDrag, PresentDragIcon, StartDrag, StartDrop}; +use yazi_term::sequence::{AgreeDrag, AgreeDrop, FinishDrop, PresentDrag, PresentDragIcon, ReadClipboard, StartDrag, StartDrop, WriteClipboard, WriteClipboardData}; use yazi_tty::TTY; use crate::Error; @@ -51,6 +51,30 @@ impl Tty { b"move" => write!(w, "{}", FinishDrop::Move), _ => return Err("invalid FinishDrop type".into_lua_err()), }, + b"ReadClipboard" => { + write!(w, "{}", ReadClipboard { + mime: &t.raw_get::("mimes")?, + pw: &t.raw_get::("pw")?, + name: &t.raw_get::("name")?, + primary: t.raw_get("primary")?, + }) + } + b"WriteClipboard" => { + let mut data = Vec::new(); + for v in &t.sequence_values::
().collect::, mlua::Error>>()? { + data.push(( + v.raw_get::("mime")?, + v.raw_get::("data")?, + v.raw_get::("alias")?, + )); + } + write!(w, "{}", WriteClipboard { + data: data + .iter() + .map(|(m, p, a)| { WriteClipboardData { mime: &m, payload: &p, alias: &a } }) + .collect(), + }) + } _ => return Err("invalid sequence kind".into_lua_err()), }; diff --git a/yazi-config/preset/keymap-default.toml b/yazi-config/preset/keymap-default.toml index 900cf806b..7b329c79b 100644 --- a/yazi-config/preset/keymap-default.toml +++ b/yazi-config/preset/keymap-default.toml @@ -100,6 +100,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 c956c76e9..5a55f54e4 100644 --- a/yazi-core/src/tasks/file.rs +++ b/yazi-core/src/tasks/file.rs @@ -1,6 +1,6 @@ use indexmap::IndexSet; use tracing::debug; -use yazi_scheduler::file::FileInCut; +use yazi_scheduler::file::{FileInCopy, FileInCut}; use yazi_shared::url::{UrlBuf, UrlBufCov, UrlLike}; use super::Tasks; @@ -23,7 +23,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) { self.scheduler.behavior.reset(); for u in src.iter() { @@ -34,7 +34,7 @@ impl Tasks { if force && *u == to { debug!("file_copy: same file, skip {to:?}"); } else { - self.scheduler.file_copy(u.0.clone(), to, force, follow); + self.scheduler.file_copy(FileInCopy::new(u.0.clone(), to, force)); } } } diff --git a/yazi-core/src/tasks/option.rs b/yazi-core/src/tasks/option.rs index 67d6c4ea7..2732bd747 100644 --- a/yazi-core/src/tasks/option.rs +++ b/yazi-core/src/tasks/option.rs @@ -1,12 +1,13 @@ 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, SStr}; #[derive(Clone, Debug)] pub enum TaskOpt { Cut(FileInCut), + Copy(FileInCopy), Plugin(PluginInEntry), } @@ -19,6 +20,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(), } @@ -27,6 +29,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), } @@ -36,6 +39,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(), } @@ -44,6 +48,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 4fa699276..5fded3d2f 100644 --- a/yazi-emulator/src/emulator.rs +++ b/yazi-emulator/src/emulator.rs @@ -8,7 +8,7 @@ use tokio::time::sleep; use tracing::{debug, error, warn}; use yazi_macro::writef; use yazi_shim::cell::RoCell; -use yazi_term::{TERM, sequence::{HideCursor, If, KittyGraphicsQuery, MoveTo, RequestBgColor, RequestCellPixelSize, RequestDA1, RequestXtVersion, RestoreCursorPos, SaveCursorPos, SetFg, SetSgr, ShowCursor}}; +use yazi_term::{TERM, sequence::{HideCursor, If, KittyGraphicsQuery, MoveTo, QueryOSC5522, RequestBgColor, RequestCellPixelSize, RequestDA1, RequestXtVersion, RestoreCursorPos, SaveCursorPos, SetFg, SetSgr, ShowCursor}}; use yazi_tty::{Handle, TTY}; 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,14 @@ impl Emulator { }; let csi_16t = Self::csi_16t(&resp).unwrap_or_default(); + 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: Self::osc_5522(&resp), }) } @@ -186,6 +191,10 @@ impl Emulator { } } + fn osc_5522(resp: &str) -> bool { + ["\x1b[?5522;1$y", "\x1b[?5522;2$y", "\x1b[?5522;3$y"].iter().any(|s| resp.contains(s)) + } + fn force_16t((w, h): (u16, u16)) -> bool { if w == 0 || h == 0 { return false; diff --git a/yazi-fm/src/dispatcher.rs b/yazi-fm/src/dispatcher.rs index 6e24e65bd..b13852cbe 100644 --- a/yazi-fm/src/dispatcher.rs +++ b/yazi-fm/src/dispatcher.rs @@ -6,7 +6,7 @@ use yazi_actor::Ctx; use yazi_config::keymap::Key; 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}; @@ -30,6 +30,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 { @@ -102,4 +103,15 @@ 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.focus() { + if let Some(text) = clip.text() { + self.dispatch_paste(text)?; + return Ok(()); + } + } + 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 000000000..d5e6fc515 --- /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 095335e81..a7124819e 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!(clipboard deprecate dnd lua mouse plugin quit reflow title update_progress); diff --git a/yazi-parser/src/spark/spark.rs b/yazi-parser/src/spark/spark.rs index 97cfd306d..fa37b87ea 100644 --- a/yazi-parser/src/spark/spark.rs +++ b/yazi-parser/src/spark/spark.rs @@ -10,6 +10,7 @@ pub enum Spark<'a> { // App AppAcceptPayload(yazi_dds::Payload<'a>), AppBootstrap(crate::VoidForm), + AppClipboard(crate::app::ClipboardForm), AppDeprecate(crate::app::DeprecateForm), AppDnd(crate::app::DndForm), AppFocus(crate::VoidForm), @@ -203,6 +204,7 @@ impl<'a> IntoLua for Spark<'a> { // App Self::AppAcceptPayload(b) => b.into_lua(lua), Self::AppBootstrap(b) => b.into_lua(lua), + Self::AppClipboard(b) => b.into_lua(lua), Self::AppDeprecate(b) => b.into_lua(lua), Self::AppDnd(b) => b.into_lua(lua), Self::AppFocus(b) => b.into_lua(lua), @@ -381,6 +383,7 @@ try_from_spark!( // App try_from_spark!(crate::ArrowForm, mgr:arrow, mgr:tab_swap); +try_from_spark!(crate::app::ClipboardForm, app:clipboard); try_from_spark!(crate::app::DeprecateForm, app:deprecate); try_from_spark!(crate::app::DndForm, app:dnd); try_from_spark!(crate::app::LuaForm, app:lua); diff --git a/yazi-plugin/preset/components/root.lua b/yazi-plugin/preset/components/root.lua index 466994a10..a8718448d 100644 --- a/yazi-plugin/preset/components/root.lua +++ b/yazi-plugin/preset/components/root.lua @@ -99,3 +99,16 @@ function Root:drop(event) return c and c.drop and c:drop(event) end end + +function Root:clipboard(event) + if event and event.type == "mimetypes" and event.pw then + -- No harm in asking for unavailable types + local mimetypes = "text/plain text/uri-list" + rt.tty:queue("ReadClipboard", { mimes = mimetypes, pw = event.pw, name = "Paste Event", primary = event.primary }) + rt.tty:flush() + elseif event and event.type == "data" then + if event.data["text/uri-list"] ~= nil then + require("clipboard").copy_uri_list(event.data["text/uri-list"]) + end + end +end diff --git a/yazi-plugin/preset/plugins/clipboard.lua b/yazi-plugin/preset/plugins/clipboard.lua new file mode 100644 index 000000000..32c9d7329 --- /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/utils/tasks.rs b/yazi-plugin/src/utils/tasks.rs index 4c5c63e76..ee058868a 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): (mlua::String, 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 6f69da1ac..bb32b11e0 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 2f6b486f0..794a750f2 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) -> Self { + Self { + follow: !from.scheme().covariant(to.scheme()), + id: Id::ZERO, + from, + to, + force, + cha: None, + retry: 0, + } + } + pub(super) fn into_link(self) -> FileInLink { FileInLink { id: self.id, @@ -171,6 +183,19 @@ 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")?.into(), + t.raw_get::("to")?.into(), + t.raw_get("force")?, + )) + } +} // --- Cut #[derive(Clone, Debug)] pub struct FileInCut { diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 35bb36931..a5970d965 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.scheme().covariant(to.scheme()); - 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 c3f6dae06..90be5cb32 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 000000000..3d0d11a61 --- /dev/null +++ b/yazi-term/src/event/clipboard.rs @@ -0,0 +1,144 @@ +use strum::{FromRepr, IntoStaticStr}; + +use crate::{event::mime::MimeList, 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: MimeList, + 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: MimeList, + 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(_) => "mimetypes", + Self::ReadData(_) => "data", + Self::ReadError(_) => "error", + Self::WriteSuccess => "success", + Self::WriteError(_) => "error", + } + } + + pub fn mimes(&self) -> Option<&MimeList> { + 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_read(&self) -> bool { + match self { + Self::ReadMimetypes(_) | 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: MimeList::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: MimeList::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, +} + +// --- 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/dnd.rs b/yazi-term/src/event/dnd.rs index d3c81b6ab..e00870e22 100644 --- a/yazi-term/src/event/dnd.rs +++ b/yazi-term/src/event/dnd.rs @@ -1,10 +1,8 @@ -use std::str::SplitWhitespace; - use base64::Engine; use strum::{FromRepr, IntoStaticStr}; use yazi_shim::BASE64_SANE; -use crate::parser::StateOsc72; +use crate::{event::mime::MimeList, parser::StateOsc72}; #[derive(Clone, Debug, Eq, PartialEq)] pub enum DndEvent { @@ -63,7 +61,7 @@ pub struct DndDropEnter { pub x: u32, pub y: u32, pub op: DndOp, - pub mimes: DndMimeList, + pub mimes: MimeList, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -71,7 +69,7 @@ pub struct DndDropReady { pub x: u32, pub y: u32, pub op: DndOp, - pub mimes: DndMimeList, + pub mimes: MimeList, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -144,7 +142,7 @@ impl DndEvent { } } - pub fn mimes(&self) -> Option<&DndMimeList> { + pub fn mimes(&self) -> Option<&MimeList> { match self { Self::DropEnter(e) => Some(&e.mimes), Self::DropReady(e) => Some(&e.mimes), @@ -185,13 +183,13 @@ impl DndEvent { x: s.x?.try_into().ok()?, y: s.y?.try_into().ok()?, op: DndOp::from_repr(s.op?)?, - mimes: DndMimeList::new(s.payload)?, + mimes: MimeList::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)?, + mimes: MimeList::new(s.payload)?, }), b'r' => Self::DropArrive(DndDropArrive { idx: s.x?.try_into().ok()?, @@ -217,14 +215,6 @@ pub enum DndOp { } // --- MIME list -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct DndMimeList(String); - -impl DndMimeList { - pub fn new(b: Vec) -> Option { Some(Self(String::from_utf8(b).ok()?)) } - - pub fn iter(&self) -> SplitWhitespace<'_> { self.0.split_whitespace() } -} // --- Error payload parsing fn parse_error(payload: Vec) -> Option<(String, String)> { diff --git a/yazi-term/src/event/event.rs b/yazi-term/src/event/event.rs index 0df955091..36988a5b6 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/mime.rs b/yazi-term/src/event/mime.rs new file mode 100644 index 000000000..b2c5e2ceb --- /dev/null +++ b/yazi-term/src/event/mime.rs @@ -0,0 +1,10 @@ +use std::str::SplitWhitespace; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MimeList(String); + +impl MimeList { + pub fn new(b: Vec) -> Option { Some(Self(String::from_utf8(b).ok()?)) } + + pub fn iter(&self) -> SplitWhitespace<'_> { self.0.split_whitespace() } +} diff --git a/yazi-term/src/event/mod.rs b/yazi-term/src/event/mod.rs index 247ca94b0..e641a4a5a 100644 --- a/yazi-term/src/event/mod.rs +++ b/yazi-term/src/event/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(dnd event keyboard modifiers mouse); +yazi_macro::mod_flat!(clipboard dnd event keyboard mime modifiers mouse); diff --git a/yazi-term/src/parser/osc.rs b/yazi-term/src/parser/osc.rs index 268dae2b6..4ae96e69f 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))?; + + if state.idx >= state.payload.len() { + state.payload.push(payload.to_vec()); + } else { + state.payload[state.idx].extend(payload); + } + + // Limit payload size to 1MiB per mime type to prevent potential DoS + // TODO A larger size would be required for directly pasting images/large files + if state.payload[state.idx].len() > 1 << 20 { + return Err(ParseError::Invalid); + } + + Ok(()) + } } diff --git a/yazi-term/src/parser/parser.rs b/yazi-term/src/parser/parser.rs index 79f2a6f2c..70d9f5741 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 b2934f53b..8a827fc55 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-term/src/sequence/clipboard.rs b/yazi-term/src/sequence/clipboard.rs index 9ef6be394..2c9b05801 100644 --- a/yazi-term/src/sequence/clipboard.rs +++ b/yazi-term/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,94 @@ 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); + let mut metadata = String::new(); + if self.pw.len() > 0 { + let b64_pw = BASE64_PAD.encode(self.pw); + let b64_name = BASE64_PAD.encode(self.name); + metadata.push_str(&format!(":pw={}:name={}", b64_pw, b64_name)); + } + if self.primary { + metadata.push_str(":loc=primary"); + } + write!(f, "\x1b]5522;type=read{};{}\x1b\\", metadata, b64_mime) + } +} + +/// 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); + let data = item.payload; + + for (_, chunk) in data.chunks(4096).enumerate() { + let b64_chunk = BASE64_PAD.encode(chunk); + write!(f, "\x1b]5522;type=wdata:mime={};{}\x1b\\", b64_mime, b64_chunk)?; + } + + if item.alias.len() > 0 { + let b64_alias = BASE64_PAD.encode(item.alias); + write!(f, "\x1b]5522;type=walias:mime={};{}\x1b\\", b64_mime, b64_alias)?; + } + } + 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 f85f5199f..08583c96d 100644 --- a/yazi-tui/src/raterm.rs +++ b/yazi-tui/src/raterm.rs @@ -6,7 +6,7 @@ use yazi_config::YAZI; use yazi_emulator::{Emulator, Mux, TMUX}; use yazi_macro::writef; use yazi_shim::cell::SyncCell; -use yazi_term::{TERM, event::{Event, KeyEventKind}, sequence::{DisableBracketedPaste, DisableDrag, DisableDrop, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, EnableDrag, EnableDrop, EnableFocusChange, EnableMouseCapture, EnterAlternateScreen, If, LeaveAlternateScreen, PopKeyboardFlags, PushKeyboardFlags, RequestCursorBlink, RequestCursorStyle, RequestDA1, RequestKeyboardFlags, RestoreCursorStyle, SetTitle, ShowCursor}, stream::EventStream}; +use yazi_term::{TERM, event::{Event, KeyEventKind}, sequence::{DisableBracketedPaste, DisableDrag, DisableDrop, DisableFocusChange, DisableMouseCapture, DisablePasteEvents, EnableBracketedPaste, EnableDrag, EnableDrop, EnableFocusChange, EnableMouseCapture, EnablePasteEvents, EnterAlternateScreen, If, LeaveAlternateScreen, PopKeyboardFlags, PushKeyboardFlags, RequestCursorBlink, RequestCursorStyle, RequestDA1, RequestKeyboardFlags, RestoreCursorStyle, SetTitle, ShowCursor}, stream::EventStream}; use yazi_tty::{TTY, TtyWriter}; use crate::{RatermBackend, RatermOption, RatermState}; @@ -42,7 +42,7 @@ impl Raterm { let opt = RatermOption::default(); writef!( TTY.writer(), - "{}{RequestCursorStyle}{RequestCursorBlink}{RequestKeyboardFlags}{RequestDA1}{}{EnableBracketedPaste}{EnableFocusChange}{}{}{}", + "{}{RequestCursorStyle}{RequestCursorBlink}{RequestKeyboardFlags}{RequestDA1}{}{EnableBracketedPaste}{EnableFocusChange}{}{}{}{EnablePasteEvents}", If(!TMUX.get(), EnterAlternateScreen), If(TMUX.get(), EnterAlternateScreen), EnableDrag(""), @@ -81,7 +81,7 @@ impl Raterm { _ = writef!( TTY.writer(), - "{}{DisableDrop}{DisableDrag}{}{}{}{DisableFocusChange}{DisableBracketedPaste}{LeaveAlternateScreen}{ShowCursor}", + "{}{DisableDrop}{DisableDrag}{DisablePasteEvents}{}{}{}{DisableFocusChange}{DisableBracketedPaste}{LeaveAlternateScreen}{ShowCursor}", If(state.mouse, DisableMouseCapture), If(state.csi_u, PopKeyboardFlags), RestoreCursorStyle { shape: state.cursor_shape, blink: state.cursor_blink }, diff --git a/yazi-widgets/src/clipboard.rs b/yazi-widgets/src/clipboard.rs index 7365b003a..9d16e9723 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 { @@ -103,4 +109,47 @@ impl Clipboard { .await .ok(); } + + /// OSC 5522 Query MIME types + pub async fn query_mime_types(&self) { + use yazi_macro::writef; + use yazi_term::sequence::ReadClipboardMimes; + use yazi_tty::TTY; + + 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_term::sequence::ReadClipboard; + use yazi_tty::TTY; + + 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_term::sequence::{WriteClipboard, WriteClipboardData}; + use yazi_tty::TTY; + + 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(); + } }