mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-23 07:41:03 +00:00
feat: osc 5522 clipboard
This commit is contained in:
parent
b25ae9f82d
commit
ae5f84ee65
36 changed files with 743 additions and 38 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -10,5 +10,6 @@ result-*
|
|||
|
||||
.idea/
|
||||
.vscode/
|
||||
.zed/
|
||||
|
||||
*.snap
|
||||
|
|
|
|||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -4745,6 +4745,7 @@ dependencies = [
|
|||
"libc",
|
||||
"mlua",
|
||||
"paste",
|
||||
"percent-encoding",
|
||||
"ratatui-core",
|
||||
"scopeguard",
|
||||
"tokio",
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
45
yazi-actor/src/app/clipboard.rs
Normal file
45
yazi-actor/src/app/clipboard.rs
Normal file
|
|
@ -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<Data> {
|
||||
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::<Table>("Root")?.call_method::<Table>("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?);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,4 +16,5 @@ yazi_macro::mod_flat!(
|
|||
theme
|
||||
title
|
||||
update_progress
|
||||
clipboard
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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::<ClipboardData>::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!();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ impl Actor for Spawn {
|
|||
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
|
||||
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),
|
||||
})
|
||||
|
|
|
|||
1
yazi-cli/src/env/env.rs
vendored
1
yazi-cli/src/env/env.rs
vendored
|
|
@ -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())?;
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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<bool>) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SStr>) -> &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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(|_| ())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
yazi-parser/src/app/clipboard.rs
Normal file
19
yazi-parser/src/app/clipboard.rs
Normal file
|
|
@ -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<ClipboardEvent> for ClipboardForm {
|
||||
fn from(event: ClipboardEvent) -> Self { Self { event } }
|
||||
}
|
||||
|
||||
impl FromLua for ClipboardForm {
|
||||
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
|
||||
}
|
||||
|
||||
impl IntoLua for ClipboardForm {
|
||||
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
32
yazi-plugin/preset/plugins/clipboard.lua
Normal file
32
yazi-plugin/preset/plugins/clipboard.lua
Normal file
|
|
@ -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
|
||||
|
|
@ -7,6 +7,7 @@ pub(super) fn term() -> Composer<ComposerGet, ComposerSet> {
|
|||
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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)?),
|
||||
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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<bool>) -> 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<Self> {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
|
|
|
|||
163
yazi-term/src/event/clipboard.rs
Normal file
163
yazi-term/src/event/clipboard.rs
Normal file
|
|
@ -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<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ClipboardData {
|
||||
pub mime: Vec<u8>,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ClipboardRead {
|
||||
pub mimes: ClipboardMimeList,
|
||||
pub primary: bool,
|
||||
pub data: Vec<ClipboardData>,
|
||||
}
|
||||
|
||||
#[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<bool> {
|
||||
match self {
|
||||
Self::ReadMimetypes(e) => Some(e.primary),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pw(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::ReadMimetypes(e) => Some(String::from_utf8_lossy(&e.pw).into_owned()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(&self) -> Option<String> {
|
||||
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<Self> {
|
||||
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<u8>) -> Option<Self> { Some(Self(String::from_utf8(b).ok()?)) }
|
||||
|
||||
pub fn iter(&self) -> SplitWhitespace<'_> { self.0.split_whitespace() }
|
||||
}
|
||||
|
||||
// --- Error payload parsing
|
||||
fn parse_error(status: Option<Osc5522Status>) -> Option<String> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<F: UserDataFields<Self>>(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<F: UserDataFields<Self>>(fields: &mut F) {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(dnd event keyboard lua modifiers mouse);
|
||||
yazi_macro::mod_flat!(clipboard dnd event keyboard lua modifiers mouse);
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<u8>,
|
||||
pub(crate) has_more: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub(crate) struct StateOsc5522 {
|
||||
pub(crate) status: Option<Osc5522Status>,
|
||||
pub(crate) read: bool,
|
||||
pub(crate) primary: bool,
|
||||
pub(crate) mime: Vec<Vec<u8>>,
|
||||
pub(crate) payload: Vec<Vec<u8>>,
|
||||
pub(crate) pw: Vec<u8>,
|
||||
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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<MultiValue> {
|
||||
|
|
@ -54,6 +54,21 @@ impl Tty {
|
|||
_ => return Err("invalid FinishDrop type".into_lua_err()),
|
||||
},
|
||||
|
||||
b"ReadClipboard" => {
|
||||
let esc_seq = ReadClipboard {
|
||||
mime: &t.raw_get::<BorrowedBytes>("mimes")?,
|
||||
pw: &t.raw_get::<BorrowedBytes>("pw")?,
|
||||
name: &t.raw_get::<BorrowedBytes>("name")?,
|
||||
primary: t.raw_get("primary")?,
|
||||
};
|
||||
write!(w, "{}", esc_seq)
|
||||
}
|
||||
b"WriteClipboard" => {
|
||||
let mime = &t.raw_get::<BorrowedBytes>("mime")?;
|
||||
let payload = &t.raw_get::<BorrowedBytes>("data")?;
|
||||
let alias = &t.raw_get::<BorrowedBytes>("alias")?;
|
||||
write!(w, "{}", WriteClipboard { data: vec![WriteClipboardData { mime, payload, alias }] })
|
||||
}
|
||||
_ => return Err("invalid sequence kind".into_lua_err()),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 : <metadata> ; <base64 MIME list> 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 ; <base64 [.]> 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=<base64 MIME type> ; <base64 data chunk> ST`
|
||||
/// `OSC 5522 ; type=wdata ST`
|
||||
pub struct WriteClipboard<'a> {
|
||||
pub data: Vec<WriteClipboardData<'a>>,
|
||||
}
|
||||
|
||||
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],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("")),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ pub struct Clipboard {
|
|||
content: Mutex<Vec<u8>>,
|
||||
}
|
||||
|
||||
pub struct ClipboardData {
|
||||
pub mime: Vec<u8>,
|
||||
pub payload: Vec<u8>,
|
||||
pub alias: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Clipboard {
|
||||
#[cfg(unix)]
|
||||
pub async fn get(&self) -> Vec<u8> {
|
||||
|
|
@ -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::<Vec<_>>();
|
||||
|
||||
writef!(TTY.writer(), "{}", WriteClipboard { data: items }).ok();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue