Skip to content

Commit

Permalink
Finally fix the wayland flaky paste
Browse files Browse the repository at this point in the history
Quote from WlDataOffer.receive:
> The receiving client reads from the read end of the pipe until EOF
  and then closes its end, at which point the transfer is complete.

The original idea was passing the stdin as the write pipe fd to the
receive(). However, sometimes the clipboard content was not pasted when
the richclip was called in neovim. That could be caused that, neovim
does not read the stdout fast enough. Since when the receive call
finishes, it ends the loop and quite the program.

To fix this, just follow the doc to create a dedicated pipe.
BTW, I was too lazy to do this at the beginning, since I was not aware of
nix-rust at that time -- I didn't want to get hands dirty with 'unsafe'.
  • Loading branch information
beeender committed Jan 8, 2025
1 parent 8b72110 commit fd6b146
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 23 deletions.
25 changes: 25 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ libc = "0.2.168"
xcb = { version = "1.5.0", features = [] }
x11rb = { version = "0.13.1", features = [] }
simplelog = "0.12.2"
nix = "0.29.0"

[build-dependencies]
vergen-git2 = { version = "1.0.2", features = ["build", "cargo"] }
5 changes: 2 additions & 3 deletions src/clipboard/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ mod x;

use super::protocol::SourceData;
use std::io::Write;
use std::os::fd::AsFd;

pub struct PasteConfig<'a, T: AsFd + Write> {
pub struct PasteConfig<'a, T: Write> {
// Only list mime-types
pub list_types_only: bool,
pub use_primary: bool,
pub expected_mime_type: String,
pub fd_to_write: &'a mut T,
pub writter: &'a mut T,
}

pub struct CopyConfig<T: SourceData> {
Expand Down
34 changes: 22 additions & 12 deletions src/clipboard/wayland.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ use super::CopyConfig;
use super::PasteConfig;
use crate::protocol::SourceData;
use anyhow::{bail, Context, Error, Result};
use nix::unistd::{pipe, read};
use std::collections::HashMap;
use std::ffi::CString;
use std::fs::File;
use std::io::Write;
use std::os::fd::AsFd;
use std::os::fd::AsRawFd;
use wayrs_client::global::GlobalExt;
use wayrs_client::protocol::wl_seat::WlSeat;
use wayrs_client::{Connection, EventCtx, IoMode};
Expand All @@ -29,7 +30,7 @@ struct CopyEventState<'a> {
source_data: &'a dyn SourceData,
}

struct PasteEventState<'a, T: AsFd + Write> {
struct PasteEventState<'a, T: Write> {
finishied: bool,
result: Option<Error>,
// Stored offers for selection and primary selection (middle-click paste).
Expand Down Expand Up @@ -71,7 +72,7 @@ fn create_wayland_client<T>() -> Result<WaylandClient<T>> {
})
}

pub fn paste_wayland<T: AsFd + Write + 'static>(cfg: PasteConfig<T>) -> Result<()> {
pub fn paste_wayland<T: Write + 'static>(cfg: PasteConfig<T>) -> Result<()> {
let mut client =
create_wayland_client::<PasteEventState<T>>().context("Faild to create wayland client")?;

Expand Down Expand Up @@ -143,9 +144,7 @@ pub fn copy_wayland<T: SourceData>(config: CopyConfig<T>) -> Result<()> {
}

#[allow(clippy::collapsible_match)]
fn wl_device_cb_for_paste<T: AsFd + Write>(
ctx: EventCtx<PasteEventState<T>, ZwlrDataControlDeviceV1>,
) {
fn wl_device_cb_for_paste<T: Write>(ctx: EventCtx<PasteEventState<T>, ZwlrDataControlDeviceV1>) {
macro_rules! unwrap_or_return {
( $e:expr, $report_error:expr) => {
match $e {
Expand Down Expand Up @@ -210,10 +209,6 @@ fn wl_device_cb_for_paste<T: AsFd + Write>(
}
let obj_id = o.unwrap();

let fd = unwrap_or_return!(
ctx.state.config.fd_to_write.as_fd().try_clone_to_owned(),
true
);
let (offer, supported_types) = ctx
.state
.offers
Expand All @@ -224,7 +219,7 @@ fn wl_device_cb_for_paste<T: AsFd + Write>(
// with "-l", list the mime-types and return
if ctx.state.config.list_types_only {
for mt in supported_types {
writeln!(ctx.state.config.fd_to_write, "{}", mt).unwrap()
writeln!(ctx.state.config.writter, "{}", mt).unwrap()
}
ctx.state.finishied = true;
ctx.conn.break_dispatch_loop();
Expand All @@ -237,12 +232,27 @@ fn wl_device_cb_for_paste<T: AsFd + Write>(
);
let mime_type = unwrap_or_return!(CString::new(str), true);

offer.receive(ctx.conn, mime_type, fd);
// offer.receive needs a fd to write, we cannot use the stdin since the read side of the
// pipe may close earlier before all data written.
let fds = unwrap_or_return!(pipe(), true);
offer.receive(ctx.conn, mime_type, fds.1);
// This looks strange, but it is working. It seems offer.receive is a request but nont a
// blocking call, which needs an extra loop to finish. Maybe a callback needs to be set
// to wait until it is processed, but I have no idea how to do that.
// conn.set_callback_for() doesn't work for the offer here.
ctx.conn.blocking_roundtrip().unwrap();
let mut buffer = vec![0; 1024 * 4];
loop {
// Read from the pipe until EOF
let n = unwrap_or_return!(read(fds.0.as_raw_fd(), &mut buffer), true);
if n > 0 {
// Write the content to the destination
unwrap_or_return!(ctx.state.config.writter.write(&buffer[0..n]), true);
} else {
break;
}
}

offer.destroy(ctx.conn);
ctx.state.finishied = true;
ctx.conn.break_dispatch_loop();
Expand Down
12 changes: 5 additions & 7 deletions src/clipboard/x.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use crate::protocol::SourceData;
use anyhow::{bail, Context, Result};
use std::collections::hash_map::HashMap;
use std::io::Write;
use std::os::fd::AsFd;
use std::rc::Rc;
use x11rb::atom_manager;
use x11rb::connection::Connection;
Expand Down Expand Up @@ -42,7 +41,7 @@ struct XClient {
atoms: AtomCollection,
}

struct XPasteState<'a, T: AsFd + Write> {
struct XPasteState<'a, T: Write> {
supported_mime_types: Option<Vec<String>>,
config: PasteConfig<'a, T>,
// Translate the config.primary
Expand Down Expand Up @@ -465,7 +464,7 @@ fn create_x_client(display_name: Option<&str>) -> Result<XClient> {
})
}

pub fn paste_x<T: AsFd + Write + 'static>(config: PasteConfig<T>) -> Result<()> {
pub fn paste_x<T: Write + 'static>(config: PasteConfig<T>) -> Result<()> {
let mut client = create_x_client(None)?;

let selection = if config.use_primary {
Expand Down Expand Up @@ -536,7 +535,7 @@ pub fn paste_x<T: AsFd + Write + 'static>(config: PasteConfig<T>) -> Result<()>
}
if state.config.list_types_only {
for line in mime_types {
writeln!(&mut state.config.fd_to_write, "{}", line)
writeln!(&mut state.config.writter, "{}", line)
.context("Failed to write to the output")?;
}
break;
Expand Down Expand Up @@ -568,7 +567,7 @@ pub fn paste_x<T: AsFd + Write + 'static>(config: PasteConfig<T>) -> Result<()>
} else {
match &mut state.receiver {
Some(receiver) => {
if receiver.receive_and_write(&client, &mut state.config.fd_to_write)?
if receiver.receive_and_write(&client, &mut state.config.writter)?
== TransferResult::Done
{
break;
Expand All @@ -595,8 +594,7 @@ pub fn paste_x<T: AsFd + Write + 'static>(config: PasteConfig<T>) -> Result<()>
};
match &mut state.receiver {
Some(receiver) => {
if receiver
.receive_and_write_incr(&client, &mut state.config.fd_to_write)?
if receiver.receive_and_write_incr(&client, &mut state.config.writter)?
== TransferResult::Done
{
break;
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ fn do_paste(arg_matches: &ArgMatches) -> Result<()> {
use_primary: *arg_matches
.get_one::<bool>("primary")
.context("`--primary` option is not specified for the `paste` command")?,
fd_to_write: &mut stdout(),
writter: &mut stdout(),
expected_mime_type: t.to_string(),
};
match choose_backend() {
Expand Down

0 comments on commit fd6b146

Please sign in to comment.