Skip to content

Commit

Permalink
fix(clipboard): correct clipboard when used through SSH or WSL (#20)
Browse files Browse the repository at this point in the history
* use arbroad clipboard lazily and correct process spawn on WSL

* use standard library instead of once_cell
  • Loading branch information
jboillot committed Mar 8, 2023
1 parent 5b99546 commit 9cc501a
Showing 1 changed file with 62 additions and 22 deletions.
84 changes: 62 additions & 22 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,43 @@
#[macro_use]
extern crate napi_derive;

use base64::{engine::general_purpose, Engine as _};
use duct::cmd;
use std::borrow::Cow;
use std::cell::Cell;
use std::env;
use std::io::Write;
use std::process::{Command, Stdio};
use duct::cmd;

use napi::Status::GenericFailure;
use napi::{bindgen_prelude::*, JsBuffer};

#[napi]
pub struct Clipboard {
inner: arboard::Clipboard,
lazy_inner: Cell<Option<arboard::Clipboard>>,
}

fn clipboard_error_to_js_error(err: arboard::Error) -> Error {
Error::new(Status::GenericFailure, format!("{err}"))
Error::new(GenericFailure, format!("{err}"))
}

#[napi]
impl Clipboard {
#[napi(constructor)]
pub fn new() -> Result<Self> {
Ok(Clipboard {
inner: arboard::Clipboard::new().map_err(clipboard_error_to_js_error)?,
lazy_inner: Cell::new(None),
})
}

fn inner(&mut self) -> std::result::Result<&mut arboard::Clipboard, arboard::Error> {
if self.lazy_inner.get_mut().is_none() {
let clipboard = arboard::Clipboard::new()?;
self.lazy_inner.set(Some(clipboard))
};
Ok(self.lazy_inner.get_mut().as_mut().unwrap())
}

/// Copy text to the clipboard. Has special handling for WSL and SSH sessions, otherwise
/// falls back to the cross-platform `clipboard` crate
#[napi]
Expand All @@ -41,8 +52,8 @@ impl Clipboard {
} else {
// we're probably running on a host/primary OS, so use the default clipboard
self
.inner
.set_text(text)
.inner()
.and_then(|clipboard| clipboard.set_text(text))
.map_err(clipboard_error_to_js_error)?;
}

Expand All @@ -55,19 +66,22 @@ impl Clipboard {
let stdout = cmd!("powershell.exe", "get-clipboard").read()?;
Ok(stdout.trim().to_string())
} else if env::var("SSH_CLIENT").is_ok() {
Err(Error::new(Status::GenericFailure, "SSH clipboard not supported"))
Err(Error::new(GenericFailure, "SSH clipboard not supported"))
} else {
// we're probably running on a host/primary OS, so use the default clipboard
self.inner.get_text().map_err(clipboard_error_to_js_error)
self
.inner()
.and_then(|clipboard| clipboard.get_text())
.map_err(clipboard_error_to_js_error)
}
}

#[napi]
/// Returns a buffer contains the raw RGBA pixels data
pub fn get_image(&mut self, env: Env) -> Result<JsBuffer> {
self
.inner
.get_image()
.inner()
.and_then(|clipboard| clipboard.get_image())
.map_err(clipboard_error_to_js_error)
.and_then(|image| unsafe {
env.create_buffer_with_borrowed_data(
Expand All @@ -86,30 +100,56 @@ impl Clipboard {
/// RGBA bytes
pub fn set_image(&mut self, width: u32, height: u32, image: Buffer) -> Result<()> {
self
.inner
.set_image(arboard::ImageData {
width: width as usize,
height: height as usize,
bytes: Cow::Borrowed(image.as_ref()),
.inner()
.and_then(|clipboard| {
clipboard.set_image(arboard::ImageData {
width: width as usize,
height: height as usize,
bytes: Cow::Borrowed(image.as_ref()),
})
})
.map_err(clipboard_error_to_js_error)
}
}

/// Set the clipboard contents using OSC 52 (picked up by most terminals)
fn set_clipboard_osc_52(text: String) {
print!("\x1B]52;c;{}\x07", base64::encode(text));
print!("\x1B]52;c;{}\x07", general_purpose::STANDARD.encode(text));
}

/// Set the Windows clipboard using clip.exe in WSL
fn set_wsl_clipboard(s: String) -> Result<()> {
let mut clipboard = Command::new("clip.exe").stdin(Stdio::piped()).spawn()?;
let mut clipboard_stdin = clipboard
.stdin
.take()
.ok_or_else(|| Error::new(Status::GenericFailure, "Could not get stdin handle for clip.exe"))?;
let mut clipboard = Command::new("clip.exe")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
{
let mut clipboard_stdin = clipboard
.stdin
.take()
.ok_or_else(|| Error::new(GenericFailure, "Could not get stdin handle for clip.exe"))?;
clipboard_stdin.write_all(s.as_bytes())?;
}

clipboard_stdin.write_all(s.as_bytes())?;
clipboard
.wait()
.map_err(|err| {
Error::new(
GenericFailure,
format!("Could not wait for clip.exe, reason: {err}"),
)
})
.and_then(|status| {
if status.success() {
Ok(())
} else {
Err(Error::new(
GenericFailure,
format!("clip.exe stopped with status {status}"),
))
}
})?;

Ok(())
}

0 comments on commit 9cc501a

Please sign in to comment.