Unnamed repository; edit this file 'description' to name the repository.
Add clipboard provider configuration (#10839)
Alfie Richards 2024-11-21
parent b6e555a · commit 68ee876
-rw-r--r--book/src/editor.md24
-rw-r--r--helix-term/src/commands/typed.rs2
-rw-r--r--helix-term/src/health.rs19
-rw-r--r--helix-view/src/clipboard.rs754
-rw-r--r--helix-view/src/editor.rs9
-rw-r--r--helix-view/src/register.rs39
6 files changed, 488 insertions, 359 deletions
diff --git a/book/src/editor.md b/book/src/editor.md
index 82d5f846..3edc38fc 100644
--- a/book/src/editor.md
+++ b/book/src/editor.md
@@ -52,6 +52,30 @@
| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
| `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"`
| `end-of-line-diagnostics` | Minimum severity of diagnostics to render at the end of the line. Set to `disable` to disable entirely. Refer to the setting about `inline-diagnostics` for more details | "disable"
+| `clipboard-provider` | Which API to use for clipboard interaction. One of `pasteboard` (MacOS), `wayland`, `x-clip`, `x-sel`, `win-32-yank`, `termux`, `tmux`, `windows`, `termcode`, `none`, or a custom command set. | Platform and environment specific. |
+
+### `[editor.clipboard-provider]` Section
+
+Helix can be configured wither to use a builtin clipboard configuration or to use
+a provided command.
+
+For instance, setting it to use OSC 52 termcodes, the configuration would be:
+```toml
+[editor]
+clipboard-provider = "termcode"
+```
+
+Alternatively, Helix can be configured to use arbitary commands for clipboard integration:
+
+```toml
+[editor.clipboard-provider.custom]
+yank = { command = "cat", args = ["test.txt"] }
+paste = { command = "tee", args = ["test.txt"] }
+primary-yank = { command = "cat", args = ["test-primary.txt"] } # optional
+primary-paste = { command = "tee", args = ["test-primary.txt"] } # optional
+```
+
+For custom commands the contents of the yank/paste is communicated over stdin/stdout.
### `[editor.statusline]` Section
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index 68ba9bab..7402a06f 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -1074,7 +1074,7 @@ fn show_clipboard_provider(
}
cx.editor
- .set_status(cx.editor.registers.clipboard_provider_name().to_string());
+ .set_status(cx.editor.registers.clipboard_provider_name());
Ok(())
}
diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs
index 0bbb5735..e59fd74d 100644
--- a/helix-term/src/health.rs
+++ b/helix-term/src/health.rs
@@ -1,10 +1,10 @@
+use crate::config::{Config, ConfigLoadError};
use crossterm::{
style::{Color, Print, Stylize},
tty::IsTty,
};
use helix_core::config::{default_lang_config, user_lang_config};
use helix_loader::grammar::load_runtime_file;
-use helix_view::clipboard::get_clipboard_provider;
use std::io::Write;
#[derive(Copy, Clone)]
@@ -53,7 +53,6 @@ pub fn general() -> std::io::Result<()> {
let lang_file = helix_loader::lang_config_file();
let log_file = helix_loader::log_file();
let rt_dirs = helix_loader::runtime_dirs();
- let clipboard_provider = get_clipboard_provider();
if config_file.exists() {
writeln!(stdout, "Config file: {}", config_file.display())?;
@@ -92,7 +91,6 @@ pub fn general() -> std::io::Result<()> {
writeln!(stdout, "{}", msg.yellow())?;
}
}
- writeln!(stdout, "Clipboard provider: {}", clipboard_provider.name())?;
Ok(())
}
@@ -101,8 +99,19 @@ pub fn clipboard() -> std::io::Result<()> {
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
- let board = get_clipboard_provider();
- match board.name().as_ref() {
+ let config = match Config::load_default() {
+ Ok(config) => config,
+ Err(ConfigLoadError::Error(err)) if err.kind() == std::io::ErrorKind::NotFound => {
+ Config::default()
+ }
+ Err(err) => {
+ writeln!(stdout, "{}", "Configuration file malformed".red())?;
+ writeln!(stdout, "{}", err)?;
+ return Ok(());
+ }
+ };
+
+ match config.editor.clipboard_provider.name().as_ref() {
"none" => {
writeln!(
stdout,
diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs
index 379accc7..234b91fe 100644
--- a/helix-view/src/clipboard.rs
+++ b/helix-view/src/clipboard.rs
@@ -1,356 +1,224 @@
// Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152
-use anyhow::Result;
+use serde::{Deserialize, Serialize};
use std::borrow::Cow;
+use thiserror::Error;
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy)]
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<()>;
+#[derive(Debug, Error)]
+pub enum ClipboardError {
+ #[error(transparent)]
+ IoError(#[from] std::io::Error),
+ #[error("could not convert terminal output to UTF-8: {0}")]
+ FromUtf8Error(#[from] std::string::FromUtf8Error),
+ #[cfg(windows)]
+ #[error("Windows API error: {0}")]
+ WinAPI(#[from] clipboard_win::ErrorCode),
+ #[error("clipboard provider command failed")]
+ CommandFailed,
+ #[error("failed to write to clipboard provider's stdin")]
+ StdinWriteFailed,
+ #[error("clipboard provider did not return any contents")]
+ MissingStdout,
+ #[error("This clipboard provider does not support reading")]
+ ReadingNotSupported,
}
-#[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())
- }
-}
+type Result<T> = std::result::Result<T, ClipboardError>;
+#[cfg(not(target_arch = "wasm32"))]
+pub use external::ClipboardProvider;
#[cfg(target_arch = "wasm32")]
-pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
- // TODO:
- Box::new(provider::FallbackProvider::new())
-}
+pub use noop::ClipboardProvider;
-#[cfg(not(any(windows, target_arch = "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") {
- command_provider! {
- paste => "tmux", "save-buffer", "-";
- copy => "tmux", "load-buffer", "-w", "-";
- }
- } else {
- Box::new(provider::FallbackProvider::new())
- }
-}
+// Clipboard not supported for wasm
+#[cfg(target_arch = "wasm32")]
+mod noop {
+ use super::*;
-#[cfg(not(target_os = "windows"))]
-pub mod provider {
- use super::{ClipboardProvider, ClipboardType};
- use anyhow::Result;
- use std::borrow::Cow;
+ #[derive(Debug, Clone)]
+ pub enum ClipboardProvider {}
- #[cfg(feature = "term")]
- mod osc52 {
- use {super::ClipboardType, crate::base64};
+ impl ClipboardProvider {
+ pub fn detect() -> Self {
+ Self
+ }
- #[derive(Debug)]
- pub struct SetClipboardCommand {
- encoded_content: String,
- clipboard_type: ClipboardType,
+ pub fn name(&self) -> Cow<str> {
+ "none".into()
}
- impl SetClipboardCommand {
- pub fn new(content: &str, clipboard_type: ClipboardType) -> Self {
- Self {
- encoded_content: base64::encode(content.as_bytes()),
- clipboard_type,
- }
- }
+ pub fn get_contents(&self, _clipboard_type: ClipboardType) -> Result<String> {
+ Err(ClipboardError::ReadingNotSupported)
}
- 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/
- write!(f, "\x1b]52;{};{}\x1b\\", kind, &self.encoded_content)
- }
+ pub fn set_contents(&self, _content: &str, _clipboard_type: ClipboardType) -> Result<()> {
+ Ok(())
}
}
+}
- #[derive(Debug)]
- pub struct FallbackProvider {
- buf: String,
- primary_buf: String,
- }
+#[cfg(not(target_arch = "wasm32"))]
+mod external {
+ use super::*;
- 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(),
- }
- }
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+ pub struct Command {
+ command: Cow<'static, str>,
+ #[serde(default)]
+ args: Cow<'static, [Cow<'static, str>]>,
}
- impl Default for FallbackProvider {
- fn default() -> Self {
- Self::new()
- }
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+ #[serde(rename_all = "kebab-case")]
+ pub struct CommandProvider {
+ yank: Command,
+ paste: Command,
+ yank_primary: Option<Command>,
+ paste_primary: Option<Command>,
}
- impl ClipboardProvider for FallbackProvider {
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+ #[serde(rename_all = "kebab-case")]
+ #[allow(clippy::large_enum_variant)]
+ pub enum ClipboardProvider {
+ Pasteboard,
+ Wayland,
+ XClip,
+ XSel,
+ Win32Yank,
+ Tmux,
+ #[cfg(windows)]
+ Windows,
+ Termux,
#[cfg(feature = "term")]
- fn name(&self) -> Cow<str> {
- Cow::Borrowed("termcode")
- }
-
- #[cfg(not(feature = "term"))]
- fn name(&self) -> Cow<str> {
- Cow::Borrowed("none")
- }
+ Termcode,
+ Custom(CommandProvider),
+ 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(),
- };
+ impl Default for ClipboardProvider {
+ #[cfg(windows)]
+ fn default() -> Self {
+ use helix_stdx::env::binary_exists;
- Ok(value)
+ if binary_exists("win32yank.exe") {
+ Self::Win32Yank
+ } else {
+ Self::Windows
+ }
}
- 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,
+ #[cfg(target_os = "macos")]
+ fn default() -> Self {
+ use helix_stdx::env::{binary_exists, env_var_is_set};
+
+ if env_var_is_set("TMUX") && binary_exists("tmux") {
+ Self::Tmux
+ } else if binary_exists("pbcopy") && binary_exists("pbpaste") {
+ Self::Pasteboard
+ } else if cfg!(feature = "term") {
+ Self::Termcode
+ } else {
+ Self::None
}
- 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()
- }
+ fn default() -> Self {
+ use helix_stdx::env::{binary_exists, env_var_is_set};
+
+ 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],
+ if env_var_is_set("WAYLAND_DISPLAY")
+ && binary_exists("wl-copy")
+ && binary_exists("wl-paste")
+ {
+ Self::Wayland
+ } else if env_var_is_set("DISPLAY") && binary_exists("xclip") {
+ Self::XClip
+ } else if env_var_is_set("DISPLAY")
+ && binary_exists("xsel")
+ // FIXME: check performance of is_exit_success
+ && is_exit_success("xsel", &["-o", "-b"])
+ {
+ Self::XSel
+ } else if binary_exists("termux-clipboard-set") && binary_exists("termux-clipboard-get")
+ {
+ Self::Termux
+ } else if env_var_is_set("TMUX") && binary_exists("tmux") {
+ Self::Tmux
+ } else if binary_exists("win32yank.exe") {
+ Self::Win32Yank
+ } else if cfg!(feature = "term") {
+ Self::Termcode
+ } else {
+ Self::None
+ }
}
+ }
- 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)?))
+ impl ClipboardProvider {
+ pub fn name(&self) -> Cow<'_, str> {
+ fn builtin_name<'a>(
+ name: &'static str,
+ provider: &'static CommandProvider,
+ ) -> Cow<'a, str> {
+ if provider.yank.command != provider.paste.command {
+ Cow::Owned(format!(
+ "{} ({}+{})",
+ name, provider.yank.command, provider.paste.command
+ ))
} else {
- Ok(None)
+ Cow::Owned(format!("{} ({})", name, provider.yank.command))
}
}
- }
-
- #[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)
- }
+ match self {
+ // These names should match the config option names from Serde
+ Self::Pasteboard => builtin_name("pasteboard", &PASTEBOARD),
+ Self::Wayland => builtin_name("wayland", &WL_CLIPBOARD),
+ Self::XClip => builtin_name("x-clip", &WL_CLIPBOARD),
+ Self::XSel => builtin_name("x-sel", &WL_CLIPBOARD),
+ Self::Win32Yank => builtin_name("win-32-yank", &WL_CLIPBOARD),
+ Self::Tmux => builtin_name("tmux", &TMUX),
+ Self::Termux => builtin_name("termux", &TERMUX),
+ #[cfg(windows)]
+ Self::Windows => "windows".into(),
+ #[cfg(feature = "term")]
+ Self::Termcode => "termcode".into(),
+ Self::Custom(command_provider) => Cow::Owned(format!(
+ "custom ({}+{})",
+ command_provider.yank.command, command_provider.paste.command
+ )),
+ Self::None => "none".into(),
}
+ }
- fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
+ pub fn get_contents(&self, clipboard_type: &ClipboardType) -> Result<String> {
+ fn yank_from_builtin(
+ provider: CommandProvider,
+ clipboard_type: &ClipboardType,
+ ) -> Result<String> {
match clipboard_type {
- ClipboardType::Clipboard => Ok(self
- .get_cmd
- .execute(None, true)?
- .context("output is missing")?),
+ ClipboardType::Clipboard => execute_command(&provider.yank, None, true)?
+ .ok_or(ClipboardError::MissingStdout),
ClipboardType::Selection => {
- if let Some(cmd) = &self.get_primary_cmd {
- return cmd.execute(None, true)?.context("output is missing");
+ if let Some(cmd) = provider.yank_primary.as_ref() {
+ return execute_command(cmd, None, true)?
+ .ok_or(ClipboardError::MissingStdout);
}
Ok(String::new())
@@ -358,56 +226,274 @@ pub mod provider {
}
}
- fn set_contents(&mut self, value: String, clipboard_type: ClipboardType) -> Result<()> {
+ match self {
+ Self::Pasteboard => yank_from_builtin(PASTEBOARD, clipboard_type),
+ Self::Wayland => yank_from_builtin(WL_CLIPBOARD, clipboard_type),
+ Self::XClip => yank_from_builtin(XCLIP, clipboard_type),
+ Self::XSel => yank_from_builtin(XSEL, clipboard_type),
+ Self::Win32Yank => yank_from_builtin(WIN32, clipboard_type),
+ Self::Tmux => yank_from_builtin(TMUX, clipboard_type),
+ Self::Termux => yank_from_builtin(TERMUX, clipboard_type),
+ #[cfg(target_os = "windows")]
+ Self::Windows => match clipboard_type {
+ ClipboardType::Clipboard => {
+ let contents =
+ clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?;
+ Ok(contents)
+ }
+ ClipboardType::Selection => Ok(String::new()),
+ },
+ #[cfg(feature = "term")]
+ Self::Termcode => Err(ClipboardError::ReadingNotSupported),
+ Self::Custom(command_provider) => {
+ execute_command(&command_provider.yank, None, true)?
+ .ok_or(ClipboardError::MissingStdout)
+ }
+ Self::None => Err(ClipboardError::ReadingNotSupported),
+ }
+ }
+
+ pub fn set_contents(&self, content: &str, clipboard_type: ClipboardType) -> Result<()> {
+ fn paste_to_builtin(
+ provider: CommandProvider,
+ content: &str,
+ clipboard_type: ClipboardType,
+ ) -> Result<()> {
let cmd = match clipboard_type {
- ClipboardType::Clipboard => &self.set_cmd,
+ ClipboardType::Clipboard => &provider.paste,
ClipboardType::Selection => {
- if let Some(cmd) = &self.set_primary_cmd {
+ if let Some(cmd) = provider.paste_primary.as_ref() {
cmd
} else {
return Ok(());
}
}
};
- cmd.execute(Some(&value), false).map(|_| ())
+
+ execute_command(cmd, Some(content), false).map(|_| ())
+ }
+
+ match self {
+ Self::Pasteboard => paste_to_builtin(PASTEBOARD, content, clipboard_type),
+ Self::Wayland => paste_to_builtin(WL_CLIPBOARD, content, clipboard_type),
+ Self::XClip => paste_to_builtin(XCLIP, content, clipboard_type),
+ Self::XSel => paste_to_builtin(XSEL, content, clipboard_type),
+ Self::Win32Yank => paste_to_builtin(WIN32, content, clipboard_type),
+ Self::Tmux => paste_to_builtin(TMUX, content, clipboard_type),
+ Self::Termux => paste_to_builtin(TERMUX, content, clipboard_type),
+ #[cfg(target_os = "windows")]
+ Self::Windows => match clipboard_type {
+ ClipboardType::Clipboard => {
+ clipboard_win::set_clipboard(clipboard_win::formats::Unicode, content)?;
+ Ok(())
+ }
+ ClipboardType::Selection => Ok(()),
+ },
+ #[cfg(feature = "term")]
+ Self::Termcode => {
+ crossterm::queue!(
+ std::io::stdout(),
+ osc52::SetClipboardCommand::new(content, clipboard_type)
+ )?;
+ Ok(())
+ }
+ Self::Custom(command_provider) => match clipboard_type {
+ ClipboardType::Clipboard => {
+ execute_command(&command_provider.paste, Some(content), false).map(|_| ())
+ }
+ ClipboardType::Selection => {
+ if let Some(cmd) = &command_provider.paste_primary {
+ execute_command(cmd, Some(content), false).map(|_| ())
+ } else {
+ Ok(())
+ }
+ }
+ },
+ Self::None => Ok(()),
}
}
}
-}
-#[cfg(target_os = "windows")]
-mod provider {
- use super::{ClipboardProvider, ClipboardType};
- use anyhow::Result;
- use std::borrow::Cow;
+ macro_rules! command_provider {
+ ($name:ident,
+ yank => $yank_cmd:literal $( , $yank_arg:literal )* ;
+ paste => $paste_cmd:literal $( , $paste_arg:literal )* ; ) => {
+ const $name: CommandProvider = CommandProvider {
+ yank: Command {
+ command: Cow::Borrowed($yank_cmd),
+ args: Cow::Borrowed(&[ $( Cow::Borrowed($yank_arg) ),* ])
+ },
+ paste: Command {
+ command: Cow::Borrowed($paste_cmd),
+ args: Cow::Borrowed(&[ $( Cow::Borrowed($paste_arg) ),* ])
+ },
+ yank_primary: None,
+ paste_primary: None,
+ };
+ };
+ ($name:ident,
+ yank => $yank_cmd:literal $( , $yank_arg:literal )* ;
+ paste => $paste_cmd:literal $( , $paste_arg:literal )* ;
+ yank_primary => $yank_primary_cmd:literal $( , $yank_primary_arg:literal )* ;
+ paste_primary => $paste_primary_cmd:literal $( , $paste_primary_arg:literal )* ; ) => {
+ const $name: CommandProvider = CommandProvider {
+ yank: Command {
+ command: Cow::Borrowed($yank_cmd),
+ args: Cow::Borrowed(&[ $( Cow::Borrowed($yank_arg) ),* ])
+ },
+ paste: Command {
+ command: Cow::Borrowed($paste_cmd),
+ args: Cow::Borrowed(&[ $( Cow::Borrowed($paste_arg) ),* ])
+ },
+ yank_primary: Some(Command {
+ command: Cow::Borrowed($yank_primary_cmd),
+ args: Cow::Borrowed(&[ $( Cow::Borrowed($yank_primary_arg) ),* ])
+ }),
+ paste_primary: Some(Command {
+ command: Cow::Borrowed($paste_primary_cmd),
+ args: Cow::Borrowed(&[ $( Cow::Borrowed($paste_primary_arg) ),* ])
+ }),
+ };
+ };
+ }
- #[derive(Default, Debug)]
- pub struct WindowsProvider;
+ command_provider! {
+ TMUX,
+ yank => "tmux", "load-buffer", "-w", "-";
+ paste => "tmux", "save-buffer", "-";
+ }
+ command_provider! {
+ PASTEBOARD,
+ yank => "pbcopy";
+ paste => "pbpaste";
+ }
+ command_provider! {
+ WL_CLIPBOARD,
+ yank => "wl-copy", "--type", "text/plain";
+ paste => "wl-paste", "--no-newline";
+ yank_primary => "wl-copy", "-p", "--type", "text/plain";
+ paste_primary => "wl-paste", "-p", "--no-newline";
+ }
+ command_provider! {
+ XCLIP,
+ yank => "xclip", "-i", "-selection", "clipboard";
+ paste => "xclip", "-o", "-selection", "clipboard";
+ yank_primary => "xclip", "-i";
+ paste_primary => "xclip", "-o";
+ }
+ command_provider! {
+ XSEL,
+ yank => "xsel", "-i", "-b";
+ paste => "xsel", "-o", "-b";
+ yank_primary => "xsel", "-i";
+ paste_primary => "xsel", "-o";
+ }
+ command_provider! {
+ WIN32,
+ yank => "win32yank.exe", "-i", "--crlf";
+ paste => "win32yank.exe", "-o", "--lf";
+ }
+ command_provider! {
+ TERMUX,
+ yank => "termux-clipboard-set";
+ paste => "termux-clipboard-get";
+ }
- impl ClipboardProvider for WindowsProvider {
- fn name(&self) -> Cow<str> {
- log::debug!("Using clipboard-win to interact with the system clipboard");
- Cow::Borrowed("clipboard-win")
+ #[cfg(feature = "term")]
+ mod osc52 {
+ use {super::ClipboardType, crate::base64};
+
+ pub struct SetClipboardCommand {
+ encoded_content: String,
+ clipboard_type: ClipboardType,
}
- 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)
+ impl SetClipboardCommand {
+ pub fn new(content: &str, clipboard_type: ClipboardType) -> Self {
+ Self {
+ encoded_content: base64::encode(content.as_bytes()),
+ clipboard_type,
}
- 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(())
+ 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/
+ write!(f, "\x1b]52;{};{}\x1b\\", kind, &self.encoded_content)
+ }
+ #[cfg(windows)]
+ fn execute_winapi(&self) -> std::result::Result<(), std::io::Error> {
+ Err(std::io::Error::new(
+ std::io::ErrorKind::Other,
+ "OSC clipboard codes not supported by winapi.",
+ ))
+ }
+ }
+ }
+
+ fn execute_command(
+ cmd: &Command,
+ 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(cmd.command.as_ref());
+
+ #[allow(unused_mut)]
+ let mut command_mut: &mut Command = command
+ .args(cmd.args.iter().map(AsRef::as_ref))
+ .stdin(stdin)
+ .stdout(stdout)
+ .stderr(Stdio::null());
+
+ // Fix for https://github.com/helix-editor/helix/issues/5424
+ #[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().ok_or(ClipboardError::StdinWriteFailed)?;
+ stdin
+ .write_all(input.as_bytes())
+ .map_err(|_| ClipboardError::StdinWriteFailed)?;
+ }
+
+ // TODO: add timer?
+ let output = child.wait_with_output()?;
+
+ if !output.status.success() {
+ log::error!(
+ "clipboard provider {} failed with stderr: \"{}\"",
+ cmd.command,
+ String::from_utf8_lossy(&output.stderr)
+ );
+ return Err(ClipboardError::CommandFailed);
+ }
+
+ if pipe_output {
+ Ok(Some(String::from_utf8(output.stdout)?))
+ } else {
+ Ok(None)
}
}
}
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 26dea3a2..48d3bc36 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -1,5 +1,6 @@
use crate::{
annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig},
+ clipboard::ClipboardProvider,
document::{
DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint,
},
@@ -345,6 +346,8 @@ pub struct Config {
/// Display diagnostic below the line they occur.
pub inline_diagnostics: InlineDiagnosticsConfig,
pub end_of_line_diagnostics: DiagnosticFilter,
+ // Set to override the default clipboard provider
+ pub clipboard_provider: ClipboardProvider,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
@@ -982,6 +985,7 @@ impl Default for Config {
jump_label_alphabet: ('a'..='z').collect(),
inline_diagnostics: InlineDiagnosticsConfig::default(),
end_of_line_diagnostics: DiagnosticFilter::Disable,
+ clipboard_provider: ClipboardProvider::default(),
}
}
}
@@ -1183,7 +1187,10 @@ impl Editor {
theme_loader,
last_theme: None,
last_selection: None,
- registers: Registers::default(),
+ registers: Registers::new(Box::new(arc_swap::access::Map::new(
+ Arc::clone(&config),
+ |config: &Config| &config.clipboard_provider,
+ ))),
status_msg: None,
autoinfo: None,
idle_timer: Box::pin(sleep(conf.idle_timeout)),
diff --git a/helix-view/src/register.rs b/helix-view/src/register.rs
index 3a2e1b7c..3f7844cd 100644
--- a/helix-view/src/register.rs
+++ b/helix-view/src/register.rs
@@ -1,10 +1,11 @@
use std::{borrow::Cow, collections::HashMap, iter};
use anyhow::Result;
+use arc_swap::access::DynAccess;
use helix_core::NATIVE_LINE_ENDING;
use crate::{
- clipboard::{get_clipboard_provider, ClipboardProvider, ClipboardType},
+ clipboard::{ClipboardProvider, ClipboardType},
Editor,
};
@@ -20,28 +21,25 @@ use crate::{
/// * Document path (`%`): filename of the current buffer
/// * System clipboard (`*`)
/// * Primary clipboard (`+`)
-#[derive(Debug)]
pub struct Registers {
/// The mapping of register to values.
/// Values are stored in reverse order when inserted with `Registers::write`.
/// The order is reversed again in `Registers::read`. This allows us to
/// efficiently prepend new values in `Registers::push`.
inner: HashMap<char, Vec<String>>,
- clipboard_provider: Box<dyn ClipboardProvider>,
+ clipboard_provider: Box<dyn DynAccess<ClipboardProvider>>,
pub last_search_register: char,
}
-impl Default for Registers {
- fn default() -> Self {
+impl Registers {
+ pub fn new(clipboard_provider: Box<dyn DynAccess<ClipboardProvider>>) -> Self {
Self {
inner: Default::default(),
- clipboard_provider: get_clipboard_provider(),
+ clipboard_provider,
last_search_register: '/',
}
}
-}
-impl Registers {
pub fn read<'a>(&'a self, name: char, editor: &'a Editor) -> Option<RegisterValues<'a>> {
match name {
'_' => Some(RegisterValues::new(iter::empty())),
@@ -64,7 +62,7 @@ impl Registers {
Some(RegisterValues::new(iter::once(path)))
}
'*' | '+' => Some(read_from_clipboard(
- self.clipboard_provider.as_ref(),
+ &self.clipboard_provider.load(),
self.inner.get(&name),
match name {
'+' => ClipboardType::Clipboard,
@@ -84,8 +82,8 @@ impl Registers {
'_' => Ok(()),
'#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support writing")),
'*' | '+' => {
- self.clipboard_provider.set_contents(
- values.join(NATIVE_LINE_ENDING.as_str()),
+ self.clipboard_provider.load().set_contents(
+ &values.join(NATIVE_LINE_ENDING.as_str()),
match name {
'+' => ClipboardType::Clipboard,
'*' => ClipboardType::Selection,
@@ -114,7 +112,10 @@ impl Registers {
'*' => ClipboardType::Selection,
_ => unreachable!(),
};
- let contents = self.clipboard_provider.get_contents(clipboard_type)?;
+ let contents = self
+ .clipboard_provider
+ .load()
+ .get_contents(&clipboard_type)?;
let saved_values = self.inner.entry(name).or_default();
if !contents_are_saved(saved_values, &contents) {
@@ -127,7 +128,8 @@ impl Registers {
}
value.push_str(&contents);
self.clipboard_provider
- .set_contents(value, clipboard_type)?;
+ .load()
+ .set_contents(&value, clipboard_type)?;
Ok(())
}
@@ -198,7 +200,8 @@ impl Registers {
fn clear_clipboard(&mut self, clipboard_type: ClipboardType) {
if let Err(err) = self
.clipboard_provider
- .set_contents("".into(), clipboard_type)
+ .load()
+ .set_contents("", clipboard_type)
{
log::error!(
"Failed to clear {} clipboard: {err}",
@@ -210,17 +213,17 @@ impl Registers {
}
}
- pub fn clipboard_provider_name(&self) -> Cow<str> {
- self.clipboard_provider.name()
+ pub fn clipboard_provider_name(&self) -> String {
+ self.clipboard_provider.load().name().into_owned()
}
}
fn read_from_clipboard<'a>(
- provider: &dyn ClipboardProvider,
+ provider: &ClipboardProvider,
saved_values: Option<&'a Vec<String>>,
clipboard_type: ClipboardType,
) -> RegisterValues<'a> {
- match provider.get_contents(clipboard_type) {
+ match provider.get_contents(&clipboard_type) {
Ok(contents) => {
// If we're pasting the same values that we just yanked, re-use
// the saved values. This allows pasting multiple selections