Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ result-*

.idea/
.vscode/
.zed/

*.snap
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 14 additions & 13 deletions yazi-actor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
41 changes: 41 additions & 0 deletions yazi-actor/src/app/clipboard.rs
Original file line number Diff line number Diff line change
@@ -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<Data> {
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::<Table>("Root")?.call_method::<Table>("new", area)?;

root.call_method::<()>("clipboard", event)?;

Ok(())
})
});

if let Err(ref e) = result {
error!("{e}");
}
succ!(result?);
}
}
1 change: 1 addition & 0 deletions yazi-actor/src/app/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
yazi_macro::mod_flat!(
accept_payload
bootstrap
clipboard
deprecate
dnd
focus
Expand Down
33 changes: 31 additions & 2 deletions yazi-actor/src/mgr/copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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() {
Expand All @@ -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!();
}
}
2 changes: 1 addition & 1 deletion yazi-actor/src/mgr/paste.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}
1 change: 1 addition & 0 deletions yazi-actor/src/tasks/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
})
Expand Down
49 changes: 49 additions & 0 deletions yazi-binding/src/clipboard.rs
Original file line number Diff line number Diff line change
@@ -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<Inner> for ClipboardEvent {
fn from(inner: Inner) -> Self { Self { inner } }
}

impl UserData for ClipboardEvent {
fn add_fields<F: UserDataFields<Self>>(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::<Result<Vec<_>, mlua::Error>>()?,
)?
.into_lua(lua),
_ => Ok(Value::Nil),
});
}
}
2 changes: 1 addition & 1 deletion yazi-binding/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
26 changes: 25 additions & 1 deletion yazi-binding/src/tty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::<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()),
};

Expand Down
1 change: 1 addition & 0 deletions yazi-config/preset/keymap-default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
6 changes: 3 additions & 3 deletions yazi-core/src/tasks/file.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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() {
Expand All @@ -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));
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion yazi-core/src/tasks/option.rs
Original file line number Diff line number Diff line change
@@ -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),
}
Expand All @@ -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(),
}
Expand All @@ -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),
}
Expand All @@ -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(),
}
Expand All @@ -44,6 +48,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),
}
Expand Down
Loading
Loading