Skip to content

Commit

Permalink
Re-enable clipboard with on-demand command escape hatch
Browse files Browse the repository at this point in the history
  • Loading branch information
bew committed May 19, 2023
1 parent 9d4a9c4 commit 798ca9f
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 31 deletions.
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,3 @@ indoc = "1.0"
[features]
default = ["clipboard"]
clipboard = ["dep:clipboard"]

42 changes: 13 additions & 29 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
#[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;
use termion::event::MouseButton::{Left, WheelDown, WheelUp};
use termion::event::MouseEvent::Press;
use termion::screen::{ToAlternateScreen, ToMainScreen};

use crate::clip::{ClipProvider, ClipError};
use crate::flatjson;
use crate::input::TuiEvent;
use crate::input::TuiEvent::{KeyEvent, MouseEvent, WinChEvent};
#[cfg(feature = "clipboard")]
use crate::lineprinter::JS_IDENTIFIER;
use crate::options::{DataFormat, Opt};
use crate::screenwriter::{MessageSeverity, ScreenWriter};
use crate::search::{JumpDirection, SearchDirection, SearchState};
use crate::types::TTYDimensions;
use crate::viewer::{Action, JsonViewer};
#[cfg(feature = "clipboard")]
use crate::viewer::Mode;

pub struct App {
Expand All @@ -33,8 +28,7 @@ pub struct App {
input_filename: String,
search_state: SearchState,
message: Option<(String, MessageSeverity)>,
#[cfg(feature = "clipboard")]
clipboard_context: Result<ClipboardContext, Box<dyn Error>>,
clip_provider: Result<ClipProvider, ClipError>,
}

// State to determine how to process the next event input.
Expand All @@ -49,13 +43,11 @@ pub struct App {
#[derive(PartialEq)]
enum InputState {
Default,
#[cfg(feature = "clipboard")]
PendingYCommand,
PendingZCommand,
}

// Various things that can be copied
#[cfg(feature = "clipboard")]
enum CopyTarget {
PrettyPrintedValue,
OneLineValue,
Expand Down Expand Up @@ -83,6 +75,7 @@ impl App {
data: String,
data_format: DataFormat,
input_filename: String,
clip_provider: Result<ClipProvider, ClipError>,
stdout: Box<dyn Write>,
) -> Result<App, String> {
let flatjson = match Self::parse_input(data, data_format) {
Expand All @@ -104,8 +97,7 @@ impl App {
input_filename,
search_state: SearchState::empty(),
message: None,
#[cfg(feature = "clipboard")]
clipboard_context: ClipboardProvider::new(),
clip_provider,
})
}

Expand Down Expand Up @@ -161,7 +153,6 @@ impl App {
}
// Handle special input states:
// y commands:
#[cfg(feature = "clipboard")]
event if self.input_state == InputState::PendingYCommand => {
let copy_target = match event {
KeyEvent(Key::Char('y')) => Some(CopyTarget::PrettyPrintedValue),
Expand Down Expand Up @@ -217,9 +208,8 @@ impl App {
None
}
}
#[cfg(feature = "clipboard")]
KeyEvent(Key::Char('y')) => {
match &self.clipboard_context {
match &self.clip_provider {
Ok(_) => {
self.input_state = InputState::PendingYCommand;
self.input_buffer.clear();
Expand All @@ -230,7 +220,6 @@ impl App {
self.set_error_message(msg);
}
}

None
}
KeyEvent(Key::Char('z')) => {
Expand Down Expand Up @@ -633,16 +622,12 @@ impl App {
let _ = write!(self.screen_writer.stdout, "{}", ToAlternateScreen);
}

#[cfg(feature = "clipboard")]
fn copy_content(&mut self, copy_target: CopyTarget) {
// Checked when the user first hits 'y'.
let clipboard = self.clipboard_context.as_mut().unwrap();

let json = &self.viewer.flatjson.1;
let focused_row_index = self.viewer.focused_row;
let focused_row = &self.viewer.flatjson[focused_row_index];

let (content_desc, content) = match copy_target {
let (desc, content) = match copy_target {
CopyTarget::PrettyPrintedValue if focused_row.is_container() => (
"pretty-printed value",
self.viewer
Expand Down Expand Up @@ -674,7 +659,7 @@ impl App {
}
}
ct @ (CopyTarget::DotPath | CopyTarget::BracketPath | CopyTarget::QueryPath) => {
let (content_desc, path_type) = match ct {
let (desc, path_type) = match ct {
CopyTarget::DotPath => ("path", flatjson::PathType::Dot),
CopyTarget::BracketPath => ("bracketed path", flatjson::PathType::Bracket),
CopyTarget::QueryPath => ("query path", flatjson::PathType::Query),
Expand All @@ -686,7 +671,7 @@ impl App {
.flatjson
.build_path_to_node(path_type, focused_row_index)
{
Ok(path) => (content_desc, path),
Ok(path) => (desc, path),
Err(err) => {
self.set_error_message(err);
return;
Expand All @@ -695,13 +680,12 @@ impl App {
}
};

if let Err(err) = clipboard.set_contents(content) {
self.set_error_message(format!(
"Unable to copy {} to clipboard: {}",
content_desc, err
));
// Checked when the user first hits 'y'.
let clip = self.clip_provider.as_mut().unwrap();
if let Err(err) = clip.copy(content) {
self.set_error_message(format!("Unable to copy {} to clipboard: {}", desc, err));
} else {
self.set_info_message(format!("Copied {} to clipboard", content_desc));
self.set_info_message(format!("Copied {} to clipboard", desc));
}
}
}
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::{ClipboardContext, ClipboardProvider};

pub enum ClipProvider {
CommandClipboard(String),
#[cfg(feature = "clipboard")]
SystemClipboard(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()
}
1 change: 0 additions & 1 deletion src/flatjson.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,6 @@ impl FlatJson {
// there are some subtle enough differences, and the code isn't that
// complicated, that I don't think it's worth it to try to have them
// share an implementation.
#[cfg(feature = "clipboard")]
pub fn pretty_printed_value(&self, value_index: Index) -> Result<String, std::fmt::Error> {
if self[value_index].is_primitive() {
return Ok(self.1[self[value_index].range.clone()].to_string());
Expand Down
26 changes: 26 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,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 @@ -34,6 +38,7 @@ mod yamlparser;

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

fn main() {
let opt = Opt::parse();
Expand Down Expand Up @@ -63,11 +68,19 @@ fn main() {
io::stdout().into_raw_mode().unwrap(),
)));

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,
clipboard_provider,
Box::new(stdout),
) {
Ok(jl) => jl,
Expand All @@ -80,6 +93,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 @@ -38,6 +38,11 @@ pub struct Opt {
#[clap(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.
#[clap(long = "json", group = "data-format", display_order = 1000)]
pub json: bool,
Expand Down

0 comments on commit 798ca9f

Please sign in to comment.