Skip to content

Commit

Permalink
Make clipboard support optional, with shell command escape hatch
Browse files Browse the repository at this point in the history
Squashed and cleaned up version of PaulJuliusMartinez#121 from the upstream repo.

PaulJuliusMartinez#121
  • Loading branch information
bew authored and aswild committed Jul 18, 2024
1 parent e6cdef7 commit 28fd799
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 11 deletions.
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ edition = "2018"
rust-version = "1.67"

[features]
default = []
default = ["clipboard"]
clipboard = ["dep:clipboard"]
sexp = []

[dependencies]
Expand All @@ -30,7 +31,7 @@ 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"
15 changes: 7 additions & 8 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
use std::error::Error;
use std::fs::File;
use std::io;
use std::io::Write;

use clipboard::{ClipboardContext, ClipboardProvider};
use rustyline::error::ReadlineError;
use rustyline::Editor;
use termion::event::Key;
Expand All @@ -12,6 +10,7 @@ use termion::event::MouseEvent::Press;
use termion::raw::RawTerminal;
use termion::screen::{ToAlternateScreen, ToMainScreen};

use crate::clip::{ClipError, ClipProvider};
use crate::flatjson;
use crate::input::TuiEvent;
use crate::input::TuiEvent::{KeyEvent, MouseEvent, WinChEvent};
Expand All @@ -31,7 +30,7 @@ pub struct App {
input_filename: String,
search_state: SearchState,
message: Option<(String, MessageSeverity)>,
clipboard_context: Result<ClipboardContext, Box<dyn Error>>,
clipboard_provider: Result<ClipProvider, ClipError>,
}

// State to determine how to process the next event input.
Expand Down Expand Up @@ -117,6 +116,7 @@ impl App {
data_format: DataFormat,
input_filename: String,
stdout: RawTerminal<Box<dyn Write>>,
clipboard_provider: Result<ClipProvider, ClipError>,
) -> Result<App, String> {
let flatjson = match Self::parse_input(data, data_format) {
Ok(flatjson) => flatjson,
Expand All @@ -137,7 +137,7 @@ impl App {
input_filename,
search_state: SearchState::empty(),
message: None,
clipboard_context: ClipboardProvider::new(),
clipboard_provider,
})
}

Expand Down Expand Up @@ -318,7 +318,7 @@ impl App {
None
}
KeyEvent(Key::Char('y')) => {
match &self.clipboard_context {
match &self.clipboard_provider {
Ok(_) => {
self.input_state = InputState::PendingYCommand;
self.input_buffer.clear();
Expand All @@ -329,7 +329,6 @@ impl App {
self.set_error_message(msg);
}
}

None
}
KeyEvent(Key::Char('z')) => {
Expand Down Expand Up @@ -888,7 +887,7 @@ impl App {
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];

Expand All @@ -904,7 +903,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}"
));
Expand Down
59 changes: 59 additions & 0 deletions src/clip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use std::fmt;
use std::io::{self, Write};
use std::process::{Command, ExitStatus, Stdio};

#[cfg(feature = "clipboard")]
use clipboard as sys_clipboard;
#[cfg(feature = "clipboard")]
use sys_clipboard::ClipboardProvider;

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<ExitStatus> {
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()
}
34 changes: 33 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ 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;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;

mod app;
mod clip;
mod flatjson;
mod highlighting;
mod input;
Expand All @@ -36,6 +40,7 @@ mod viewer;
mod yamlparser;

use app::App;
use clip::{ClipError, ClipProvider};
use options::{DataFormat, Opt};

fn main() {
Expand Down Expand Up @@ -67,7 +72,19 @@ fn main() {
))) as Box<dyn std::io::Write>;
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}");
Expand All @@ -78,6 +95,21 @@ fn main() {
app.run(Box::new(input::get_input()));
}

#[cfg(feature = "clipboard")]
fn default_clip_provider() -> Result<ClipProvider, ClipError> {
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<ClipProvider, ClipError> {
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 {
Expand Down
5 changes: 5 additions & 0 deletions src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

/// Parse input as JSON, regardless of file extension.
#[arg(long = "json", group = "data-format", display_order = 1000)]
pub json: bool,
Expand Down

0 comments on commit 28fd799

Please sign in to comment.