on master help sign in
Use OSC 52 as a fallback for setting the system clipboard (#3220)
1bed2f3Charlie Groves
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 @@
3use anyhow::Result;
3use anyhow::Result;
4use std::borrow::Cow;
4use std::borrow::Cow;
5
5
6+#[derive(Clone, Copy, Debug)]
6pub enum ClipboardType {
7pub enum ClipboardType {
7 Clipboard,
8 Clipboard,
8 Selection,
9 Selection,
@@ -72,45 +73,48 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
72
73
73#[cfg(target_os = "macos")]
74#[cfg(target_os = "macos")]
74pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
75pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
75− use provider::command::exists;
76+ use crate::env::binary_exists;
76
77
77− if exists("pbcopy") && exists("pbpaste") {
78+ if binary_exists("pbcopy") && binary_exists("pbpaste") {
78 command_provider! {
79 command_provider! {
79 paste => "pbpaste";
80 paste => "pbpaste";
80 copy => "pbcopy";
81 copy => "pbcopy";
81 }
82 }
82 } else {
83 } else {
83− Box::new(provider::NopProvider::new())
84+ Box::new(provider::FallbackProvider::new())
84 }
85 }
85}
86}
86
87
87#[cfg(target_os = "wasm32")]
88#[cfg(target_os = "wasm32")]
88pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
89pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
89 // TODO:
90 // TODO:
90− Box::new(provider::NopProvider::new())
91+ Box::new(provider::FallbackProvider::new())
91}
92}
92
93
93#[cfg(not(any(windows, target_os = "wasm32", target_os = "macos")))]
94#[cfg(not(any(windows, target_os = "wasm32", target_os = "macos")))]
94pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
95pub 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;
96 // TODO: support for user-defined provider, probably when we have plugin support by setting a
98 // TODO: support for user-defined provider, probably when we have plugin support by setting a
97 // variable?
99 // variable?
98
100
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") {
100 command_provider! {
102 command_provider! {
101 paste => "wl-paste", "--no-newline";
103 paste => "wl-paste", "--no-newline";
102 copy => "wl-copy", "--type", "text/plain";
104 copy => "wl-copy", "--type", "text/plain";
103 primary_paste => "wl-paste", "-p", "--no-newline";
105 primary_paste => "wl-paste", "-p", "--no-newline";
104 primary_copy => "wl-copy", "-p", "--type", "text/plain";
106 primary_copy => "wl-copy", "-p", "--type", "text/plain";
105 }
107 }
106− } else if env_var_is_set("DISPLAY") && exists("xclip") {
108+ } else if env_var_is_set("DISPLAY") && binary_exists("xclip") {
107 command_provider! {
109 command_provider! {
108 paste => "xclip", "-o", "-selection", "clipboard";
110 paste => "xclip", "-o", "-selection", "clipboard";
109 copy => "xclip", "-i", "-selection", "clipboard";
111 copy => "xclip", "-i", "-selection", "clipboard";
110 primary_paste => "xclip", "-o";
112 primary_paste => "xclip", "-o";
111 primary_copy => "xclip", "-i";
113 primary_copy => "xclip", "-i";
112 }
114 }
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"])
114 {
118 {
115 // FIXME: check performance of is_exit_success
119 // FIXME: check performance of is_exit_success
116 command_provider! {
120 command_provider! {
@@ -119,43 +123,78 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
119 primary_paste => "xsel", "-o";
123 primary_paste => "xsel", "-o";
120 primary_copy => "xsel", "-i";
124 primary_copy => "xsel", "-i";
121 }
125 }
122− } else if exists("win32yank.exe") {
126+ } else if binary_exists("win32yank.exe") {
123 command_provider! {
127 command_provider! {
124 paste => "win32yank.exe", "-o", "--lf";
128 paste => "win32yank.exe", "-o", "--lf";
125 copy => "win32yank.exe", "-i", "--crlf";
129 copy => "win32yank.exe", "-i", "--crlf";
126 }
130 }
127− } else if exists("termux-clipboard-set") && exists("termux-clipboard-get") {
131+ } else if binary_exists("termux-clipboard-set") && binary_exists("termux-clipboard-get") {
128 command_provider! {
132 command_provider! {
129 paste => "termux-clipboard-get";
133 paste => "termux-clipboard-get";
130 copy => "termux-clipboard-set";
134 copy => "termux-clipboard-set";
131 }
135 }
132− } else if env_var_is_set("TMUX") && exists("tmux") {
136+ } else if env_var_is_set("TMUX") && binary_exists("tmux") {
133 command_provider! {
137 command_provider! {
134 paste => "tmux", "save-buffer", "-";
138 paste => "tmux", "save-buffer", "-";
135 copy => "tmux", "load-buffer", "-";
139 copy => "tmux", "load-buffer", "-";
136 }
140 }
137 } else {
141 } else {
138− Box::new(provider::NopProvider::new())
142+ Box::new(provider::FallbackProvider::new())
139 }
143 }
140}
144}
141
145
146+#[cfg(not(target_os = "windows"))]
142pub mod provider {
147pub mod provider {
143 use super::{ClipboardProvider, ClipboardType};
148 use super::{ClipboardProvider, ClipboardType};
144 use anyhow::Result;
149 use anyhow::Result;
145 use std::borrow::Cow;
150 use std::borrow::Cow;
146
151
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+
148 #[derive(Debug)]
183 #[derive(Debug)]
149− pub struct NopProvider {
184+ pub struct FallbackProvider {
150 buf: String,
185 buf: String,
151 primary_buf: String,
186 primary_buf: String,
152 }
187 }
153
188
154− #[cfg(not(target_os = "windows"))]
189+ impl FallbackProvider {
155− impl NopProvider {
156 pub fn new() -> Self {
190 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"))]
157 log::warn!(
196 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"
159 );
198 );
160 Self {
199 Self {
161 buf: String::new(),
200 buf: String::new(),
@@ -164,20 +203,27 @@ pub mod provider {
164 }
203 }
165 }
204 }
166
205
167− #[cfg(not(target_os = "windows"))]
168− impl Default for NopProvider {
206+ impl Default for FallbackProvider {
169 fn default() -> Self {
207 fn default() -> Self {
170 Self::new()
208 Self::new()
171 }
209 }
172 }
210 }
173
211
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"))]
176 fn name(&self) -> Cow<str> {
219 fn name(&self) -> Cow<str> {
177 Cow::Borrowed("none")
220 Cow::Borrowed("none")
178 }
221 }
179
222
180 fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
223 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
181 let value = match clipboard_type {
227 let value = match clipboard_type {
182 ClipboardType::Clipboard => self.buf.clone(),
228 ClipboardType::Clipboard => self.buf.clone(),
183 ClipboardType::Selection => self.primary_buf.clone(),
229 ClipboardType::Selection => self.primary_buf.clone(),
@@ -187,6 +233,12 @@ pub mod provider {
187 }
233 }
188
234
189 fn set_contents(&mut self, content: String, clipboard_type: ClipboardType) -> Result<()> {
235 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
190 match clipboard_type {
242 match clipboard_type {
191 ClipboardType::Clipboard => self.buf = content,
243 ClipboardType::Clipboard => self.buf = content,
192 ClipboardType::Selection => self.primary_buf = content,
244 ClipboardType::Selection => self.primary_buf = content,
@@ -195,52 +247,11 @@ pub mod provider {
195 }
247 }
196 }
248 }
197
249
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−
230 #[cfg(not(target_arch = "wasm32"))]
250 #[cfg(not(target_arch = "wasm32"))]
231 pub mod command {
251 pub mod command {
232 use super::*;
252 use super::*;
233 use anyhow::{bail, Context as _, Result};
253 use anyhow::{bail, Context as _, Result};
234
254
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−
244 #[cfg(not(any(windows, target_os = "macos")))]
255 #[cfg(not(any(windows, target_os = "macos")))]
245 pub fn is_exit_success(program: &str, args: &[&str]) -> bool {
256 pub fn is_exit_success(program: &str, args: &[&str]) -> bool {
246 std::process::Command::new(program)
257 std::process::Command::new(program)
@@ -343,3 +354,40 @@ pub mod provider {
343 }
354 }
344 }
355 }
345}
356}
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 {
187
187
188#[cfg(windows)]
188#[cfg(windows)]
189pub fn get_terminal_provider() -> Option<TerminalConfig> {
189pub fn get_terminal_provider() -> Option<TerminalConfig> {
190− use crate::clipboard::provider::command::exists;
190+ use crate::env::binary_exists;
191
191
192− if exists("wt") {
192+ if binary_exists("wt") {
193 return Some(TerminalConfig {
193 return Some(TerminalConfig {
194 command: "wt".to_string(),
194 command: "wt".to_string(),
195 args: vec![
195 args: vec![
@@ -210,16 +210,16 @@ pub fn get_terminal_provider() -> Option<TerminalConfig> {
210
210
211#[cfg(not(any(windows, target_os = "wasm32")))]
211#[cfg(not(any(windows, target_os = "wasm32")))]
212pub fn get_terminal_provider() -> Option<TerminalConfig> {
212pub 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};
214
214
215− if env_var_is_set("TMUX") && exists("tmux") {
215+ if env_var_is_set("TMUX") && binary_exists("tmux") {
216 return Some(TerminalConfig {
216 return Some(TerminalConfig {
217 command: "tmux".to_string(),
217 command: "tmux".to_string(),
218 args: vec!["split-window".to_string()],
218 args: vec!["split-window".to_string()],
219 });
219 });
220 }
220 }
221
221
222− if env_var_is_set("WEZTERM_UNIX_SOCKET") && exists("wezterm") {
222+ if env_var_is_set("WEZTERM_UNIX_SOCKET") && binary_exists("wezterm") {
223 return Some(TerminalConfig {
223 return Some(TerminalConfig {
224 command: "wezterm".to_string(),
224 command: "wezterm".to_string(),
225 args: vec!["cli".to_string(), "split-pane".to_string()],
225 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;
4pub mod clipboard;
4pub mod clipboard;
5pub mod document;
5pub mod document;
6pub mod editor;
6pub mod editor;
7+pub mod env;
7pub mod graphics;
8pub mod graphics;
8pub mod gutter;
9pub mod gutter;
9pub mod handlers {
10pub mod handlers {
10 pub mod dap;
11 pub mod dap;
11 pub mod lsp;
12 pub mod lsp;
12}
13}
14+pub mod base64;
13pub mod info;
15pub mod info;
14pub mod input;
16pub mod input;
15pub mod keyboard;
17pub mod keyboard;