diff --git a/Cargo.lock b/Cargo.lock index 7fd3c796..0ebb7c5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cc" version = "1.2.1" @@ -637,6 +643,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.89" @@ -655,6 +670,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "rayon" version = "1.10.0" @@ -771,11 +816,13 @@ dependencies = [ "lazy_static", "log", "nix 0.25.1", + "rand", "rayon", "regex", "shlex", "time", "timer", + "tmux_interface", "tuikit", "unicode-width", "vte", @@ -876,6 +923,12 @@ dependencies = [ "chrono", ] +[[package]] +name = "tmux_interface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3885889703554d7b0dd515111432fc3b2f9cceb15b2139f9b12da73365299c05" + [[package]] name = "tuikit" version = "0.5.0" @@ -1114,3 +1167,24 @@ dependencies = [ "clap_mangen", "skim", ] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/bin/sk-tmux b/bin/sk-tmux index fc7456de..2b3301d5 100755 --- a/bin/sk-tmux +++ b/bin/sk-tmux @@ -27,6 +27,7 @@ # sk-tmux: starts sk in a tmux pane # usage: sk-tmux [LAYOUT OPTIONS] [--] [SK OPTIONS] +echo "[WRN] This script is deprecated in favor or \`sk --tmux\` and will be removed in a later release" >&2 fail() { >&2 echo "$1" diff --git a/man/man1/sk-tmux.1 b/man/man1/sk-tmux.1 index 0600e32a..3182b609 100644 --- a/man/man1/sk-tmux.1 +++ b/man/man1/sk-tmux.1 @@ -22,10 +22,10 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH sk-tmux 1 "Oct 2018" "sk 0.10.4" "sk-tmux - open sk in tmux split pane" +.TH sk-tmux 1 "Oct 2018" "sk 0.10.4" "! DEPRECATED IN FLAVOR OF sk --tmux ! - sk-tmux - open sk in tmux split pane" .SH NAME -sk-tmux - open sk in tmux split pane +! DEPRECATED IN FLAVOR OF sk --tmux ! - sk-tmux - open sk in tmux split pane .SH SYNOPSIS .B sk-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [sk OPTIONS] diff --git a/skim/Cargo.toml b/skim/Cargo.toml index bbd4afc0..53b03813 100644 --- a/skim/Cargo.toml +++ b/skim/Cargo.toml @@ -43,6 +43,8 @@ crossbeam = "0.8.2" beef = "0.5.2" # compact cow defer-drop = "1.3.0" indexmap = "2.6.0" +rand = "0.8.5" +tmux_interface = "0.3.2" [features] default = ["cli"] diff --git a/skim/src/bin/main.rs b/skim/src/bin/main.rs index cc1ca714..4d15de22 100644 --- a/skim/src/bin/main.rs +++ b/skim/src/bin/main.rs @@ -119,10 +119,12 @@ fn sk_main() -> Result { //------------------------------------------------------------------------------ // output - let Some(result) = Skim::run_with(&opts, rx_item) else { - return Ok(0); + let Some(result) = (match opts.tmux { + Some(_) => crate::tmux::run_with(&opts), + None => Skim::run_with(&opts, rx_item), + }) else { + return Ok(135); }; - if result.is_abort { return Ok(130); } diff --git a/skim/src/lib.rs b/skim/src/lib.rs index 29f7f3c6..e1f73284 100644 --- a/skim/src/lib.rs +++ b/skim/src/lib.rs @@ -45,6 +45,7 @@ mod selection; mod spinlock; mod theme; mod util; +pub mod tmux; //------------------------------------------------------------------------------ pub trait AsAny { diff --git a/skim/src/options.rs b/skim/src/options.rs index d6284367..fd7929c3 100644 --- a/skim/src/options.rs +++ b/skim/src/options.rs @@ -633,8 +633,6 @@ pub struct SkimOptions { #[arg(long, short, help_heading = "Scripting")] pub filter: Option, - /// Reserved for later use - /// /// Run in a tmux popup /// /// Format: sk --tmux [,SIZE[%]][,SIZE[%]] diff --git a/skim/src/tmux.rs b/skim/src/tmux.rs new file mode 100644 index 00000000..7f9a787c --- /dev/null +++ b/skim/src/tmux.rs @@ -0,0 +1,183 @@ +use std::{borrow::Cow, sync::Arc}; + +use rand::{distributions::Alphanumeric, Rng}; +use tmux_interface::Tmux; +use tuikit::key::Key; + +use crate::{event::Event, SkimItem, SkimOptions, SkimOutput}; + +#[derive(Debug)] +enum TmuxWindowDir { + Center, + Top, + Bottom, + Left, + Right, +} + +impl From<&str> for TmuxWindowDir { + fn from(value: &str) -> Self { + use TmuxWindowDir::*; + match value { + "center" => Center, + "top" => Top, + "bottom" => Bottom, + "left" => Left, + "right" => Right, + _ => Center, + } + } +} + +#[derive(Debug)] +pub struct TmuxOptions<'a> { + pub width: &'a str, + pub height: &'a str, + pub x: &'a str, + pub y: &'a str, +} + +struct SkimTmuxOutput { + line: String, +} + +impl SkimItem for SkimTmuxOutput { + fn text(&self) -> Cow<'_, str> { + Cow::from(&self.line) + } +} + +impl<'a> From<&'a String> for TmuxOptions<'a> { + fn from(value: &'a String) -> Self { + let (raw_dir, size) = value.split_once(",").unwrap_or((value, "50%")); + let dir = TmuxWindowDir::from(raw_dir); + let (height, width) = if let Some((lhs, rhs)) = size.split_once(",") { + match dir { + TmuxWindowDir::Center | TmuxWindowDir::Left | TmuxWindowDir::Right => (rhs, lhs), + TmuxWindowDir::Top | TmuxWindowDir::Bottom => (lhs, rhs), + } + } else { + match dir { + TmuxWindowDir::Left | TmuxWindowDir::Right => ("100%", size), + TmuxWindowDir::Top | TmuxWindowDir::Bottom => (size, "100%"), + TmuxWindowDir::Center => (size, size), + } + }; + + let (x, y) = match dir { + TmuxWindowDir::Center => ("C", "C"), + TmuxWindowDir::Top => ("C", "0%"), + TmuxWindowDir::Bottom => ("C", "100%"), + TmuxWindowDir::Left => ("0%", "C"), + TmuxWindowDir::Right => ("100%", "C"), + }; + + Self { height, width, x, y } + } +} + +pub fn run_with(opts: &SkimOptions) -> Option { + // Create temp dir for downstream output + let temp_dir_name = format!( + "sk-tmux-{}", + &rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(8) + .map(char::from) + .collect::(), + ); + let temp_dir = std::env::temp_dir().join(&temp_dir_name); + std::fs::create_dir(&temp_dir).unwrap_or_else(|_| panic!("Failed to create temp dir {}", temp_dir.display())); + + let tmp_stdout = temp_dir.join("stdout"); + let tmp_stderr = temp_dir.join("stderr"); + + // Build args to send to downstream sk invocation + let mut tmux_shell_cmd = String::new(); + let mut prev_is_tmux_flag = false; + for arg in std::env::args() { + debug!("[tmux] Got arg {}", arg); + if prev_is_tmux_flag { + prev_is_tmux_flag = false; + if !arg.starts_with("-") { + continue; + } + } + if arg == "--tmux" { + debug!("[tmux] Found tmux arg, skipping this and the next"); + prev_is_tmux_flag = true; + continue; + } else if arg.starts_with("--tmux") { + debug!("[tmux] Found equal tmux arg, skipping"); + continue; + } + tmux_shell_cmd.push_str(&format!(" {arg}")); + } + tmux_shell_cmd.push_str(&format!(" >{} 2>{}", tmp_stdout.display(), tmp_stderr.display())); + + // Run downstream sk in tmux + let raw_tmux_opts = &opts.tmux.clone().unwrap(); + let tmux_opts = TmuxOptions::from(raw_tmux_opts); + let tmux_cmd = tmux_interface::commands::tmux_command::TmuxCommand::new() + .name("popup") + .push_flag("-E") + .push_option("-h", tmux_opts.height) + .push_option("-w", tmux_opts.width) + .push_option("-x", tmux_opts.x) + .push_option("-y", tmux_opts.y) + .push_param(tmux_shell_cmd) + .to_owned(); + + let status = Tmux::with_command(tmux_cmd) + .output() + .expect("Failed to run command in popup") + .status(); + + let output_ending = if opts.print0 { "\0" } else { "\n" }; + let stdout_bytes = std::fs::read_to_string(tmp_stdout).unwrap_or_default(); + let mut stdout = stdout_bytes.split(output_ending); + + let query_str = if opts.print_query && status.success() { + stdout.next().expect("Not enough lines to unpack in downstream result") + } else { + "" + }; + + let command_str = if opts.print_cmd && status.success() { + stdout.next().expect("Not enough lines to unpack in downstream result") + } else { + "" + }; + + let accept_key = if !opts.expect.is_empty() && status.success() { + Some( + stdout + .next() + .expect("Not enough lines to unpack in downstream result") + .to_string(), + ) + } else { + None + }; + + let mut selected_items: Vec> = vec![]; + for line in stdout { + selected_items.push(Arc::new(SkimTmuxOutput { line: line.to_string() })); + } + + let is_abort = !status.success(); + let final_event = match is_abort { + true => Event::EvActAbort, + false => Event::EvActAccept(accept_key), + }; + + let skim_output = SkimOutput { + final_event, + is_abort, + final_key: Key::Null, + query: query_str.to_string(), + cmd: command_str.to_string(), + selected_items, + }; + Some(skim_output) +}