mirror of
https://github.com/denisidoro/navi.git
synced 2026-07-28 20:41:02 +00:00
237 lines
8.1 KiB
Rust
237 lines
8.1 KiB
Rust
use crate::cheatsh;
|
|
use crate::common::clipboard;
|
|
use crate::common::shell::BashSpawnError;
|
|
use crate::display;
|
|
use crate::env_vars;
|
|
use crate::fetcher::Fetcher;
|
|
use crate::filesystem;
|
|
use crate::finder::Finder;
|
|
use crate::structures::cheat::{Suggestion, VariableMap};
|
|
use crate::structures::config::Action;
|
|
use crate::structures::config::Config;
|
|
use crate::structures::config::Source;
|
|
use crate::structures::finder::{Opts as FinderOpts, SuggestionType};
|
|
use crate::tldr;
|
|
use crate::welcome;
|
|
use anyhow::Context;
|
|
use anyhow::Error;
|
|
use std::env;
|
|
use std::fs;
|
|
use std::io::Write;
|
|
use std::process::{Command, Stdio};
|
|
|
|
fn gen_core_finder_opts(config: &Config) -> Result<FinderOpts, Error> {
|
|
let opts = FinderOpts {
|
|
preview: if config.no_preview {
|
|
None
|
|
} else {
|
|
Some(format!("{} preview {{}}", filesystem::exe_string()?))
|
|
},
|
|
autoselect: !config.get_no_autoselect(),
|
|
overrides: config.fzf_overrides.clone(),
|
|
suggestion_type: SuggestionType::SnippetSelection,
|
|
query: if config.get_best_match() { None } else { config.get_query() },
|
|
filter: if config.get_best_match() { config.get_query() } else { None },
|
|
..Default::default()
|
|
};
|
|
|
|
Ok(opts)
|
|
}
|
|
|
|
fn extract_from_selections(raw_snippet: &str, is_single: bool) -> Result<(&str, &str, &str, &str), Error> {
|
|
let mut lines = raw_snippet.split('\n');
|
|
let key = if !is_single {
|
|
lines.next().context("Key was promised but not present in `selections`")?
|
|
} else {
|
|
"enter"
|
|
};
|
|
|
|
let mut parts = lines.next().context("No more parts in `selections`")?.split(display::DELIMITER).skip(3);
|
|
|
|
let tags = parts.next().unwrap_or("");
|
|
let comment = parts.next().unwrap_or("");
|
|
let snippet = parts.next().unwrap_or("");
|
|
Ok((key, tags, comment, snippet))
|
|
}
|
|
|
|
fn prompt_finder(variable_name: &str, config: &Config, suggestion: Option<&Suggestion>, variable_count: usize) -> Result<String, Error> {
|
|
env::remove_var(env_vars::PREVIEW_COLUMN);
|
|
env::remove_var(env_vars::PREVIEW_DELIMITER);
|
|
env::remove_var(env_vars::PREVIEW_MAP);
|
|
|
|
let mut extra_preview = None;
|
|
|
|
let (suggestions, opts) = if let Some(s) = suggestion {
|
|
let (suggestion_command, suggestion_opts) = s;
|
|
|
|
if let Some(sopts) = suggestion_opts {
|
|
if let Some(c) = &sopts.column {
|
|
env::set_var(env_vars::PREVIEW_COLUMN, c.to_string());
|
|
}
|
|
if let Some(d) = &sopts.delimiter {
|
|
env::set_var(env_vars::PREVIEW_DELIMITER, d);
|
|
}
|
|
if let Some(m) = &sopts.map {
|
|
env::set_var(env_vars::PREVIEW_MAP, m);
|
|
}
|
|
if let Some(p) = &sopts.preview {
|
|
extra_preview = Some(format!(";echo;{}", p));
|
|
}
|
|
}
|
|
|
|
let child = Command::new("bash")
|
|
.stdout(Stdio::piped())
|
|
.arg("-c")
|
|
.arg(&suggestion_command)
|
|
.spawn()
|
|
.map_err(|e| BashSpawnError::new(suggestion_command, e))?;
|
|
|
|
let text = String::from_utf8(child.wait_with_output().context("Failed to wait and collect output from bash")?.stdout)
|
|
.context("Suggestions are invalid utf8")?;
|
|
|
|
(text, suggestion_opts)
|
|
} else {
|
|
('\n'.to_string(), &None)
|
|
};
|
|
|
|
let mut opts = FinderOpts {
|
|
autoselect: !config.get_no_autoselect(),
|
|
overrides: config.fzf_overrides_var.clone(),
|
|
preview: Some(format!(
|
|
r#"navi preview-var "$(cat <<NAVIEOF
|
|
{{}}
|
|
NAVIEOF
|
|
)" "$(cat <<NAVIEOF
|
|
{{q}}
|
|
NAVIEOF
|
|
)" "{}"{}"#,
|
|
variable_name,
|
|
extra_preview.clone().unwrap_or_default()
|
|
)),
|
|
..opts.clone().unwrap_or_default()
|
|
};
|
|
|
|
if opts.preview_window.is_none() {
|
|
opts.preview_window = Some(if extra_preview.is_none() {
|
|
format!("up:{}", variable_count + 3)
|
|
} else {
|
|
"right:50%".to_string()
|
|
});
|
|
}
|
|
|
|
if suggestion.is_none() {
|
|
opts.suggestion_type = SuggestionType::Disabled;
|
|
};
|
|
|
|
let (output, _) = config
|
|
.finder
|
|
.call(opts, |stdin| {
|
|
stdin.write_all(suggestions.as_bytes()).context("Could not write to finder's stdin")?;
|
|
Ok(None)
|
|
})
|
|
.context("finder was unable to prompt with suggestions")?;
|
|
|
|
Ok(output)
|
|
}
|
|
|
|
fn unique_result_count(results: &[&str]) -> usize {
|
|
let mut vars = results.to_owned();
|
|
vars.sort();
|
|
vars.dedup();
|
|
vars.len()
|
|
}
|
|
|
|
fn replace_variables_from_snippet(snippet: &str, tags: &str, variables: VariableMap, config: &Config) -> Result<String, Error> {
|
|
let mut interpolated_snippet = String::from(snippet);
|
|
let variables_found: Vec<&str> = display::VAR_REGEX.find_iter(snippet).map(|m| m.as_str()).collect();
|
|
let variable_count = unique_result_count(&variables_found);
|
|
|
|
for bracketed_variable_name in variables_found {
|
|
let variable_name = &bracketed_variable_name[1..bracketed_variable_name.len() - 1];
|
|
|
|
let env_value = env::var(variable_name);
|
|
|
|
let value = if let Ok(e) = env_value {
|
|
e
|
|
} else if let Some(suggestion) = variables.get_suggestion(&tags, &variable_name) {
|
|
let mut new_suggestion = suggestion.clone();
|
|
new_suggestion.0 = replace_variables_from_snippet(&new_suggestion.0, tags, variables.clone(), config)?;
|
|
prompt_finder(variable_name, &config, Some(&new_suggestion), variable_count)?
|
|
} else {
|
|
prompt_finder(variable_name, &config, None, variable_count)?
|
|
};
|
|
|
|
env::set_var(variable_name, &value);
|
|
|
|
interpolated_snippet = if value.as_str() == "\n" {
|
|
interpolated_snippet.replacen(bracketed_variable_name, "", 1)
|
|
} else {
|
|
interpolated_snippet.replacen(bracketed_variable_name, value.as_str(), 1)
|
|
};
|
|
}
|
|
|
|
Ok(interpolated_snippet)
|
|
}
|
|
|
|
pub fn main(config: Config) -> Result<(), Error> {
|
|
let opts = gen_core_finder_opts(&config).context("Failed to generate finder options")?;
|
|
|
|
let (raw_selection, variables) = config
|
|
.finder
|
|
.call(opts, |stdin| {
|
|
let mut writer = display::terminal::Writer::new();
|
|
|
|
let fetcher: Box<dyn Fetcher> = match config.source() {
|
|
Source::CHEATSH(query) => Box::new(cheatsh::Fetcher::new(query)),
|
|
Source::TLDR(query) => Box::new(tldr::Fetcher::new(query)),
|
|
Source::FILESYSTEM(path) => Box::new(filesystem::Fetcher::new(path)),
|
|
};
|
|
|
|
let res = fetcher
|
|
.fetch(stdin, &mut writer)
|
|
.context("Failed to parse variables intended for finder")?;
|
|
|
|
if let Some(variables) = res {
|
|
Ok(Some(variables))
|
|
} else {
|
|
welcome::populate_cheatsheet(&mut writer, stdin);
|
|
Ok(Some(VariableMap::new()))
|
|
}
|
|
})
|
|
.context("Failed getting selection and variables from finder")?;
|
|
|
|
let (key, tags, comment, snippet) = extract_from_selections(&raw_selection, config.get_best_match())?;
|
|
|
|
env::set_var(env_vars::PREVIEW_INITIAL_SNIPPET, &snippet);
|
|
env::set_var(env_vars::PREVIEW_TAGS, &tags);
|
|
env::set_var(env_vars::PREVIEW_COMMENT, &comment);
|
|
|
|
let interpolated_snippet = display::with_new_lines(
|
|
replace_variables_from_snippet(snippet, tags, variables.expect("No variables received from finder"), &config)
|
|
.context("Failed to replace variables from snippet")?,
|
|
);
|
|
|
|
match config.action() {
|
|
Action::PRINT => {
|
|
println!("{}", interpolated_snippet);
|
|
}
|
|
Action::SAVE(filepath) => {
|
|
fs::write(filepath, interpolated_snippet).context("Unable to save output")?;
|
|
}
|
|
Action::EXECUTE => {
|
|
if key == "ctrl-y" {
|
|
clipboard::copy(interpolated_snippet)?;
|
|
} else {
|
|
Command::new("bash")
|
|
.arg("-c")
|
|
.arg(&interpolated_snippet[..])
|
|
.spawn()
|
|
.map_err(|e| BashSpawnError::new(&interpolated_snippet[..], e))?
|
|
.wait()
|
|
.context("bash was not running")?;
|
|
}
|
|
}
|
|
};
|
|
|
|
Ok(())
|
|
}
|