use crate::deser; use crate::prelude::*; use std::io::Write; use std::process::{self, Output}; use std::process::{Command, Stdio}; pub mod structures; use clap::ValueEnum; pub use post::process; use structures::Opts; use structures::SuggestionType; const MIN_FZF_VERSION_MAJOR: u32 = 0; const MIN_FZF_VERSION_MINOR: u32 = 23; const MIN_FZF_VERSION_PATCH: u32 = 1; mod post; #[derive(Debug, Clone, Copy, Deserialize, ValueEnum)] pub enum FinderChoice { Fzf, Skim, } impl FromStr for FinderChoice { type Err = &'static str; fn from_str(s: &str) -> Result { match s { "fzf" => Ok(FinderChoice::Fzf), "skim" => Ok(FinderChoice::Skim), _ => Err("no match"), } } } fn parse(out: Output, opts: Opts) -> Result { let text = match out.status.code() { Some(0) | Some(1) | Some(2) => { String::from_utf8(out.stdout).context("Invalid utf8 received from finder")? } Some(130) => process::exit(130), _ => { let err = String::from_utf8(out.stderr) .unwrap_or_else(|_| "".to_owned()); panic!("External command failed:\n {err}") } }; let output = post::parse_output_single(text, opts.suggestion_type)?; post::process(output, opts.column, opts.delimiter.as_deref(), opts.map) } impl FinderChoice { fn check_fzf_version() -> Option<(u32, u32, u32)> { let output = Command::new("fzf") .arg("--version") .output() .ok()? .stdout; let version_string = String::from_utf8(output).ok()?; let version_parts: Vec<_> = version_string.split('.').collect(); if version_parts.len() == 3 { let major = version_parts[0].parse().ok()?; let minor = version_parts[1].parse().ok()?; let patch = version_parts[2].split_whitespace().next()?.parse().ok()?; Some((major, minor, patch)) } else { None } } pub fn call(&self, finder_opts: Opts, stdin_fn: F) -> Result<(String, R)> where F: Fn(&mut dyn Write) -> Result, { let finder_str = match self { Self::Fzf => "fzf", Self::Skim => "sk", }; if let Self::Fzf = self { if let Some((major, minor, patch)) = Self::check_fzf_version() { if major == MIN_FZF_VERSION_MAJOR && minor < MIN_FZF_VERSION_MINOR && patch < MIN_FZF_VERSION_PATCH { eprintln!("Warning: Fzf version {}.{} does not support the preview window layout used by navi.", major, minor); eprintln!("Consider updating Fzf to a version >= {}.{}.{} or use a compatible layout.", MIN_FZF_VERSION_MAJOR, MIN_FZF_VERSION_MINOR, MIN_FZF_VERSION_PATCH ); process::exit(1); } } } let mut command = Command::new(finder_str); let opts = finder_opts.clone(); let preview_height = match self { FinderChoice::Skim => 3, _ => 2, }; let bindings = if opts.suggestion_type == SuggestionType::MultipleSelections { ",ctrl-r:toggle-all" } else { "" }; command.args([ "--preview", "", "--preview-window", format!("up:{preview_height}:nohidden").as_str(), "--delimiter", deser::terminal::DELIMITER.to_string().as_str(), "--ansi", "--bind", format!("ctrl-j:down,ctrl-k:up{bindings}").as_str(), "--exact", ]); if !opts.show_all_columns { command.args(["--with-nth", "1,2,3"]); } if !opts.prevent_select1 { if let Self::Fzf = self { command.arg("--select-1"); } } match opts.suggestion_type { SuggestionType::MultipleSelections => { command.arg("--multi"); } SuggestionType::Disabled => { if let Self::Fzf = self { command.args(["--print-query", "--no-select-1"]); }; } SuggestionType::SnippetSelection => { command.args(["--expect", "ctrl-y,ctrl-o,enter"]); } SuggestionType::SingleRecommendation => { command.args(["--print-query", "--expect", "tab,enter"]); } _ => {} } if let Some(p) = opts.preview { command.args(["--preview", &p]); } if let Some(q) = opts.query { command.args(["--query", &q]); } if let Some(f) = opts.filter { command.args(["--filter", &f]); } if let Some(d) = opts.delimiter { command.args(["--delimiter", &d]); } if let Some(h) = opts.header { command.args(["--header", &h]); } if let Some(p) = opts.prompt { command.args(["--prompt", &p]); } if let Some(pw) = opts.preview_window { command.args(["--preview-window", &pw]); } if opts.header_lines > 0 { command.args(["--header-lines", format!("{}", opts.header_lines).as_str()]); } if let Some(o) = opts.overrides { shellwords::split(&o)? .into_iter() .filter(|s| !s.is_empty()) .for_each(|s| { command.arg(s); }); } command .env("SHELL", CONFIG.finder_shell()) .stdin(Stdio::piped()) .stdout(Stdio::piped()); debug!(cmd = ?command); let child = command.spawn(); let mut child = match child { Ok(x) => x, Err(_) => { let repo = match self { Self::Fzf => "https://github.com/junegunn/fzf", Self::Skim => "https://github.com/lotabout/skim", }; eprintln!( "navi was unable to call {cmd}. Please make sure it's correctly installed. Refer to {repo} for more info.", cmd = &finder_str, repo = repo ); process::exit(33) } }; let stdin = child .stdin .as_mut() .ok_or_else(|| anyhow!("Unable to acquire stdin of finder"))?; let mut writer: Box<&mut dyn Write> = Box::new(stdin); let return_value = stdin_fn(&mut writer).context("Failed to pass data to finder")?; let out = child.wait_with_output().context("Failed to wait for finder")?; let output = parse(out, finder_opts).context("Unable to get output")?; Ok((output, return_value)) } }