Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make clipboard support optional, with shell command escape hatch #121

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,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,8 +1,6 @@
use std::error::Error;
use std::io;
use std::io::Write;

use clipboard::{ClipboardContext, ClipboardProvider};
use rustyline::error::ReadlineError;
use rustyline::Editor;
use termion::event::Key;
Expand All @@ -11,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};
Expand All @@ -30,7 +29,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 @@ -104,6 +103,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 @@ -124,7 +124,7 @@ impl App {
input_filename,
search_state: SearchState::empty(),
message: None,
clipboard_context: ClipboardProvider::new(),
clipboard_provider,
})
}

Expand Down Expand Up @@ -283,7 +283,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 @@ -294,7 +294,6 @@ impl App {
self.set_error_message(msg);
}
}

None
}
KeyEvent(Key::Char('z')) => {
Expand Down Expand Up @@ -815,7 +814,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 @@ -831,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}"
));
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