mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-21 17:06:18 +04:00
Add clipboard provider configuration (#10839)
This commit is contained in:
parent
b6e555a2ed
commit
68ee87695b
@ -52,6 +52,30 @@ ### `[editor]` Section
|
||||
| `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
|
||||
|
||||
|
@ -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(())
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -1,164 +1,408 @@
|
||||
// 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?
|
||||
// Clipboard not supported for wasm
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod noop {
|
||||
use super::*;
|
||||
|
||||
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";
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ClipboardProvider {}
|
||||
|
||||
impl ClipboardProvider {
|
||||
pub fn detect() -> Self {
|
||||
Self
|
||||
}
|
||||
} 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";
|
||||
|
||||
pub fn name(&self) -> Cow<str> {
|
||||
"none".into()
|
||||
}
|
||||
} 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";
|
||||
|
||||
pub fn get_contents(&self, _clipboard_type: ClipboardType) -> Result<String> {
|
||||
Err(ClipboardError::ReadingNotSupported)
|
||||
}
|
||||
} else if binary_exists("win32yank.exe") {
|
||||
command_provider! {
|
||||
paste => "win32yank.exe", "-o", "--lf";
|
||||
copy => "win32yank.exe", "-i", "--crlf";
|
||||
|
||||
pub fn set_contents(&self, _content: &str, _clipboard_type: ClipboardType) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
} 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"))]
|
||||
pub mod provider {
|
||||
use super::{ClipboardProvider, ClipboardType};
|
||||
use anyhow::Result;
|
||||
use std::borrow::Cow;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod external {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Command {
|
||||
command: Cow<'static, str>,
|
||||
#[serde(default)]
|
||||
args: Cow<'static, [Cow<'static, str>]>,
|
||||
}
|
||||
|
||||
#[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>,
|
||||
}
|
||||
|
||||
#[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")]
|
||||
Termcode,
|
||||
Custom(CommandProvider),
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for ClipboardProvider {
|
||||
#[cfg(windows)]
|
||||
fn default() -> Self {
|
||||
use helix_stdx::env::binary_exists;
|
||||
|
||||
if binary_exists("win32yank.exe") {
|
||||
Self::Win32Yank
|
||||
} else {
|
||||
Self::Windows
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(windows, target_os = "macos")))]
|
||||
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()
|
||||
}
|
||||
|
||||
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 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 {
|
||||
Cow::Owned(format!("{} ({})", name, provider.yank.command))
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
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 => execute_command(&provider.yank, None, true)?
|
||||
.ok_or(ClipboardError::MissingStdout),
|
||||
ClipboardType::Selection => {
|
||||
if let Some(cmd) = provider.yank_primary.as_ref() {
|
||||
return execute_command(cmd, None, true)?
|
||||
.ok_or(ClipboardError::MissingStdout);
|
||||
}
|
||||
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 => &provider.paste,
|
||||
ClipboardType::Selection => {
|
||||
if let Some(cmd) = provider.paste_primary.as_ref() {
|
||||
cmd
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) ),* ])
|
||||
}),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
mod osc52 {
|
||||
use {super::ClipboardType, crate::base64};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SetClipboardCommand {
|
||||
encoded_content: String,
|
||||
clipboard_type: ClipboardType,
|
||||
@ -182,232 +426,74 @@ fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||
// 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.",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FallbackProvider {
|
||||
buf: String,
|
||||
primary_buf: String,
|
||||
}
|
||||
fn execute_command(
|
||||
cmd: &Command,
|
||||
input: Option<&str>,
|
||||
pipe_output: bool,
|
||||
) -> Result<Option<String>> {
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
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"
|
||||
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)
|
||||
);
|
||||
#[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")
|
||||
return Err(ClipboardError::CommandFailed);
|
||||
}
|
||||
|
||||
#[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(())
|
||||
if pipe_output {
|
||||
Ok(Some(String::from_utf8(output.stdout)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 @@ fn default() -> Self {
|
||||
jump_label_alphabet: ('a'..='z').collect(),
|
||||
inline_diagnostics: InlineDiagnosticsConfig::default(),
|
||||
end_of_line_diagnostics: DiagnosticFilter::Disable,
|
||||
clipboard_provider: ClipboardProvider::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1183,7 +1187,10 @@ pub fn new(
|
||||
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)),
|
||||
|
@ -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 @@
|
||||
/// * 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 @@ pub fn read<'a>(&'a self, name: char, editor: &'a Editor) -> Option<RegisterValu
|
||||
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 @@ pub fn write(&mut self, name: char, mut values: Vec<String>) -> Result<()> {
|
||||
'_' => 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 @@ pub fn push(&mut self, name: char, mut value: String) -> Result<()> {
|
||||
'*' => 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 @@ pub fn push(&mut self, name: char, mut value: String) -> Result<()> {
|
||||
}
|
||||
value.push_str(&contents);
|
||||
self.clipboard_provider
|
||||
.set_contents(value, clipboard_type)?;
|
||||
.load()
|
||||
.set_contents(&value, clipboard_type)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -198,7 +200,8 @@ pub fn remove(&mut self, name: char) -> bool {
|
||||
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 @@ fn clear_clipboard(&mut self, clipboard_type: ClipboardType) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user