From bf42f031931904c32fc1396e4e9bc16231d50808 Mon Sep 17 00:00:00 2001 From: G Pery Date: Thu, 22 Dec 2022 21:22:15 +0100 Subject: [PATCH 1/2] Make clipboard an optional feature --- Cargo.toml | 7 ++++++- src/app.rs | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5b90093..4790604 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,12 @@ clap = { version = "4.0", features = ["derive"] } isatty = "0.1" libc-stdhandle = "0.1.0" yaml-rust = "0.4" -clipboard = "0.5" +clipboard = { version = "0.5", optional = true } [dev-dependencies] indoc = "1.0" + +[features] +default = ["clipboard"] +clipboard = ["dep:clipboard"] + diff --git a/src/app.rs b/src/app.rs index 5db44ff..88dbd9e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,7 +1,9 @@ +#[cfg(feature = "clipboard")] use std::error::Error; use std::io; use std::io::Write; +#[cfg(feature = "clipboard")] use clipboard::{ClipboardContext, ClipboardProvider}; use rustyline::error::ReadlineError; use rustyline::Editor; @@ -30,6 +32,7 @@ pub struct App { input_filename: String, search_state: SearchState, message: Option<(String, MessageSeverity)>, + #[cfg(feature = "clipboard")] clipboard_context: Result>, } @@ -124,6 +127,7 @@ impl App { input_filename, search_state: SearchState::empty(), message: None, + #[cfg(feature = "clipboard")] clipboard_context: ClipboardProvider::new(), }) } @@ -220,6 +224,7 @@ impl App { None } // y commands: + #[cfg(feature = "clipboard")] event if self.input_state == InputState::PendingYCommand => { let content_target = match event { KeyEvent(Key::Char('y')) => Some(ContentTarget::PrettyPrintedValue), @@ -282,6 +287,7 @@ impl App { self.buffer_input(b'p'); None } + #[cfg(feature = "clipboard")] KeyEvent(Key::Char('y')) => { match &self.clipboard_context { Ok(_) => { @@ -811,6 +817,7 @@ impl App { Ok(data) } + #[cfg(feature = "clipboard")] fn copy_content(&mut self, content_target: ContentTarget) { match self.get_content_target_data(content_target) { Ok(content) => { From 47e8277a8e5ced77bb79e7ccffff39ea223576ae Mon Sep 17 00:00:00 2001 From: Benoit de Chezelles Date: Fri, 19 May 2023 17:30:49 +0200 Subject: [PATCH 2/2] Re-enable clipboard with on-demand command escape hatch --- Cargo.toml | 1 - src/app.rs | 22 ++++++------------- src/clip.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 34 ++++++++++++++++++++++++++++- src/options.rs | 5 +++++ 5 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 src/clip.rs diff --git a/Cargo.toml b/Cargo.toml index 4790604..a7535ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,4 +34,3 @@ indoc = "1.0" [features] default = ["clipboard"] clipboard = ["dep:clipboard"] - diff --git a/src/app.rs b/src/app.rs index 88dbd9e..c792d2d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,10 +1,6 @@ -#[cfg(feature = "clipboard")] -use std::error::Error; use std::io; use std::io::Write; -#[cfg(feature = "clipboard")] -use clipboard::{ClipboardContext, ClipboardProvider}; use rustyline::error::ReadlineError; use rustyline::Editor; use termion::event::Key; @@ -13,6 +9,7 @@ use termion::event::MouseEvent::Press; use termion::raw::RawTerminal; use termion::screen::{ToAlternateScreen, ToMainScreen}; +use crate::clip::{ClipProvider, ClipError}; use crate::flatjson; use crate::input::TuiEvent; use crate::input::TuiEvent::{KeyEvent, MouseEvent, WinChEvent}; @@ -32,8 +29,7 @@ pub struct App { input_filename: String, search_state: SearchState, message: Option<(String, MessageSeverity)>, - #[cfg(feature = "clipboard")] - clipboard_context: Result>, + clipboard_provider: Result, } // State to determine how to process the next event input. @@ -107,6 +103,7 @@ impl App { data_format: DataFormat, input_filename: String, stdout: RawTerminal>, + clipboard_provider: Result, ) -> Result { let flatjson = match Self::parse_input(data, data_format) { Ok(flatjson) => flatjson, @@ -127,8 +124,7 @@ impl App { input_filename, search_state: SearchState::empty(), message: None, - #[cfg(feature = "clipboard")] - clipboard_context: ClipboardProvider::new(), + clipboard_provider, }) } @@ -224,7 +220,6 @@ impl App { None } // y commands: - #[cfg(feature = "clipboard")] event if self.input_state == InputState::PendingYCommand => { let content_target = match event { KeyEvent(Key::Char('y')) => Some(ContentTarget::PrettyPrintedValue), @@ -287,9 +282,8 @@ impl App { self.buffer_input(b'p'); None } - #[cfg(feature = "clipboard")] KeyEvent(Key::Char('y')) => { - match &self.clipboard_context { + match &self.clipboard_provider { Ok(_) => { self.input_state = InputState::PendingYCommand; self.input_buffer.clear(); @@ -300,7 +294,6 @@ impl App { self.set_error_message(msg); } } - None } KeyEvent(Key::Char('z')) => { @@ -817,12 +810,11 @@ impl App { Ok(data) } - #[cfg(feature = "clipboard")] fn copy_content(&mut self, content_target: ContentTarget) { match self.get_content_target_data(content_target) { Ok(content) => { // Checked when the user first hits 'y'. - let clipboard = self.clipboard_context.as_mut().unwrap(); + let clipboard = self.clipboard_provider.as_mut().unwrap(); let focused_row = &self.viewer.flatjson[self.viewer.focused_row]; @@ -838,7 +830,7 @@ impl App { ContentTarget::QueryPath => "query path", }; - if let Err(err) = clipboard.set_contents(content) { + if let Err(err) = clipboard.copy(content) { self.set_error_message(format!( "Unable to copy {content_type} to clipboard: {err}" )); diff --git a/src/clip.rs b/src/clip.rs new file mode 100644 index 0000000..7be7334 --- /dev/null +++ b/src/clip.rs @@ -0,0 +1,58 @@ +use std::fmt; +use std::io::{self, Write}; +use std::process::{Command, ExitStatus, Stdio}; + +#[cfg(feature = "clipboard")] +use clipboard as sys_clipboard; + +#[derive(Debug)] +pub enum ClipProvider { + CommandClipboard(String), + #[cfg(feature = "clipboard")] + SystemClipboard(sys_clipboard::ClipboardContext), +} + +#[derive(Debug)] +pub struct ClipError(pub String); +impl fmt::Display for ClipError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl ClipProvider { + pub fn copy(&mut self, content: String) -> Result<(), ClipError> { + match self { + Self::CommandClipboard(shell_command) => { + let status = send_content_to_shell_command(content, shell_command) + .map_err(|err| ClipError(err.to_string()))?; + match status.code() { + Some(code) if !status.success() => { + Err(ClipError(std::format!("Command failed with status code {}", code))) + }, + Some(_) => Ok(()), + None => Err(ClipError("Command terminated by signal".to_string())), + } + }, + + #[cfg(feature = "clipboard")] + Self::SystemClipboard(context) => { + context.set_contents(content) + .map_err(|err| ClipError(err.to_string())) + }, + } + } +} + +fn send_content_to_shell_command(content: String, shell_command: &str) -> io::Result { + let mut child = Command::new("sh") + .args(&["-c", shell_command]) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + let mut stdin = child.stdin.take().expect("Failed to grab stdin"); + stdin.write_all(content.as_bytes())?; + drop(stdin); + child.wait() +} diff --git a/src/main.rs b/src/main.rs index 33d942f..1417a1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,9 @@ use std::io; use std::io::Read; use std::path::PathBuf; +#[cfg(feature = "clipboard")] +use clipboard as sys_clipboard; + use clap::Parser; use termion::cursor::HideCursor; use termion::input::MouseTerminal; @@ -19,6 +22,7 @@ use termion::raw::IntoRawMode; use termion::screen::AlternateScreen; mod app; +mod clip; mod flatjson; mod highlighting; mod input; @@ -37,6 +41,7 @@ mod yamlparser; use app::App; use options::{DataFormat, Opt}; +use clip::{ClipProvider, ClipError}; fn main() { let opt = Opt::parse(); @@ -67,7 +72,21 @@ fn main() { ))) as Box; let raw_stdout = stdout.into_raw_mode().unwrap(); - let mut app = match App::new(&opt, input_string, data_format, input_filename, raw_stdout) { + let clipboard_provider = match opt.clipboard_command { + Some(ref command) => { + Ok(ClipProvider::CommandClipboard(command.to_string())) + }, + None => default_clip_provider(), + }; + + let mut app = match App::new( + &opt, + input_string, + data_format, + input_filename, + raw_stdout, + clipboard_provider, + ) { Ok(jl) => jl, Err(err) => { eprintln!("{err}"); @@ -78,6 +97,19 @@ fn main() { app.run(Box::new(input::get_input())); } +#[cfg(feature = "clipboard")] +fn default_clip_provider() -> Result { + match sys_clipboard::ClipboardProvider::new() { + Ok(clip) => Ok(ClipProvider::SystemClipboard(clip)), + Err(err) => Err(ClipError(err.to_string())), + } +} + +#[cfg(not(feature = "clipboard"))] +fn default_clip_provider() -> Result { + Err(ClipError("No clipboard support, use --clipboard-cmd to set one".to_string())) +} + fn print_pretty_printed_input(input: String, data_format: DataFormat) { // Don't try to pretty print YAML input; just pass it through. if data_format == DataFormat::Yaml { diff --git a/src/options.rs b/src/options.rs index e1da0ce..e3e129d 100644 --- a/src/options.rs +++ b/src/options.rs @@ -71,6 +71,11 @@ pub struct Opt { #[arg(long = "scrolloff", default_value_t = 3)] pub scrolloff: u16, + /// Shell command to run for copy actions instead of the default clipboard. + /// The copy content will be sent into the command's stdin. + #[clap(long = "clipboard-cmd")] + pub clipboard_command: Option, + /// Parse input as JSON, regardless of file extension. #[arg(long = "json", group = "data-format", display_order = 1000)] pub json: bool,