Add clipboard provider configuration (#10839)

pull/11448/head^2
Alfie Richards 4 days ago committed by GitHub
parent b6e555a2ed
commit 68ee87695b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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` | `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"` | `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" | `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 ### `[editor.statusline]` Section

@ -1074,7 +1074,7 @@ fn show_clipboard_provider(
} }
cx.editor cx.editor
.set_status(cx.editor.registers.clipboard_provider_name().to_string()); .set_status(cx.editor.registers.clipboard_provider_name());
Ok(()) Ok(())
} }

@ -1,10 +1,10 @@
use crate::config::{Config, ConfigLoadError};
use crossterm::{ use crossterm::{
style::{Color, Print, Stylize}, style::{Color, Print, Stylize},
tty::IsTty, tty::IsTty,
}; };
use helix_core::config::{default_lang_config, user_lang_config}; use helix_core::config::{default_lang_config, user_lang_config};
use helix_loader::grammar::load_runtime_file; use helix_loader::grammar::load_runtime_file;
use helix_view::clipboard::get_clipboard_provider;
use std::io::Write; use std::io::Write;
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
@ -53,7 +53,6 @@ pub fn general() -> std::io::Result<()> {
let lang_file = helix_loader::lang_config_file(); let lang_file = helix_loader::lang_config_file();
let log_file = helix_loader::log_file(); let log_file = helix_loader::log_file();
let rt_dirs = helix_loader::runtime_dirs(); let rt_dirs = helix_loader::runtime_dirs();
let clipboard_provider = get_clipboard_provider();
if config_file.exists() { if config_file.exists() {
writeln!(stdout, "Config file: {}", config_file.display())?; writeln!(stdout, "Config file: {}", config_file.display())?;
@ -92,7 +91,6 @@ pub fn general() -> std::io::Result<()> {
writeln!(stdout, "{}", msg.yellow())?; writeln!(stdout, "{}", msg.yellow())?;
} }
} }
writeln!(stdout, "Clipboard provider: {}", clipboard_provider.name())?;
Ok(()) Ok(())
} }
@ -101,8 +99,19 @@ pub fn clipboard() -> std::io::Result<()> {
let stdout = std::io::stdout(); let stdout = std::io::stdout();
let mut stdout = stdout.lock(); let mut stdout = stdout.lock();
let board = get_clipboard_provider(); let config = match Config::load_default() {
match board.name().as_ref() { 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" => { "none" => {
writeln!( writeln!(
stdout, stdout,

@ -1,356 +1,224 @@
// Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152 // 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 std::borrow::Cow;
use thiserror::Error;
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy)]
pub enum ClipboardType { pub enum ClipboardType {
Clipboard, Clipboard,
Selection, Selection,
} }
pub trait ClipboardProvider: std::fmt::Debug { #[derive(Debug, Error)]
fn name(&self) -> Cow<str>; pub enum ClipboardError {
fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String>; #[error(transparent)]
fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()>; 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))] type Result<T> = std::result::Result<T, ClipboardError>;
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(not(target_arch = "wasm32"))]
pub use external::ClipboardProvider;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> { pub use noop::ClipboardProvider;
// TODO:
Box::new(provider::FallbackProvider::new())
}
#[cfg(not(any(windows, target_arch = "wasm32", target_os = "macos")))] // Clipboard not supported for wasm
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> { #[cfg(target_arch = "wasm32")]
use helix_stdx::env::{binary_exists, env_var_is_set}; mod noop {
use provider::command::is_exit_success; use super::*;
// 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())
}
}
#[cfg(not(target_os = "windows"))] #[derive(Debug, Clone)]
pub mod provider { pub enum ClipboardProvider {}
use super::{ClipboardProvider, ClipboardType};
use anyhow::Result;
use std::borrow::Cow;
#[cfg(feature = "term")] impl ClipboardProvider {
mod osc52 { pub fn detect() -> Self {
use {super::ClipboardType, crate::base64}; Self
}
#[derive(Debug)] pub fn name(&self) -> Cow<str> {
pub struct SetClipboardCommand { "none".into()
encoded_content: String,
clipboard_type: ClipboardType,
} }
impl SetClipboardCommand { pub fn get_contents(&self, _clipboard_type: ClipboardType) -> Result<String> {
pub fn new(content: &str, clipboard_type: ClipboardType) -> Self { Err(ClipboardError::ReadingNotSupported)
Self {
encoded_content: base64::encode(content.as_bytes()),
clipboard_type,
}
}
} }
impl crossterm::Command for SetClipboardCommand { pub fn set_contents(&self, _content: &str, _clipboard_type: ClipboardType) -> Result<()> {
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result { Ok(())
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)
}
} }
} }
}
#[derive(Debug)] #[cfg(not(target_arch = "wasm32"))]
pub struct FallbackProvider { mod external {
buf: String, use super::*;
primary_buf: String,
}
impl FallbackProvider { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub fn new() -> Self { pub struct Command {
#[cfg(feature = "term")] command: Cow<'static, str>,
log::debug!( #[serde(default)]
"No native clipboard provider found. Yanking by OSC 52 and pasting will be internal to Helix" args: Cow<'static, [Cow<'static, str>]>,
);
#[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 { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
fn default() -> Self { #[serde(rename_all = "kebab-case")]
Self::new() 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")] #[cfg(feature = "term")]
fn name(&self) -> Cow<str> { Termcode,
Cow::Borrowed("termcode") Custom(CommandProvider),
} None,
}
#[cfg(not(feature = "term"))]
fn name(&self) -> Cow<str> {
Cow::Borrowed("none")
}
fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> { impl Default for ClipboardProvider {
// This is the same noop if term is enabled or not. #[cfg(windows)]
// We don't use the get side of OSC 52 as it isn't often enabled, it's a security hole, fn default() -> Self {
// and it would require this to be async to listen for the response use helix_stdx::env::binary_exists;
let value = match clipboard_type {
ClipboardType::Clipboard => self.buf.clone(),
ClipboardType::Selection => self.primary_buf.clone(),
};
Ok(value) if binary_exists("win32yank.exe") {
Self::Win32Yank
} else {
Self::Windows
}
} }
fn set_contents(&mut self, content: String, clipboard_type: ClipboardType) -> Result<()> { #[cfg(target_os = "macos")]
#[cfg(feature = "term")] fn default() -> Self {
crossterm::execute!( use helix_stdx::env::{binary_exists, env_var_is_set};
std::io::stdout(),
osc52::SetClipboardCommand::new(&content, clipboard_type) if env_var_is_set("TMUX") && binary_exists("tmux") {
)?; Self::Tmux
// Set our internal variables to use in get_content regardless of using OSC 52 } else if binary_exists("pbcopy") && binary_exists("pbpaste") {
match clipboard_type { Self::Pasteboard
ClipboardType::Clipboard => self.buf = content, } else if cfg!(feature = "term") {
ClipboardType::Selection => self.primary_buf = content, 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")))] #[cfg(not(any(windows, target_os = "macos")))]
pub fn is_exit_success(program: &str, args: &[&str]) -> bool { fn default() -> Self {
std::process::Command::new(program) use helix_stdx::env::{binary_exists, env_var_is_set};
.args(args)
.output() fn is_exit_success(program: &str, args: &[&str]) -> bool {
.ok() std::process::Command::new(program)
.and_then(|out| out.status.success().then_some(())) .args(args)
.is_some() .output()
} .ok()
.and_then(|out| out.status.success().then_some(()))
.is_some()
}
#[derive(Debug)] if env_var_is_set("WAYLAND_DISPLAY")
pub struct Config { && binary_exists("wl-copy")
pub prg: &'static str, && binary_exists("wl-paste")
pub args: &'static [&'static str], {
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 { impl ClipboardProvider {
fn execute(&self, input: Option<&str>, pipe_output: bool) -> Result<Option<String>> { pub fn name(&self) -> Cow<'_, str> {
use std::io::Write; fn builtin_name<'a>(
use std::process::{Command, Stdio}; name: &'static str,
provider: &'static CommandProvider,
let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null); ) -> Cow<'a, str> {
let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null); if provider.yank.command != provider.paste.command {
Cow::Owned(format!(
let mut command: Command = Command::new(self.prg); "{} ({}+{})",
name, provider.yank.command, provider.paste.command
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 { } 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 { match self {
fn name(&self) -> Cow<str> { // These names should match the config option names from Serde
if self.get_cmd.prg != self.set_cmd.prg { Self::Pasteboard => builtin_name("pasteboard", &PASTEBOARD),
Cow::Owned(format!("{}+{}", self.get_cmd.prg, self.set_cmd.prg)) Self::Wayland => builtin_name("wayland", &WL_CLIPBOARD),
} else { Self::XClip => builtin_name("x-clip", &WL_CLIPBOARD),
Cow::Borrowed(self.get_cmd.prg) 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 { match clipboard_type {
ClipboardType::Clipboard => Ok(self ClipboardType::Clipboard => execute_command(&provider.yank, None, true)?
.get_cmd .ok_or(ClipboardError::MissingStdout),
.execute(None, true)?
.context("output is missing")?),
ClipboardType::Selection => { ClipboardType::Selection => {
if let Some(cmd) = &self.get_primary_cmd { if let Some(cmd) = provider.yank_primary.as_ref() {
return cmd.execute(None, true)?.context("output is missing"); return execute_command(cmd, None, true)?
.ok_or(ClipboardError::MissingStdout);
} }
Ok(String::new()) 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 { let cmd = match clipboard_type {
ClipboardType::Clipboard => &self.set_cmd, ClipboardType::Clipboard => &provider.paste,
ClipboardType::Selection => { ClipboardType::Selection => {
if let Some(cmd) = &self.set_primary_cmd { if let Some(cmd) = provider.paste_primary.as_ref() {
cmd cmd
} else { } else {
return Ok(()); 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")] macro_rules! command_provider {
mod provider { ($name:ident,
use super::{ClipboardProvider, ClipboardType}; yank => $yank_cmd:literal $( , $yank_arg:literal )* ;
use anyhow::Result; paste => $paste_cmd:literal $( , $paste_arg:literal )* ; ) => {
use std::borrow::Cow; 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)] command_provider! {
pub struct WindowsProvider; 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 { #[cfg(feature = "term")]
fn name(&self) -> Cow<str> { mod osc52 {
log::debug!("Using clipboard-win to interact with the system clipboard"); use {super::ClipboardType, crate::base64};
Cow::Borrowed("clipboard-win")
pub struct SetClipboardCommand {
encoded_content: String,
clipboard_type: ClipboardType,
} }
fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> { impl SetClipboardCommand {
match clipboard_type { pub fn new(content: &str, clipboard_type: ClipboardType) -> Self {
ClipboardType::Clipboard => { Self {
let contents = clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?; encoded_content: base64::encode(content.as_bytes()),
Ok(contents) clipboard_type,
} }
ClipboardType::Selection => Ok(String::new()),
} }
} }
fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()> { impl crossterm::Command for SetClipboardCommand {
match clipboard_type { fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
ClipboardType::Clipboard => { let kind = match &self.clipboard_type {
clipboard_win::set_clipboard(clipboard_win::formats::Unicode, contents)?; ClipboardType::Clipboard => "c",
} ClipboardType::Selection => "p",
ClipboardType::Selection => {} };
}; // Send an OSC 52 set command: https://terminalguide.namepad.de/seq/osc-52/
Ok(()) 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)
} }
} }
} }

@ -1,5 +1,6 @@
use crate::{ use crate::{
annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig}, annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig},
clipboard::ClipboardProvider,
document::{ document::{
DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint, DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint,
}, },
@ -345,6 +346,8 @@ pub struct Config {
/// Display diagnostic below the line they occur. /// Display diagnostic below the line they occur.
pub inline_diagnostics: InlineDiagnosticsConfig, pub inline_diagnostics: InlineDiagnosticsConfig,
pub end_of_line_diagnostics: DiagnosticFilter, 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)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
@ -982,6 +985,7 @@ impl Default for Config {
jump_label_alphabet: ('a'..='z').collect(), jump_label_alphabet: ('a'..='z').collect(),
inline_diagnostics: InlineDiagnosticsConfig::default(), inline_diagnostics: InlineDiagnosticsConfig::default(),
end_of_line_diagnostics: DiagnosticFilter::Disable, end_of_line_diagnostics: DiagnosticFilter::Disable,
clipboard_provider: ClipboardProvider::default(),
} }
} }
} }
@ -1183,7 +1187,10 @@ impl Editor {
theme_loader, theme_loader,
last_theme: None, last_theme: None,
last_selection: 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, status_msg: None,
autoinfo: None, autoinfo: None,
idle_timer: Box::pin(sleep(conf.idle_timeout)), idle_timer: Box::pin(sleep(conf.idle_timeout)),

@ -1,10 +1,11 @@
use std::{borrow::Cow, collections::HashMap, iter}; use std::{borrow::Cow, collections::HashMap, iter};
use anyhow::Result; use anyhow::Result;
use arc_swap::access::DynAccess;
use helix_core::NATIVE_LINE_ENDING; use helix_core::NATIVE_LINE_ENDING;
use crate::{ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider, ClipboardType}, clipboard::{ClipboardProvider, ClipboardType},
Editor, Editor,
}; };
@ -20,28 +21,25 @@ use crate::{
/// * Document path (`%`): filename of the current buffer /// * Document path (`%`): filename of the current buffer
/// * System clipboard (`*`) /// * System clipboard (`*`)
/// * Primary clipboard (`+`) /// * Primary clipboard (`+`)
#[derive(Debug)]
pub struct Registers { pub struct Registers {
/// The mapping of register to values. /// The mapping of register to values.
/// Values are stored in reverse order when inserted with `Registers::write`. /// Values are stored in reverse order when inserted with `Registers::write`.
/// The order is reversed again in `Registers::read`. This allows us to /// The order is reversed again in `Registers::read`. This allows us to
/// efficiently prepend new values in `Registers::push`. /// efficiently prepend new values in `Registers::push`.
inner: HashMap<char, Vec<String>>, inner: HashMap<char, Vec<String>>,
clipboard_provider: Box<dyn ClipboardProvider>, clipboard_provider: Box<dyn DynAccess<ClipboardProvider>>,
pub last_search_register: char, pub last_search_register: char,
} }
impl Default for Registers { impl Registers {
fn default() -> Self { pub fn new(clipboard_provider: Box<dyn DynAccess<ClipboardProvider>>) -> Self {
Self { Self {
inner: Default::default(), inner: Default::default(),
clipboard_provider: get_clipboard_provider(), clipboard_provider,
last_search_register: '/', last_search_register: '/',
} }
} }
}
impl Registers {
pub fn read<'a>(&'a self, name: char, editor: &'a Editor) -> Option<RegisterValues<'a>> { pub fn read<'a>(&'a self, name: char, editor: &'a Editor) -> Option<RegisterValues<'a>> {
match name { match name {
'_' => Some(RegisterValues::new(iter::empty())), '_' => Some(RegisterValues::new(iter::empty())),
@ -64,7 +62,7 @@ impl Registers {
Some(RegisterValues::new(iter::once(path))) Some(RegisterValues::new(iter::once(path)))
} }
'*' | '+' => Some(read_from_clipboard( '*' | '+' => Some(read_from_clipboard(
self.clipboard_provider.as_ref(), &self.clipboard_provider.load(),
self.inner.get(&name), self.inner.get(&name),
match name { match name {
'+' => ClipboardType::Clipboard, '+' => ClipboardType::Clipboard,
@ -84,8 +82,8 @@ impl Registers {
'_' => Ok(()), '_' => Ok(()),
'#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support writing")), '#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support writing")),
'*' | '+' => { '*' | '+' => {
self.clipboard_provider.set_contents( self.clipboard_provider.load().set_contents(
values.join(NATIVE_LINE_ENDING.as_str()), &values.join(NATIVE_LINE_ENDING.as_str()),
match name { match name {
'+' => ClipboardType::Clipboard, '+' => ClipboardType::Clipboard,
'*' => ClipboardType::Selection, '*' => ClipboardType::Selection,
@ -114,7 +112,10 @@ impl Registers {
'*' => ClipboardType::Selection, '*' => ClipboardType::Selection,
_ => unreachable!(), _ => 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(); let saved_values = self.inner.entry(name).or_default();
if !contents_are_saved(saved_values, &contents) { if !contents_are_saved(saved_values, &contents) {
@ -127,7 +128,8 @@ impl Registers {
} }
value.push_str(&contents); value.push_str(&contents);
self.clipboard_provider self.clipboard_provider
.set_contents(value, clipboard_type)?; .load()
.set_contents(&value, clipboard_type)?;
Ok(()) Ok(())
} }
@ -198,7 +200,8 @@ impl Registers {
fn clear_clipboard(&mut self, clipboard_type: ClipboardType) { fn clear_clipboard(&mut self, clipboard_type: ClipboardType) {
if let Err(err) = self if let Err(err) = self
.clipboard_provider .clipboard_provider
.set_contents("".into(), clipboard_type) .load()
.set_contents("", clipboard_type)
{ {
log::error!( log::error!(
"Failed to clear {} clipboard: {err}", "Failed to clear {} clipboard: {err}",
@ -210,17 +213,17 @@ impl Registers {
} }
} }
pub fn clipboard_provider_name(&self) -> Cow<str> { pub fn clipboard_provider_name(&self) -> String {
self.clipboard_provider.name() self.clipboard_provider.load().name().into_owned()
} }
} }
fn read_from_clipboard<'a>( fn read_from_clipboard<'a>(
provider: &dyn ClipboardProvider, provider: &ClipboardProvider,
saved_values: Option<&'a Vec<String>>, saved_values: Option<&'a Vec<String>>,
clipboard_type: ClipboardType, clipboard_type: ClipboardType,
) -> RegisterValues<'a> { ) -> RegisterValues<'a> {
match provider.get_contents(clipboard_type) { match provider.get_contents(&clipboard_type) {
Ok(contents) => { Ok(contents) => {
// If we're pasting the same values that we just yanked, re-use // If we're pasting the same values that we just yanked, re-use
// the saved values. This allows pasting multiple selections // the saved values. This allows pasting multiple selections

Loading…
Cancel
Save