mirror of https://github.com/helix-editor/helix
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
453 lines
15 KiB
Rust
453 lines
15 KiB
Rust
// Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152
|
|
|
|
use anyhow::Result;
|
|
use std::borrow::Cow;
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub enum ClipboardType {
|
|
Clipboard,
|
|
Selection,
|
|
}
|
|
|
|
pub trait ClipboardProvider: std::fmt::Debug {
|
|
fn name(&self) -> Cow<str>;
|
|
fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String>;
|
|
fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()>;
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
macro_rules! command_provider {
|
|
(paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{
|
|
log::debug!(
|
|
"Using {} to interact with the system clipboard",
|
|
if $set_prg != $get_prg { format!("{}+{}", $set_prg, $get_prg)} else { $set_prg.to_string() }
|
|
);
|
|
Box::new(provider::command::Provider {
|
|
get_cmd: provider::command::Config {
|
|
prg: $get_prg,
|
|
args: &[ $( $get_arg ),* ],
|
|
},
|
|
set_cmd: provider::command::Config {
|
|
prg: $set_prg,
|
|
args: &[ $( $set_arg ),* ],
|
|
},
|
|
get_primary_cmd: None,
|
|
set_primary_cmd: None,
|
|
})
|
|
}};
|
|
|
|
(paste => $get_prg:literal $( , $get_arg:literal )* ;
|
|
copy => $set_prg:literal $( , $set_arg:literal )* ;
|
|
primary_paste => $pr_get_prg:literal $( , $pr_get_arg:literal )* ;
|
|
primary_copy => $pr_set_prg:literal $( , $pr_set_arg:literal )* ;
|
|
) => {{
|
|
log::debug!(
|
|
"Using {} to interact with the system and selection (primary) clipboard",
|
|
if $set_prg != $get_prg { format!("{}+{}", $set_prg, $get_prg)} else { $set_prg.to_string() }
|
|
);
|
|
Box::new(provider::command::Provider {
|
|
get_cmd: provider::command::Config {
|
|
prg: $get_prg,
|
|
args: &[ $( $get_arg ),* ],
|
|
},
|
|
set_cmd: provider::command::Config {
|
|
prg: $set_prg,
|
|
args: &[ $( $set_arg ),* ],
|
|
},
|
|
get_primary_cmd: Some(provider::command::Config {
|
|
prg: $pr_get_prg,
|
|
args: &[ $( $pr_get_arg ),* ],
|
|
}),
|
|
set_primary_cmd: Some(provider::command::Config {
|
|
prg: $pr_set_prg,
|
|
args: &[ $( $pr_set_arg ),* ],
|
|
}),
|
|
})
|
|
}};
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
|
Box::<provider::WindowsProvider>::default()
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
|
use helix_stdx::env::{binary_exists, env_var_is_set};
|
|
|
|
if env_var_is_set("TMUX") && binary_exists("tmux") {
|
|
command_provider! {
|
|
paste => "tmux", "save-buffer", "-";
|
|
copy => "tmux", "load-buffer", "-w", "-";
|
|
}
|
|
} else if binary_exists("pbcopy") && binary_exists("pbpaste") {
|
|
command_provider! {
|
|
paste => "pbpaste";
|
|
copy => "pbcopy";
|
|
}
|
|
} else {
|
|
Box::new(provider::FallbackProvider::new())
|
|
}
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
|
// TODO:
|
|
Box::new(provider::FallbackProvider::new())
|
|
}
|
|
|
|
#[cfg(not(any(windows, target_arch = "wasm32")))]
|
|
// Check if we are in tmux and the allow-passthrough is on.
|
|
fn tmux_allow_passthrough() -> bool {
|
|
use helix_stdx::env::env_var_is_set;
|
|
|
|
if !env_var_is_set("TMUX") {
|
|
return false;
|
|
}
|
|
|
|
use std::process::Command;
|
|
let result = Command::new("tmux")
|
|
.arg("show")
|
|
.arg("-gw")
|
|
.arg("allow-passthrough")
|
|
.output();
|
|
let output = match result {
|
|
Ok(out) => out,
|
|
Err(_) => return false,
|
|
};
|
|
|
|
if !output.status.success() {
|
|
return false;
|
|
}
|
|
|
|
let output = match String::from_utf8(output.stdout) {
|
|
Ok(out) => out,
|
|
Err(_) => return false,
|
|
};
|
|
|
|
output.contains("on")
|
|
}
|
|
|
|
#[cfg(not(any(windows, target_os = "wasm32", target_os = "macos")))]
|
|
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
|
use helix_stdx::env::{binary_exists, env_var_is_set};
|
|
use provider::command::is_exit_success;
|
|
// TODO: support for user-defined provider, probably when we have plugin support by setting a
|
|
// variable?
|
|
|
|
if env_var_is_set("WAYLAND_DISPLAY") && binary_exists("wl-copy") && binary_exists("wl-paste") {
|
|
command_provider! {
|
|
paste => "wl-paste", "--no-newline";
|
|
copy => "wl-copy", "--type", "text/plain";
|
|
primary_paste => "wl-paste", "-p", "--no-newline";
|
|
primary_copy => "wl-copy", "-p", "--type", "text/plain";
|
|
}
|
|
} else if env_var_is_set("DISPLAY") && binary_exists("xclip") {
|
|
command_provider! {
|
|
paste => "xclip", "-o", "-selection", "clipboard";
|
|
copy => "xclip", "-i", "-selection", "clipboard";
|
|
primary_paste => "xclip", "-o";
|
|
primary_copy => "xclip", "-i";
|
|
}
|
|
} else if env_var_is_set("DISPLAY")
|
|
&& binary_exists("xsel")
|
|
&& is_exit_success("xsel", &["-o", "-b"])
|
|
{
|
|
// FIXME: check performance of is_exit_success
|
|
command_provider! {
|
|
paste => "xsel", "-o", "-b";
|
|
copy => "xsel", "-i", "-b";
|
|
primary_paste => "xsel", "-o";
|
|
primary_copy => "xsel", "-i";
|
|
}
|
|
} else if binary_exists("win32yank.exe") {
|
|
command_provider! {
|
|
paste => "win32yank.exe", "-o", "--lf";
|
|
copy => "win32yank.exe", "-i", "--crlf";
|
|
}
|
|
} else if binary_exists("termux-clipboard-set") && binary_exists("termux-clipboard-get") {
|
|
command_provider! {
|
|
paste => "termux-clipboard-get";
|
|
copy => "termux-clipboard-set";
|
|
}
|
|
} else if env_var_is_set("TMUX") && binary_exists("tmux") && !tmux_allow_passthrough() {
|
|
command_provider! {
|
|
paste => "tmux", "save-buffer", "-";
|
|
copy => "tmux", "load-buffer", "-w", "-";
|
|
}
|
|
} else {
|
|
Box::new(provider::FallbackProvider::new())
|
|
}
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub mod provider {
|
|
use super::{ClipboardProvider, ClipboardType};
|
|
use anyhow::Result;
|
|
use std::borrow::Cow;
|
|
|
|
#[cfg(feature = "term")]
|
|
mod osc52 {
|
|
use crate::clipboard::tmux_allow_passthrough;
|
|
use {super::ClipboardType, crate::base64};
|
|
|
|
#[derive(Debug)]
|
|
pub struct SetClipboardCommand {
|
|
encoded_content: String,
|
|
clipboard_type: ClipboardType,
|
|
}
|
|
|
|
impl SetClipboardCommand {
|
|
pub fn new(content: &str, clipboard_type: ClipboardType) -> Self {
|
|
Self {
|
|
encoded_content: base64::encode(content.as_bytes()),
|
|
clipboard_type,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl crossterm::Command for SetClipboardCommand {
|
|
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
|
|
let kind = match &self.clipboard_type {
|
|
ClipboardType::Clipboard => "c",
|
|
ClipboardType::Selection => "p",
|
|
};
|
|
|
|
// Send an OSC 52 set command: https://terminalguide.namepad.de/seq/osc-52/
|
|
let mut osc52 = format!("\x1b]52;{};{}\x07", kind, &self.encoded_content);
|
|
if tmux_allow_passthrough() {
|
|
// If we are inside tmux and we are allow to passthrough, escape it too.
|
|
osc52 = format!("\x1bPtmux;\x1b{}\x1b\\", osc52);
|
|
}
|
|
f.write_str(&osc52)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct FallbackProvider {
|
|
buf: String,
|
|
primary_buf: String,
|
|
}
|
|
|
|
impl FallbackProvider {
|
|
pub fn new() -> Self {
|
|
#[cfg(feature = "term")]
|
|
log::debug!(
|
|
"No native clipboard provider found. Yanking by OSC 52 and pasting will be internal to Helix"
|
|
);
|
|
#[cfg(not(feature = "term"))]
|
|
log::warn!(
|
|
"No native clipboard provider found! Yanking and pasting will be internal to Helix"
|
|
);
|
|
Self {
|
|
buf: String::new(),
|
|
primary_buf: String::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for FallbackProvider {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl ClipboardProvider for FallbackProvider {
|
|
#[cfg(feature = "term")]
|
|
fn name(&self) -> Cow<str> {
|
|
Cow::Borrowed("termcode")
|
|
}
|
|
|
|
#[cfg(not(feature = "term"))]
|
|
fn name(&self) -> Cow<str> {
|
|
Cow::Borrowed("none")
|
|
}
|
|
|
|
fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
|
|
// This is the same noop if term is enabled or not.
|
|
// We don't use the get side of OSC 52 as it isn't often enabled, it's a security hole,
|
|
// and it would require this to be async to listen for the response
|
|
let value = match clipboard_type {
|
|
ClipboardType::Clipboard => self.buf.clone(),
|
|
ClipboardType::Selection => self.primary_buf.clone(),
|
|
};
|
|
|
|
Ok(value)
|
|
}
|
|
|
|
fn set_contents(&mut self, content: String, clipboard_type: ClipboardType) -> Result<()> {
|
|
#[cfg(feature = "term")]
|
|
crossterm::execute!(
|
|
std::io::stdout(),
|
|
osc52::SetClipboardCommand::new(&content, clipboard_type)
|
|
)?;
|
|
// Set our internal variables to use in get_content regardless of using OSC 52
|
|
match clipboard_type {
|
|
ClipboardType::Clipboard => self.buf = content,
|
|
ClipboardType::Selection => self.primary_buf = content,
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub mod command {
|
|
use super::*;
|
|
use anyhow::{bail, Context as _};
|
|
|
|
#[cfg(not(any(windows, target_os = "macos")))]
|
|
pub fn is_exit_success(program: &str, args: &[&str]) -> bool {
|
|
std::process::Command::new(program)
|
|
.args(args)
|
|
.output()
|
|
.ok()
|
|
.and_then(|out| out.status.success().then_some(()))
|
|
.is_some()
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Config {
|
|
pub prg: &'static str,
|
|
pub args: &'static [&'static str],
|
|
}
|
|
|
|
impl Config {
|
|
fn execute(&self, input: Option<&str>, pipe_output: bool) -> Result<Option<String>> {
|
|
use std::io::Write;
|
|
use std::process::{Command, Stdio};
|
|
|
|
let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null);
|
|
let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null);
|
|
|
|
let mut command: Command = Command::new(self.prg);
|
|
|
|
let mut command_mut: &mut Command = command
|
|
.args(self.args)
|
|
.stdin(stdin)
|
|
.stdout(stdout)
|
|
.stderr(Stdio::null());
|
|
|
|
// Fix for https://github.com/helix-editor/helix/issues/5424
|
|
if cfg!(unix) {
|
|
use std::os::unix::process::CommandExt;
|
|
|
|
unsafe {
|
|
command_mut = command_mut.pre_exec(|| match libc::setsid() {
|
|
-1 => Err(std::io::Error::last_os_error()),
|
|
_ => Ok(()),
|
|
});
|
|
}
|
|
}
|
|
|
|
let mut child = command_mut.spawn()?;
|
|
|
|
if let Some(input) = input {
|
|
let mut stdin = child.stdin.take().context("stdin is missing")?;
|
|
stdin
|
|
.write_all(input.as_bytes())
|
|
.context("couldn't write in stdin")?;
|
|
}
|
|
|
|
// TODO: add timer?
|
|
let output = child.wait_with_output()?;
|
|
|
|
if !output.status.success() {
|
|
bail!("clipboard provider {} failed", self.prg);
|
|
}
|
|
|
|
if pipe_output {
|
|
Ok(Some(String::from_utf8(output.stdout)?))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Provider {
|
|
pub get_cmd: Config,
|
|
pub set_cmd: Config,
|
|
pub get_primary_cmd: Option<Config>,
|
|
pub set_primary_cmd: Option<Config>,
|
|
}
|
|
|
|
impl ClipboardProvider for Provider {
|
|
fn name(&self) -> Cow<str> {
|
|
if self.get_cmd.prg != self.set_cmd.prg {
|
|
Cow::Owned(format!("{}+{}", self.get_cmd.prg, self.set_cmd.prg))
|
|
} else {
|
|
Cow::Borrowed(self.get_cmd.prg)
|
|
}
|
|
}
|
|
|
|
fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
|
|
match clipboard_type {
|
|
ClipboardType::Clipboard => Ok(self
|
|
.get_cmd
|
|
.execute(None, true)?
|
|
.context("output is missing")?),
|
|
ClipboardType::Selection => {
|
|
if let Some(cmd) = &self.get_primary_cmd {
|
|
return cmd.execute(None, true)?.context("output is missing");
|
|
}
|
|
|
|
Ok(String::new())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set_contents(&mut self, value: String, clipboard_type: ClipboardType) -> Result<()> {
|
|
let cmd = match clipboard_type {
|
|
ClipboardType::Clipboard => &self.set_cmd,
|
|
ClipboardType::Selection => {
|
|
if let Some(cmd) = &self.set_primary_cmd {
|
|
cmd
|
|
} else {
|
|
return Ok(());
|
|
}
|
|
}
|
|
};
|
|
cmd.execute(Some(&value), false).map(|_| ())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
mod provider {
|
|
use super::{ClipboardProvider, ClipboardType};
|
|
use anyhow::Result;
|
|
use std::borrow::Cow;
|
|
|
|
#[derive(Default, Debug)]
|
|
pub struct WindowsProvider;
|
|
|
|
impl ClipboardProvider for WindowsProvider {
|
|
fn name(&self) -> Cow<str> {
|
|
log::debug!("Using clipboard-win to interact with the system clipboard");
|
|
Cow::Borrowed("clipboard-win")
|
|
}
|
|
|
|
fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
|
|
match clipboard_type {
|
|
ClipboardType::Clipboard => {
|
|
let contents = clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?;
|
|
Ok(contents)
|
|
}
|
|
ClipboardType::Selection => Ok(String::new()),
|
|
}
|
|
}
|
|
|
|
fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()> {
|
|
match clipboard_type {
|
|
ClipboardType::Clipboard => {
|
|
clipboard_win::set_clipboard(clipboard_win::formats::Unicode, contents)?;
|
|
}
|
|
ClipboardType::Selection => {}
|
|
};
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|