Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(clipboard): correct clipboard when used through SSH or WSL #20

Merged
merged 2 commits into from
Mar 8, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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(())
}