Skip to content

Commit

Permalink
Merge 'Make clipboard support optional, with shell command escape hatch'
Browse files Browse the repository at this point in the history
  • Loading branch information
aswild committed Jul 18, 2024
2 parents e6cdef7 + 47e8277 commit e1dc96b
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 10 deletions.
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ 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"]
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::{ClipProvider, ClipError};
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
58 changes: 58 additions & 0 deletions src/clip.rs
Original file line number Diff line number Diff line change
@@ -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<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 @@ -37,6 +41,7 @@ mod yamlparser;

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

fn main() {
let opt = Opt::parse();
Expand Down Expand Up @@ -67,7 +72,21 @@ 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 +97,19 @@ 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 e1dc96b

Please sign in to comment.