mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-23 15:51:03 +00:00
Merge b74dab360f into b25ae9f82d
This commit is contained in:
commit
22f1ba16a6
37 changed files with 718 additions and 54 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -10,5 +10,6 @@ result-*
|
|||
|
||||
.idea/
|
||||
.vscode/
|
||||
.zed/
|
||||
|
||||
*.snap
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
|
|||
- Show file icons in trash/delete/overwrite confirmations ([#4096])
|
||||
- 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])
|
||||
|
||||
|
|
@ -1768,6 +1769,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
|
|||
[#4012]: https://github.com/sxyazi/yazi/pull/4012
|
||||
[#4022]: https://github.com/sxyazi/yazi/pull/4022
|
||||
[#4031]: https://github.com/sxyazi/yazi/pull/4031
|
||||
[#4035]: https://github.com/sxyazi/yazi/pull/4035
|
||||
[#4040]: https://github.com/sxyazi/yazi/pull/4040
|
||||
[#4064]: https://github.com/sxyazi/yazi/pull/4064
|
||||
[#4065]: https://github.com/sxyazi/yazi/pull/4065
|
||||
|
|
|
|||
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -4744,7 +4744,9 @@ dependencies = [
|
|||
"inventory",
|
||||
"libc",
|
||||
"mlua",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
"percent-encoding",
|
||||
"ratatui-core",
|
||||
"scopeguard",
|
||||
"tokio",
|
||||
|
|
|
|||
|
|
@ -40,19 +40,21 @@ 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 }
|
||||
parking_lot = { 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 }
|
||||
|
|
|
|||
39
yazi-actor/src/app/clipboard.rs
Normal file
39
yazi-actor/src/app/clipboard.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
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)?;
|
||||
|
||||
root.call_method::<()>("clipboard", form.event)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
|
||||
if let Err(ref e) = result {
|
||||
error!("{e}");
|
||||
}
|
||||
succ!(result?);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
yazi_macro::mod_flat!(
|
||||
accept_payload
|
||||
bootstrap
|
||||
clipboard
|
||||
deprecate
|
||||
dnd
|
||||
focus
|
||||
|
|
|
|||
|
|
@ -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::<ClipboardData>::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!();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,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;
|
||||
|
|
|
|||
|
|
@ -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,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(|_| ())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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!(clipboard deprecate dnd lua mouse plugin quit reflow title update_progress);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
@ -205,6 +206,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),
|
||||
|
|
@ -386,6 +388,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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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
|
||||
|
|
@ -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) -> Self {
|
||||
Self {
|
||||
follow: !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")?))
|
||||
}
|
||||
}
|
||||
// --- 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),
|
||||
);
|
||||
|
|
|
|||
144
yazi-term/src/event/clipboard.rs
Normal file
144
yazi-term/src/event/clipboard.rs
Normal file
|
|
@ -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<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: MimeList,
|
||||
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(_) => "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<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_read(&self) -> bool {
|
||||
match self {
|
||||
Self::ReadMimetypes(_) | 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: 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<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,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<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(payload: Vec<u8>) -> Option<(String, String)> {
|
||||
|
|
|
|||
|
|
@ -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,37 @@ 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| Ok((lua.create_string(&d.mime)?, lua.create_external_string(&*d.data)?)))
|
||||
.collect::<Result<Vec<_>, mlua::Error>>()?,
|
||||
)?
|
||||
.into_lua(lua),
|
||||
_ => Ok(Value::Nil),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- MouseEvent
|
||||
impl UserData for MouseEvent {
|
||||
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
|
||||
|
|
|
|||
10
yazi-term/src/event/mime.rs
Normal file
10
yazi-term/src/event/mime.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
use std::str::SplitWhitespace;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MimeList(String);
|
||||
|
||||
impl MimeList {
|
||||
pub fn new(b: Vec<u8>) -> Option<Self> { Some(Self(String::from_utf8(b).ok()?)) }
|
||||
|
||||
pub fn iter(&self) -> SplitWhitespace<'_> { self.0.split_whitespace() }
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(dnd event keyboard lua modifiers mouse);
|
||||
yazi_macro::mod_flat!(clipboard dnd event keyboard lua mime 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))?;
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,32 @@ impl Tty {
|
|||
_ => return Err("invalid FinishDrop type".into_lua_err()),
|
||||
},
|
||||
|
||||
// Clipboard
|
||||
b"ReadClipboard" => {
|
||||
write!(w, "{}", ReadClipboard {
|
||||
mime: &t.raw_get::<BorrowedBytes>("mimes")?,
|
||||
pw: &t.raw_get::<BorrowedBytes>("pw")?,
|
||||
name: &t.raw_get::<BorrowedBytes>("name")?,
|
||||
primary: t.raw_get("primary")?,
|
||||
})
|
||||
}
|
||||
b"WriteClipboard" => {
|
||||
let mut data = Vec::new();
|
||||
for v in &t.sequence_values::<Table>().collect::<Result<Vec<_>, mlua::Error>>()? {
|
||||
data.push((
|
||||
v.raw_get::<BorrowedBytes>("mime")?,
|
||||
v.raw_get::<BorrowedBytes>("data")?,
|
||||
v.raw_get::<BorrowedBytes>("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()),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 : <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);
|
||||
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 ; <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);
|
||||
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],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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