on master help sign in
Use OSC 52 as a fallback for setting the system clipboard (#3220)

This adds a simple base64 implementation to keep us from adding a crate for one function. It's mostly based on https://github.com/marshallpierce/rust-base64/blob/a675443d327e175f735a37f574de803d6a332591/src/engine/naive.rs#L42

commit1bed2f30435cfa45502ad2c481a1f4ffe4a1119a authorCharlie Groves <c@sevorg.org> dateTue 1 Nov 2022, 21:12 (4 years ago) parentb156f57
5 files changed+288−67
helix-view/src/base64.rs+163 helix-view/src/clipboard.rs+110−62 helix-view/src/editor.rs+5−5 helix-view/src/env.rs+8 helix-view/src/lib.rs+2
helix-view/src/base64.rsnew file+163
@@ -0,0 +1,163 @@
1+// A minimal base64 implementation to keep from pulling in a crate for just that. It's based on
2+// https://github.com/marshallpierce/rust-base64 but without all the customization options.
3+// The biggest portion comes from
4+// https://github.com/marshallpierce/rust-base64/blob/a675443d327e175f735a37f574de803d6a332591/src/engine/naive.rs#L42
5+// Thanks, rust-base64!
6+
7+// The MIT License (MIT)
8+
9+// Copyright (c) 2015 Alice Maz
10+
11+// Permission is hereby granted, free of charge, to any person obtaining a copy
12+// of this software and associated documentation files (the "Software"), to deal
13+// in the Software without restriction, including without limitation the rights
14+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15+// copies of the Software, and to permit persons to whom the Software is
16+// furnished to do so, subject to the following conditions:
17+
18+// The above copyright notice and this permission notice shall be included in
19+// all copies or substantial portions of the Software.
20+
21+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27+// THE SOFTWARE.
28+
29+use std::ops::{BitAnd, BitOr, Shl, Shr};
30+
31+const PAD_BYTE: u8 = b'=';
32+const ENCODE_TABLE: &[u8] =
33+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".as_bytes();
34+const LOW_SIX_BITS: u32 = 0x3F;
35+
36+pub fn encode(input: &[u8]) -> String {
37+ let rem = input.len() % 3;
38+ let complete_chunks = input.len() / 3;
39+ let remainder_chunk = if rem == 0 { 0 } else { 1 };
40+ let encoded_size = (complete_chunks + remainder_chunk) * 4;
41+
42+ let mut output = vec![0; encoded_size];
43+
44+ // complete chunks first
45+ let complete_chunk_len = input.len() - rem;
46+
47+ let mut input_index = 0_usize;
48+ let mut output_index = 0_usize;
49+ while input_index < complete_chunk_len {
50+ let chunk = &input[input_index..input_index + 3];
51+
52+ // populate low 24 bits from 3 bytes
53+ let chunk_int: u32 =
54+ (chunk[0] as u32).shl(16) | (chunk[1] as u32).shl(8) | (chunk[2] as u32);
55+ // encode 4x 6-bit output bytes
56+ output[output_index] = ENCODE_TABLE[chunk_int.shr(18) as usize];
57+ output[output_index + 1] = ENCODE_TABLE[chunk_int.shr(12_u8).bitand(LOW_SIX_BITS) as usize];
58+ output[output_index + 2] = ENCODE_TABLE[chunk_int.shr(6_u8).bitand(LOW_SIX_BITS) as usize];
59+ output[output_index + 3] = ENCODE_TABLE[chunk_int.bitand(LOW_SIX_BITS) as usize];
60+
61+ input_index += 3;
62+ output_index += 4;
63+ }
64+
65+ // then leftovers
66+ if rem == 2 {
67+ let chunk = &input[input_index..input_index + 2];
68+
69+ // high six bits of chunk[0]
70+ output[output_index] = ENCODE_TABLE[chunk[0].shr(2) as usize];
71+ // bottom 2 bits of [0], high 4 bits of [1]
72+ output[output_index + 1] = ENCODE_TABLE
73+ [(chunk[0].shl(4_u8).bitor(chunk[1].shr(4_u8)) as u32).bitand(LOW_SIX_BITS) as usize];
74+ // bottom 4 bits of [1], with the 2 bottom bits as zero
75+ output[output_index + 2] =
76+ ENCODE_TABLE[(chunk[1].shl(2_u8) as u32).bitand(LOW_SIX_BITS) as usize];
77+ output[output_index + 3] = PAD_BYTE;
78+ } else if rem == 1 {
79+ let byte = input[input_index];
80+ output[output_index] = ENCODE_TABLE[byte.shr(2) as usize];
81+ output[output_index + 1] =
82+ ENCODE_TABLE[(byte.shl(4_u8) as u32).bitand(LOW_SIX_BITS) as usize];
83+ output[output_index + 2] = PAD_BYTE;
84+ output[output_index + 3] = PAD_BYTE;
85+ }
86+ String::from_utf8(output).expect("Invalid UTF8")
87+}
88+
89+#[cfg(test)]
90+mod tests {
91+ fn compare_encode(expected: &str, target: &[u8]) {
92+ assert_eq!(expected, super::encode(target));
93+ }
94+
95+ #[test]
96+ fn encode_rfc4648_0() {
97+ compare_encode("", b"");
98+ }
99+
100+ #[test]
101+ fn encode_rfc4648_1() {
102+ compare_encode("Zg==", b"f");
103+ }
104+
105+ #[test]
106+ fn encode_rfc4648_2() {
107+ compare_encode("Zm8=", b"fo");
108+ }
109+
110+ #[test]
111+ fn encode_rfc4648_3() {
112+ compare_encode("Zm9v", b"foo");
113+ }
114+
115+ #[test]
116+ fn encode_rfc4648_4() {
117+ compare_encode("Zm9vYg==", b"foob");
118+ }
119+
120+ #[test]
121+ fn encode_rfc4648_5() {
122+ compare_encode("Zm9vYmE=", b"fooba");
123+ }
124+
125+ #[test]
126+ fn encode_rfc4648_6() {
127+ compare_encode("Zm9vYmFy", b"foobar");
128+ }
129+
130+ #[test]
131+ fn encode_all_ascii() {
132+ let mut ascii = Vec::<u8>::with_capacity(128);
133+
134+ for i in 0..128 {
135+ ascii.push(i);
136+ }
137+
138+ compare_encode(
139+ "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7P\
140+ D0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn8\
141+ =",
142+ &ascii,
143+ );
144+ }
145+
146+ #[test]
147+ fn encode_all_bytes() {
148+ let mut bytes = Vec::<u8>::with_capacity(256);
149+
150+ for i in 0..255 {
151+ bytes.push(i);
152+ }
153+ bytes.push(255); //bug with "overflowing" ranges?
154+
155+ compare_encode(
156+ "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7P\
157+ D0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn\
158+ +AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6\
159+ /wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==",
160+ &bytes,
161+ );
162+ }
163+}
helix-view/src/clipboard.rs+110−62
@@ -3,6 +3,7 @@
33use anyhow::Result;
44use std::borrow::Cow;
55
6+#[derive(Clone, Copy, Debug)]
67pub enum ClipboardType {
78 Clipboard,
89 Selection,
@@ -72,45 +73,48 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
7273
7374#[cfg(target_os = "macos")]
7475pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
75− use provider::command::exists;
76+ use crate::env::binary_exists;
7677
77− if exists("pbcopy") && exists("pbpaste") {
78+ if binary_exists("pbcopy") && binary_exists("pbpaste") {
7879 command_provider! {
7980 paste => "pbpaste";
8081 copy => "pbcopy";
8182 }
8283 } else {
83− Box::new(provider::NopProvider::new())
84+ Box::new(provider::FallbackProvider::new())
8485 }
8586}
8687
8788#[cfg(target_os = "wasm32")]
8889pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
8990 // TODO:
90− Box::new(provider::NopProvider::new())
91+ Box::new(provider::FallbackProvider::new())
9192}
9293
9394#[cfg(not(any(windows, target_os = "wasm32", target_os = "macos")))]
9495pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
95− use provider::command::{env_var_is_set, exists, is_exit_success};
96+ use crate::env::{binary_exists, env_var_is_set};
97+ use provider::command::is_exit_success;
9698 // TODO: support for user-defined provider, probably when we have plugin support by setting a
9799 // variable?
98100
99− if env_var_is_set("WAYLAND_DISPLAY") && exists("wl-copy") && exists("wl-paste") {
101+ if env_var_is_set("WAYLAND_DISPLAY") && binary_exists("wl-copy") && binary_exists("wl-paste") {
100102 command_provider! {
101103 paste => "wl-paste", "--no-newline";
102104 copy => "wl-copy", "--type", "text/plain";
103105 primary_paste => "wl-paste", "-p", "--no-newline";
104106 primary_copy => "wl-copy", "-p", "--type", "text/plain";
105107 }
106− } else if env_var_is_set("DISPLAY") && exists("xclip") {
108+ } else if env_var_is_set("DISPLAY") && binary_exists("xclip") {
107109 command_provider! {
108110 paste => "xclip", "-o", "-selection", "clipboard";
109111 copy => "xclip", "-i", "-selection", "clipboard";
110112 primary_paste => "xclip", "-o";
111113 primary_copy => "xclip", "-i";
112114 }
113− } else if env_var_is_set("DISPLAY") && exists("xsel") && is_exit_success("xsel", &["-o", "-b"])
115+ } else if env_var_is_set("DISPLAY")
116+ && binary_exists("xsel")
117+ && is_exit_success("xsel", &["-o", "-b"])
114118 {
115119 // FIXME: check performance of is_exit_success
116120 command_provider! {
@@ -119,43 +123,78 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
119123 primary_paste => "xsel", "-o";
120124 primary_copy => "xsel", "-i";
121125 }
122− } else if exists("win32yank.exe") {
126+ } else if binary_exists("win32yank.exe") {
123127 command_provider! {
124128 paste => "win32yank.exe", "-o", "--lf";
125129 copy => "win32yank.exe", "-i", "--crlf";
126130 }
127− } else if exists("termux-clipboard-set") && exists("termux-clipboard-get") {
131+ } else if binary_exists("termux-clipboard-set") && binary_exists("termux-clipboard-get") {
128132 command_provider! {
129133 paste => "termux-clipboard-get";
130134 copy => "termux-clipboard-set";
131135 }
132− } else if env_var_is_set("TMUX") && exists("tmux") {
136+ } else if env_var_is_set("TMUX") && binary_exists("tmux") {
133137 command_provider! {
134138 paste => "tmux", "save-buffer", "-";
135139 copy => "tmux", "load-buffer", "-";
136140 }
137141 } else {
138− Box::new(provider::NopProvider::new())
142+ Box::new(provider::FallbackProvider::new())
139143 }
140144}
141145
146+#[cfg(not(target_os = "windows"))]
142147pub mod provider {
143148 use super::{ClipboardProvider, ClipboardType};
144149 use anyhow::Result;
145150 use std::borrow::Cow;
146151
147− #[cfg(not(target_os = "windows"))]
152+ #[cfg(feature = "term")]
153+ mod osc52 {
154+ use {super::ClipboardType, crate::base64, crossterm};
155+
156+ #[derive(Debug)]
157+ pub struct SetClipboardCommand {
158+ encoded_content: String,
159+ clipboard_type: ClipboardType,
160+ }
161+
162+ impl SetClipboardCommand {
163+ pub fn new(content: &str, clipboard_type: ClipboardType) -> Self {
164+ Self {
165+ encoded_content: base64::encode(content.as_bytes()),
166+ clipboard_type,
167+ }
168+ }
169+ }
170+
171+ impl crossterm::Command for SetClipboardCommand {
172+ fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
173+ let kind = match &self.clipboard_type {
174+ ClipboardType::Clipboard => "c",
175+ ClipboardType::Selection => "p",
176+ };
177+ // Send an OSC 52 set command: https://terminalguide.namepad.de/seq/osc-52/
178+ write!(f, "\x1b]52;{};{}\x1b\\", kind, &self.encoded_content)
179+ }
180+ }
181+ }
182+
148183 #[derive(Debug)]
149− pub struct NopProvider {
184+ pub struct FallbackProvider {
150185 buf: String,
151186 primary_buf: String,
152187 }
153188
154− #[cfg(not(target_os = "windows"))]
155− impl NopProvider {
189+ impl FallbackProvider {
156190 pub fn new() -> Self {
191+ #[cfg(feature = "term")]
192+ log::debug!(
193+ "No native clipboard provider found. Yanking by OSC 52 and pasting will be internal to Helix"
194+ );
195+ #[cfg(not(feature = "term"))]
157196 log::warn!(
158− "No clipboard provider found! Yanking and pasting will be internal to Helix"
197+ "No native clipboard provider found! Yanking and pasting will be internal to Helix"
159198 );
160199 Self {
161200 buf: String::new(),
@@ -164,20 +203,27 @@ pub mod provider {
164203 }
165204 }
166205
167− #[cfg(not(target_os = "windows"))]
168− impl Default for NopProvider {
206+ impl Default for FallbackProvider {
169207 fn default() -> Self {
170208 Self::new()
171209 }
172210 }
173211
174− #[cfg(not(target_os = "windows"))]
175− impl ClipboardProvider for NopProvider {
212+ impl ClipboardProvider for FallbackProvider {
213+ #[cfg(feature = "term")]
214+ fn name(&self) -> Cow<str> {
215+ Cow::Borrowed("termcode")
216+ }
217+
218+ #[cfg(not(feature = "term"))]
176219 fn name(&self) -> Cow<str> {
177220 Cow::Borrowed("none")
178221 }
179222
180223 fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
224+ // This is the same noop if term is enabled or not.
225+ // We don't use the get side of OSC 52 as it isn't often enabled, it's a security hole,
226+ // and it would require this to be async to listen for the response
181227 let value = match clipboard_type {
182228 ClipboardType::Clipboard => self.buf.clone(),
183229 ClipboardType::Selection => self.primary_buf.clone(),
@@ -187,6 +233,12 @@ pub mod provider {
187233 }
188234
189235 fn set_contents(&mut self, content: String, clipboard_type: ClipboardType) -> Result<()> {
236+ #[cfg(feature = "term")]
237+ crossterm::execute!(
238+ std::io::stdout(),
239+ osc52::SetClipboardCommand::new(&content, clipboard_type)
240+ )?;
241+ // Set our internal variables to use in get_content regardless of using OSC 52
190242 match clipboard_type {
191243 ClipboardType::Clipboard => self.buf = content,
192244 ClipboardType::Selection => self.primary_buf = content,
@@ -195,52 +247,11 @@ pub mod provider {
195247 }
196248 }
197249
198− #[cfg(target_os = "windows")]
199− #[derive(Default, Debug)]
200− pub struct WindowsProvider;
201−
202− #[cfg(target_os = "windows")]
203− impl ClipboardProvider for WindowsProvider {
204− fn name(&self) -> Cow<str> {
205− log::info!("Using clipboard-win to interact with the system clipboard");
206− Cow::Borrowed("clipboard-win")
207− }
208−
209− fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
210− match clipboard_type {
211− ClipboardType::Clipboard => {
212− let contents = clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?;
213− Ok(contents)
214− }
215− ClipboardType::Selection => Ok(String::new()),
216− }
217− }
218−
219− fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()> {
220− match clipboard_type {
221− ClipboardType::Clipboard => {
222− clipboard_win::set_clipboard(clipboard_win::formats::Unicode, contents)?;
223− }
224− ClipboardType::Selection => {}
225− };
226− Ok(())
227− }
228− }
229−
230250 #[cfg(not(target_arch = "wasm32"))]
231251 pub mod command {
232252 use super::*;
233253 use anyhow::{bail, Context as _, Result};
234254
235− pub fn exists(executable_name: &str) -> bool {
236− which::which(executable_name).is_ok()
237− }
238−
239− #[cfg(not(windows))]
240− pub fn env_var_is_set(env_var_name: &str) -> bool {
241− std::env::var_os(env_var_name).is_some()
242− }
243−
244255 #[cfg(not(any(windows, target_os = "macos")))]
245256 pub fn is_exit_success(program: &str, args: &[&str]) -> bool {
246257 std::process::Command::new(program)
@@ -343,3 +354,40 @@ pub mod provider {
343354 }
344355 }
345356}
357+
358+#[cfg(target_os = "windows")]
359+mod provider {
360+ use super::{ClipboardProvider, ClipboardType};
361+ use anyhow::Result;
362+ use std::borrow::Cow;
363+
364+ #[derive(Default, Debug)]
365+ pub struct WindowsProvider;
366+
367+ impl ClipboardProvider for WindowsProvider {
368+ fn name(&self) -> Cow<str> {
369+ log::info!("Using clipboard-win to interact with the system clipboard");
370+ Cow::Borrowed("clipboard-win")
371+ }
372+
373+ fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
374+ match clipboard_type {
375+ ClipboardType::Clipboard => {
376+ let contents = clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?;
377+ Ok(contents)
378+ }
379+ ClipboardType::Selection => Ok(String::new()),
380+ }
381+ }
382+
383+ fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()> {
384+ match clipboard_type {
385+ ClipboardType::Clipboard => {
386+ clipboard_win::set_clipboard(clipboard_win::formats::Unicode, contents)?;
387+ }
388+ ClipboardType::Selection => {}
389+ };
390+ Ok(())
391+ }
392+ }
393+}
helix-view/src/editor.rs+5−5
@@ -187,9 +187,9 @@ pub struct TerminalConfig {
187187
188188#[cfg(windows)]
189189pub fn get_terminal_provider() -> Option<TerminalConfig> {
190− use crate::clipboard::provider::command::exists;
190+ use crate::env::binary_exists;
191191
192− if exists("wt") {
192+ if binary_exists("wt") {
193193 return Some(TerminalConfig {
194194 command: "wt".to_string(),
195195 args: vec![
@@ -210,16 +210,16 @@ pub fn get_terminal_provider() -> Option<TerminalConfig> {
210210
211211#[cfg(not(any(windows, target_os = "wasm32")))]
212212pub fn get_terminal_provider() -> Option<TerminalConfig> {
213− use crate::clipboard::provider::command::{env_var_is_set, exists};
213+ use crate::env::{binary_exists, env_var_is_set};
214214
215− if env_var_is_set("TMUX") && exists("tmux") {
215+ if env_var_is_set("TMUX") && binary_exists("tmux") {
216216 return Some(TerminalConfig {
217217 command: "tmux".to_string(),
218218 args: vec!["split-window".to_string()],
219219 });
220220 }
221221
222− if env_var_is_set("WEZTERM_UNIX_SOCKET") && exists("wezterm") {
222+ if env_var_is_set("WEZTERM_UNIX_SOCKET") && binary_exists("wezterm") {
223223 return Some(TerminalConfig {
224224 command: "wezterm".to_string(),
225225 args: vec!["cli".to_string(), "split-pane".to_string()],
helix-view/src/env.rsnew file+8
@@ -0,0 +1,8 @@
1+pub fn binary_exists(binary_name: &str) -> bool {
2+ which::which(binary_name).is_ok()
3+}
4+
5+#[cfg(not(windows))]
6+pub fn env_var_is_set(env_var_name: &str) -> bool {
7+ std::env::var_os(env_var_name).is_some()
8+}
helix-view/src/lib.rs+2
@@ -4,12 +4,14 @@ pub mod macros;
44pub mod clipboard;
55pub mod document;
66pub mod editor;
7+pub mod env;
78pub mod graphics;
89pub mod gutter;
910pub mod handlers {
1011 pub mod dap;
1112 pub mod lsp;
1213}
14+pub mod base64;
1315pub mod info;
1416pub mod input;
1517pub mod keyboard;