diff --git a/Cargo.lock b/Cargo.lock index b1694b1..e4d74af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,11 +70,24 @@ name = "copyrat" version = "0.1.0" dependencies = [ "clap", + "duct", "regex", "sequence_trie", "termion", ] +[[package]] +name = "duct" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc6a0a59ed0888e0041cf708e66357b7ae1a82f1c67247e1f93b5e0818f7d8d" +dependencies = [ + "libc", + "once_cell", + "os_pipe", + "shared_child", +] + [[package]] name = "hashbrown" version = "0.9.1" @@ -133,6 +146,22 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" +[[package]] +name = "once_cell" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" + +[[package]] +name = "os_pipe" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "os_str_bytes" version = "2.4.0" @@ -222,6 +251,16 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ee22067b7ccd072eeb64454b9c6e1b33b61cd0d49e895fd48676a184580e0c3" +[[package]] +name = "shared_child" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6be9f7d5565b1483af3e72975e2dee33879b3b86bd48c0929fccf6585d79e65a" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "strsim" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index 61b4148..8020aab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ termion = "1.5" regex = "1.4" clap = { version = "3.0.0-beta.2", features = ["suggestions", "color", "wrap_help"]} sequence_trie = "0.3.6" +duct = "0.13" [[bin]] name = "copyrat" @@ -20,4 +21,4 @@ path = "src/main.rs" [[bin]] name = "tmux-copyrat" -path = "src/bridge.rs" +path = "src/tmux_copyrat.rs" diff --git a/src/lib.rs b/src/lib.rs index 6ed2448..57527ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,8 +7,10 @@ pub mod alphabets; pub mod colors; pub mod error; pub mod model; +pub mod output_destination; pub mod process; pub mod regexes; +pub mod selection; pub mod ui; /// Run copyrat on an input string `buffer`, configured by `Opt`. @@ -16,7 +18,7 @@ pub mod ui; /// # Note /// /// Maybe the decision to take ownership of the buffer is a bit bold. -pub fn run(buffer: String, opt: &CliOpt) -> Option<(String, bool)> { +pub fn run(buffer: String, opt: &CliOpt) -> Option { let mut model = model::Model::new( &buffer, &opt.alphabet, @@ -39,11 +41,14 @@ pub fn run(buffer: String, opt: &CliOpt) -> Option<(String, bool)> { }, }; - let selection: Option<(String, bool)> = { + let default_output_destination = output_destination::OutputDestination::Tmux; + + let selection: Option = { let mut ui = ui::Ui::new( &mut model, opt.unique_hint, opt.focus_wrap_around, + default_output_destination, &opt.colors, &opt.hint_alignment, hint_style, diff --git a/src/main.rs b/src/main.rs index ad08248..5f8eeb6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use std::fs::OpenOptions; use std::io::prelude::*; use std::io::{self, Read}; -use copyrat::{run, CliOpt}; +use copyrat::{run, selection::Selection, CliOpt}; fn main() { let opt = CliOpt::parse(); @@ -17,14 +17,14 @@ fn main() { // Execute copyrat over the buffer (will take control over stdout). // This returns the selected matche. - let selection: Option<(String, bool)> = run(buffer, &opt); + let selection: Option = run(buffer, &opt); // Early exit, signaling no selections were found. if selection.is_none() { std::process::exit(1); } - let (text, _) = selection.unwrap(); + let Selection { text, .. } = selection.unwrap(); // Write output to a target_path if provided, else print to original stdout. match opt.target_path { diff --git a/src/output_destination.rs b/src/output_destination.rs new file mode 100644 index 0000000..e6fdbe9 --- /dev/null +++ b/src/output_destination.rs @@ -0,0 +1,30 @@ +use std::fmt; + +/// Describes the type of buffer the selected should be copied to: either a +/// tmux buffer or the system clipboard. +#[derive(Clone)] +pub enum OutputDestination { + /// The selection will be copied to the tmux buffer. + Tmux, + /// The selection will be copied to the system clipboard. + Clipboard, +} + +impl OutputDestination { + /// Toggle between the variants of `OutputDestination`. + pub fn toggle(&mut self) { + match *self { + Self::Tmux => *self = Self::Clipboard, + Self::Clipboard => *self = Self::Tmux, + } + } +} + +impl fmt::Display for OutputDestination { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Tmux => write!(f, "tmux"), + Self::Clipboard => write!(f, "clipboard"), + } + } +} diff --git a/src/selection.rs b/src/selection.rs new file mode 100644 index 0000000..bac4697 --- /dev/null +++ b/src/selection.rs @@ -0,0 +1,7 @@ +use crate::output_destination::OutputDestination; + +pub struct Selection { + pub text: String, + pub uppercased: bool, + pub output_destination: OutputDestination, +} diff --git a/src/bridge.rs b/src/tmux_copyrat.rs similarity index 63% rename from src/bridge.rs rename to src/tmux_copyrat.rs index faebe5b..3b05e08 100644 --- a/src/bridge.rs +++ b/src/tmux_copyrat.rs @@ -2,7 +2,7 @@ use clap::Clap; use std::collections::HashMap; use std::str::FromStr; -use copyrat::{error, process, CliOpt}; +use copyrat::{error, output_destination::OutputDestination, selection::Selection, CliOpt}; mod tmux; @@ -31,6 +31,13 @@ struct BridgeOpt { #[clap(long, arg_enum, default_value = "visible-area")] capture_region: tmux::CaptureRegion, + /// Name of the copy-to-clipboard executable. + /// + /// If the output destination is set to keyboard, copyrat will pipe the + /// selected text to this executable. + #[clap(long, default_value = "pbcopy")] + clipboard_exe: String, + // Include CLI Options #[clap(flatten)] cli_options: CliOpt, @@ -77,28 +84,50 @@ fn main() -> Result<(), error::ParseError> { let buffer = tmux::capture_pane(&active_pane, &opt.capture_region)?; - // We have to dance a little with Panes, because this process i/o streams + // We have to dance a little with Panes, because this process' i/o streams // are connected to the pane in the window newly created for us, instead // of the active current pane. let temp_pane_spec = format!("{}.0", opt.window_name); tmux::swap_pane_with(&temp_pane_spec)?; - let selections = copyrat::run(buffer, &opt.cli_options); + let selection = copyrat::run(buffer, &opt.cli_options); tmux::swap_pane_with(&temp_pane_spec)?; // Finally copy selection to a tmux buffer, and paste it to the active // buffer if it was uppercased. - match selections { + match selection { None => return Ok(()), - Some((text, uppercased)) => { - let args = vec!["set-buffer", &text]; - process::execute("tmux", &args)?; - + Some(Selection { + text, + uppercased, + output_destination, + }) => { if uppercased { - let args = vec!["paste-buffer", "-t", active_pane.id.as_str()]; - process::execute("tmux", &args)?; + // let args = vec!["send-keys", "-t", active_pane.id.as_str(), &text]; + // process::execute("tmux", &args)?; + duct::cmd!("tmux", "send-keys", "-t", active_pane.id.as_str(), &text).run()?; + } + + match output_destination { + OutputDestination::Tmux => { + // let args = vec!["set-buffer", &text]; + // process::execute("tmux", &args)?; + duct::cmd!("tmux", "set-buffer", &text).run()?; + + // if uppercased { + // let args = vec!["paste-buffer", "-t", active_pane.id.as_str()]; + // process::execute("tmux", &args)?; + // } + } + OutputDestination::Clipboard => { + // let args = [("echo", vec![&text[..]]), ("pbcopy", vec![])]; + // process::execute_piped(&args[..])?; + duct::cmd!("echo", "-n", &text) + .pipe(duct::cmd!(opt.clipboard_exe)) + .read()?; + } } } } diff --git a/src/ui.rs b/src/ui.rs index 1e35fbc..11ee285 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -8,7 +8,7 @@ use sequence_trie::SequenceTrie; use termion::{self, color, cursor, event, style}; use crate::error::ParseError; -use crate::{colors, model}; +use crate::{colors, model, output_destination::OutputDestination, process, selection::Selection}; pub struct Ui<'a> { model: &'a mut model::Model<'a>, @@ -18,6 +18,7 @@ pub struct Ui<'a> { lookup_trie: SequenceTrie, focus_index: usize, focus_wrap_around: bool, + default_output_destination: OutputDestination, rendering_colors: &'a UiColors, hint_alignment: &'a HintAlignment, hint_style: Option, @@ -28,6 +29,7 @@ impl<'a> Ui<'a> { model: &'a mut model::Model<'a>, unique_hint: bool, focus_wrap_around: bool, + default_output_destination: OutputDestination, rendering_colors: &'a UiColors, hint_alignment: &'a HintAlignment, hint_style: Option, @@ -47,6 +49,7 @@ impl<'a> Ui<'a> { lookup_trie, focus_index, focus_wrap_around, + default_output_destination, rendering_colors, hint_alignment, hint_style, @@ -408,8 +411,9 @@ impl<'a> Ui<'a> { return Event::Exit; } - let mut uppercased = false; let mut typed_hint = String::new(); + let mut uppercased = false; + let mut output_destination = self.default_output_destination.clone(); self.full_render(writer); @@ -471,11 +475,28 @@ impl<'a> Ui<'a> { // Yank/copy event::Key::Char(_ch @ 'y') | event::Key::Char(_ch @ '\n') => { let text = self.matches.get(self.focus_index).unwrap().text; - return Event::Match((text.to_string(), false)); + return Event::Match(Selection { + text: text.to_string(), + uppercased: false, + output_destination, + }); } event::Key::Char(_ch @ 'Y') => { let text = self.matches.get(self.focus_index).unwrap().text; - return Event::Match((text.to_string(), true)); + return Event::Match(Selection { + text: text.to_string(), + uppercased: true, + output_destination, + }); + } + + event::Key::Char(_ch @ ' ') => { + output_destination.toggle(); + let message = format!("output destination: `{}`", output_destination); + let args = vec!["display-message", &message]; + process::execute("tmux", &args) + .expect("could not make tmux display the message."); + continue; } // Use a Trie or another data structure to determine @@ -496,7 +517,7 @@ impl<'a> Ui<'a> { .get_node(&typed_hint.chars().collect::>()); if node.is_none() { - // An unknown key was entered. + // A key outside the alphabet was entered. return Event::Exit; } @@ -508,7 +529,11 @@ impl<'a> Ui<'a> { ); let mat = self.matches.get(*match_index).expect("By construction, the value in a leaf should correspond to an existing hint."); let text = mat.text.to_string(); - return Event::Match((text, uppercased)); + return Event::Match(Selection { + text, + uppercased, + output_destination, + }); } else { // The prefix of a hint was entered, but we // still need more keys. @@ -530,7 +555,7 @@ impl<'a> Ui<'a> { /// /// - Setup steps: switch to alternate screen, switch to raw mode, hide the cursor. /// - Teardown steps: show cursor, back to main screen. - pub fn present(&mut self) -> Option<(String, bool)> { + pub fn present(&mut self) -> Option { use std::io::Write; use termion::raw::IntoRawMode; use termion::screen::AlternateScreen; @@ -546,7 +571,7 @@ impl<'a> Ui<'a> { let selection = match self.listen(&mut stdin, &mut stdout) { Event::Exit => None, - Event::Match((text, uppercased)) => Some((text, uppercased)), + Event::Match(selection) => Some(selection), }; write!(stdout, "{}", cursor::Show).unwrap(); @@ -623,7 +648,7 @@ enum Event { /// Exit with no selected matches, Exit, /// A vector of matched text and whether it was selected with uppercase. - Match((String, bool)), + Match(Selection), } #[cfg(test)] @@ -915,6 +940,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; lookup_trie: SequenceTrie::new(), focus_index: 0, focus_wrap_around: false, + default_output_destination: OutputDestination::Tmux, rendering_colors: &rendering_colors, hint_alignment: &hint_alignment, hint_style: None, @@ -968,6 +994,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; ); let unique_hint = false; let wrap_around = false; + let default_output_destination = OutputDestination::Tmux; let rendering_colors = UiColors { text_fg: Box::new(color::Black), @@ -986,6 +1013,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; &mut model, unique_hint, wrap_around, + default_output_destination, &rendering_colors, &hint_alignment, hint_style,