diff --git a/Cargo.lock b/Cargo.lock index 83252cc..286eff6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,6 +147,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "edit" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323032447eba6f5aca88b46d6e7815151c16c53e4128569420c09d7840db3bfc" +dependencies = [ + "tempfile", + "which", +] + [[package]] name = "getrandom" version = "0.1.15" @@ -200,6 +210,7 @@ version = "2.13.0" dependencies = [ "anyhow", "directories-next", + "edit", "lazy_static", "raw_tty", "regex", @@ -218,6 +229,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -278,6 +295,47 @@ dependencies = [ "proc-macro2 1.0.21", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + [[package]] name = "raw_tty" version = "0.1.0" @@ -332,6 +390,15 @@ version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "rust-argon2" version = "0.8.2" @@ -448,6 +515,20 @@ dependencies = [ "unicode-xid 0.2.1", ] +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "terminal_size" version = "0.1.13" @@ -576,6 +657,15 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index b1c4b57..2801ef5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ shellwords = "1.1.0" anyhow = "1.0.32" thiserror = "1.0.20" strip-ansi-escapes = "0.1.0" +edit = "0.1.2" [lib] name = "navi" diff --git a/src/cheatsh.rs b/src/cheatsh.rs index 9cec1ec..224e9fc 100644 --- a/src/cheatsh.rs +++ b/src/cheatsh.rs @@ -27,7 +27,7 @@ fn lines(query: &str, markdown: &str) -> impl Iterator Result, Error> { let mut variables = VariableMap::new(); let mut visited_lines = HashSet::new(); - parser::read_lines(lines(query, cheat), "cheat.sh", &mut variables, &mut visited_lines, writer, stdin)?; + parser::read_lines(lines(query, cheat), "cheat.sh", 0, &mut variables, &mut visited_lines, writer, stdin)?; Ok(Some(variables)) } @@ -85,7 +85,7 @@ impl Fetcher { } impl fetcher::Fetcher for Fetcher { - fn fetch(&self, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer) -> Result, Error> { + fn fetch(&self, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer, _files: &mut Vec) -> Result, Error> { let cheat = fetch(&self.query)?; read_all(&self.query, &cheat, stdin, writer) } diff --git a/src/cmds/alfred.rs b/src/cmds/alfred.rs index b541c8d..4898ba5 100644 --- a/src/cmds/alfred.rs +++ b/src/cmds/alfred.rs @@ -18,7 +18,7 @@ pub fn main(config: Config) -> Result<(), Error> { let fetcher = filesystem::Fetcher::new(config.path); fetcher - .fetch(stdin, &mut writer) + .fetch(stdin, &mut writer, &mut Vec::new()) .context("Failed to parse variables intended for finder")?; // make sure everything was printed to stdout before attempting to close the items vector @@ -55,7 +55,7 @@ pub fn suggestions(config: Config, dry_run: bool) -> Result<(), Error> { let fetcher = filesystem::Fetcher::new(config.path); let variables = fetcher - .fetch(stdin, &mut writer) + .fetch(stdin, &mut writer, &mut Vec::new()) .context("Failed to parse variables intended for finder")? .expect("Empty variable map"); diff --git a/src/cmds/core.rs b/src/cmds/core.rs index 8989f83..58e5177 100644 --- a/src/cmds/core.rs +++ b/src/cmds/core.rs @@ -15,9 +15,11 @@ use crate::tldr; use crate::welcome; use anyhow::Context; use anyhow::Error; +use edit; use std::env; use std::fs; use std::io::Write; +use std::path::Path; use std::process::{Command, Stdio}; fn gen_core_finder_opts(config: &Config) -> Result { @@ -38,7 +40,7 @@ fn gen_core_finder_opts(config: &Config) -> Result { Ok(opts) } -fn extract_from_selections(raw_snippet: &str, is_single: bool) -> Result<(&str, &str, &str, &str), Error> { +fn extract_from_selections(raw_snippet: &str, is_single: bool) -> Result<(&str, &str, &str, &str, Option), Error> { let mut lines = raw_snippet.split('\n'); let key = if is_single { "enter" @@ -51,7 +53,8 @@ fn extract_from_selections(raw_snippet: &str, is_single: bool) -> Result<(&str, let tags = parts.next().unwrap_or(""); let comment = parts.next().unwrap_or(""); let snippet = parts.next().unwrap_or(""); - Ok((key, tags, comment, snippet)) + let file_index = parts.next().unwrap_or("").parse().ok(); + Ok((key, tags, comment, snippet, file_index)) } fn prompt_finder(variable_name: &str, config: &Config, suggestion: Option<&Suggestion>, variable_count: usize) -> Result { @@ -134,7 +137,7 @@ NAVIEOF let (output, _) = config .finder - .call(opts, |stdin| { + .call(opts, &mut Vec::new(), |stdin, _| { stdin.write_all(suggestions.as_bytes()).context("Could not write to finder's stdin")?; Ok(None) }) @@ -185,9 +188,11 @@ fn replace_variables_from_snippet(snippet: &str, tags: &str, variables: Variable pub fn main(config: Config) -> Result<(), Error> { let opts = gen_core_finder_opts(&config).context("Failed to generate finder options")?; + let mut files = Vec::new(); + let (raw_selection, variables) = config .finder - .call(opts, |stdin| { + .call(opts, &mut files, |stdin, files| { let mut writer = display::terminal::Writer::new(); let fetcher: Box = match config.source() { @@ -197,7 +202,7 @@ pub fn main(config: Config) -> Result<(), Error> { }; let res = fetcher - .fetch(stdin, &mut writer) + .fetch(stdin, &mut writer, files) .context("Failed to parse variables intended for finder")?; if let Some(variables) = res { @@ -209,7 +214,7 @@ pub fn main(config: Config) -> Result<(), Error> { }) .context("Failed getting selection and variables from finder")?; - let (key, tags, comment, snippet) = extract_from_selections(&raw_selection, config.get_best_match())?; + let (key, tags, comment, snippet, file_index) = 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); @@ -230,6 +235,8 @@ pub fn main(config: Config) -> Result<(), Error> { Action::EXECUTE => { if key == "ctrl-y" { clipboard::copy(interpolated_snippet)?; + } else if key == "ctrl-o" { + edit::edit_file(Path::new(&files[file_index.expect("No files found")])).expect("Cound not open file in external editor"); } else { Command::new("bash") .arg("-c") diff --git a/src/cmds/repo.rs b/src/cmds/repo.rs index b0b323a..8e5ea5f 100644 --- a/src/cmds/repo.rs +++ b/src/cmds/repo.rs @@ -25,7 +25,7 @@ pub fn browse(finder: &FinderChoice) -> Result<(), Error> { }; let (repo, _) = finder - .call(opts, |stdin| { + .call(opts, &mut Vec::new(), |stdin, _| { stdin.write_all(repos.as_bytes()).context("Unable to prompt featured repositories")?; Ok(None) }) @@ -44,7 +44,7 @@ pub fn ask_if_should_import_all(finder: &FinderChoice) -> Result { }; let (response, _) = finder - .call(opts, |stdin| { + .call(opts, &mut Vec::new(), |stdin, _| { stdin.write_all(b"Yes\nNo").context("Unable to writer alternatives")?; Ok(None) }) @@ -85,7 +85,7 @@ pub fn add(uri: String, finder: &FinderChoice) -> Result<(), Error> { all_files } else { let (files, _) = finder - .call(opts, |stdin| { + .call(opts, &mut Vec::new(), |stdin, _| { stdin.write_all(all_files.as_bytes()).context("Unable to prompt cheats to import")?; Ok(None) }) diff --git a/src/display/terminal.rs b/src/display/terminal.rs index d0968e8..ed864fd 100644 --- a/src/display/terminal.rs +++ b/src/display/terminal.rs @@ -152,16 +152,18 @@ impl Writer { impl display::Writer for Writer { fn write(&mut self, item: Item) -> String { format!( - "{tag_color}{tags_short}{delimiter}{comment_color}{comment_short}{delimiter}{snippet_color}{snippet_short}{delimiter}{tags}{delimiter}{comment}{delimiter}{snippet}{delimiter}\n", - tags_short = limit_str(item.tags, self.tag_width), - comment_short = limit_str(item.comment, self.comment_width), - snippet_short = display::fix_newlines(item.snippet), - comment_color = color::Fg(*COMMENT_COLOR), - tag_color = color::Fg(*TAG_COLOR), - snippet_color = color::Fg(*SNIPPET_COLOR), - tags = item.tags, - comment = item.comment, - delimiter = display::DELIMITER, - snippet = &item.snippet) + "{tag_color}{tags_short}{delimiter}{comment_color}{comment_short}{delimiter}{snippet_color}{snippet_short}{delimiter}{tags}{delimiter}{comment}{delimiter}{snippet}{delimiter}{file_index}{delimiter}\n", + tags_short = limit_str(item.tags, self.tag_width), + comment_short = limit_str(item.comment, self.comment_width), + snippet_short = display::fix_newlines(item.snippet), + comment_color = color::Fg(*COMMENT_COLOR), + tag_color = color::Fg(*TAG_COLOR), + snippet_color = color::Fg(*SNIPPET_COLOR), + tags = item.tags, + comment = item.comment, + delimiter = display::DELIMITER, + snippet = &item.snippet, + file_index = item.file_index, + ) } } diff --git a/src/fetcher/filesystem.rs b/src/fetcher/filesystem.rs index 2efb824..37fcd25 100644 --- a/src/fetcher/filesystem.rs +++ b/src/fetcher/filesystem.rs @@ -31,13 +31,14 @@ fn paths_from_path_param<'a>(env_var: &'a str) -> impl Iterator // TODO: move fn read_file( path: &str, + file_index: usize, variables: &mut VariableMap, visited_lines: &mut HashSet, writer: &mut dyn Writer, stdin: &mut std::process::ChildStdin, ) -> Result<(), Error> { let lines = read_lines(path)?; - parser::read_lines(lines, path, variables, visited_lines, writer, stdin) + parser::read_lines(lines, path, file_index, variables, visited_lines, writer, stdin) } pub fn default_cheat_pathbuf() -> Result { @@ -57,7 +58,7 @@ pub fn cheat_paths(path: Option) -> Result { } } -pub fn read_all(path: Option, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer) -> Result, Error> { +pub fn read_all(path: Option, files: &mut Vec, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer) -> Result, Error> { let mut variables = VariableMap::new(); let mut found_something = false; let mut visited_lines = HashSet::new(); @@ -73,7 +74,8 @@ pub fn read_all(path: Option, stdin: &mut std::process::ChildStdin, writ for folder in folders { for file in all_cheat_files(folder) { let full_filename = format!("{}/{}", &folder, &file); - if read_file(&full_filename, &mut variables, &mut visited_lines, writer, stdin).is_ok() && !found_something { + files.push(full_filename.clone()); + if read_file(&full_filename, files.len()-1, &mut variables, &mut visited_lines, writer, stdin).is_ok() && !found_something { found_something = true } } @@ -101,7 +103,7 @@ mod tests { let child_stdin = child.stdin.as_mut().unwrap(); let mut visited_lines: HashSet = HashSet::new(); let mut writer: Box = Box::new(display::terminal::Writer::new()); - read_file(path, &mut variables, &mut visited_lines, &mut *writer, child_stdin).unwrap(); + read_file(path, 0, &mut variables, &mut visited_lines, &mut *writer, child_stdin).unwrap(); let expected_suggestion = ( r#" echo -e "$(whoami)\nroot" "#.to_string(), Some(FinderOpts { diff --git a/src/fetcher/mod.rs b/src/fetcher/mod.rs index 36b77f2..3f91657 100644 --- a/src/fetcher/mod.rs +++ b/src/fetcher/mod.rs @@ -5,5 +5,5 @@ use crate::structures::cheat::VariableMap; use anyhow::Error; pub trait Fetcher { - fn fetch(&self, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer) -> Result, Error>; + fn fetch(&self, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer, files: &mut Vec) -> Result, Error>; } diff --git a/src/filesystem.rs b/src/filesystem.rs index 93a788a..f93f6a7 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -21,7 +21,7 @@ impl Fetcher { } impl fetcher::Fetcher for Fetcher { - fn fetch(&self, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer) -> Result, Error> { - read_all(self.path.clone(), stdin, writer) + fn fetch(&self, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer, files: &mut Vec) -> Result, Error> { + read_all(self.path.clone(), files, stdin, writer) } } diff --git a/src/finder.rs b/src/finder.rs index 9914d5f..d41bf7f 100644 --- a/src/finder.rs +++ b/src/finder.rs @@ -14,9 +14,9 @@ pub enum FinderChoice { } pub trait Finder { - fn call(&self, opts: Opts, stdin_fn: F) -> Result<(String, Option), Error> + fn call(&self, opts: Opts, files: &mut Vec, stdin_fn: F) -> Result<(String, Option), Error> where - F: Fn(&mut process::ChildStdin) -> Result, Error>; + F: Fn(&mut process::ChildStdin, &mut Vec) -> Result, Error>; } fn apply_map(text: String, map_fn: Option) -> String { @@ -102,9 +102,9 @@ fn parse(out: Output, opts: Opts) -> Result { } impl Finder for FinderChoice { - fn call(&self, finder_opts: Opts, stdin_fn: F) -> Result<(String, Option), Error> + fn call(&self, finder_opts: Opts, files: &mut Vec, stdin_fn: F) -> Result<(String, Option), Error> where - F: Fn(&mut process::ChildStdin) -> Result, Error>, + F: Fn(&mut process::ChildStdin, &mut Vec) -> Result, Error>, { let finder_str = match self { Self::Fzf => "fzf", @@ -152,7 +152,7 @@ impl Finder for FinderChoice { command.args(&["--print-query", "--no-select-1"]); } SuggestionType::SnippetSelection => { - command.args(&["--expect", "ctrl-y,enter"]); + command.args(&["--expect", "ctrl-y,ctrl-o,enter"]); } SuggestionType::SingleRecommendation => { command.args(&["--print-query", "--expect", "tab,enter"]); @@ -215,7 +215,7 @@ impl Finder for FinderChoice { }; let stdin = child.stdin.as_mut().ok_or_else(|| anyhow!("Unable to acquire stdin of finder"))?; - let result_map = stdin_fn(stdin).context("Failed to pass data to finder")?; + let result_map = stdin_fn(stdin, files).context("Failed to pass data to finder")?; let out = child.wait_with_output().context("Failed to wait for finder")?; diff --git a/src/parser.rs b/src/parser.rs index 80320a5..4501c5b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -99,7 +99,14 @@ fn parse_variable_line(line: &str) -> Result<(&str, &str, Option), E Ok((variable, command, command_options)) } -fn write_cmd(tags: &str, comment: &str, snippet: &str, writer: &mut dyn Writer, stdin: &mut std::process::ChildStdin) -> Result<(), Error> { +fn write_cmd( + tags: &str, + comment: &str, + snippet: &str, + file_index: &usize, + writer: &mut dyn Writer, + stdin: &mut std::process::ChildStdin, +) -> Result<(), Error> { if snippet.len() <= 1 { Ok(()) } else { @@ -107,6 +114,7 @@ fn write_cmd(tags: &str, comment: &str, snippet: &str, writer: &mut dyn Writer, tags: &tags, comment: &comment, snippet: &snippet, + file_index: &file_index, }; stdin .write_all(writer.write(item).as_bytes()) @@ -125,6 +133,7 @@ fn without_prefix(line: &str) -> String { pub fn read_lines( lines: impl Iterator>, id: &str, + file_index: usize, variables: &mut VariableMap, visited_lines: &mut HashSet, writer: &mut dyn Writer, @@ -149,7 +158,7 @@ pub fn read_lines( } // tag else if line.starts_with('%') { - should_break = write_cmd(&tags, &comment, &snippet, writer, stdin).is_err(); + should_break = write_cmd(&tags, &comment, &snippet, &file_index, writer, stdin).is_err(); snippet = String::from(""); tags = without_prefix(&line); } @@ -163,13 +172,13 @@ pub fn read_lines( } // comment else if line.starts_with('#') { - should_break = write_cmd(&tags, &comment, &snippet, writer, stdin).is_err(); + should_break = write_cmd(&tags, &comment, &snippet, &file_index, writer, stdin).is_err(); snippet = String::from(""); comment = without_prefix(&line); } // variable else if line.starts_with('$') { - should_break = write_cmd(&tags, &comment, &snippet, writer, stdin).is_err(); + should_break = write_cmd(&tags, &comment, &snippet, &file_index, writer, stdin).is_err(); snippet = String::from(""); let (variable, command, opts) = parse_variable_line(&line) .with_context(|| format!("Failed to parse variable line. See line number {} in cheatsheet `{}`", line_nr + 1, id))?; @@ -191,7 +200,7 @@ pub fn read_lines( } if !should_break { - let _ = write_cmd(&tags, &comment, &snippet, writer, stdin); + let _ = write_cmd(&tags, &comment, &snippet, &file_index, writer, stdin); } Ok(()) diff --git a/src/structures/item.rs b/src/structures/item.rs index 64fe432..c370e6f 100644 --- a/src/structures/item.rs +++ b/src/structures/item.rs @@ -2,4 +2,5 @@ pub struct Item<'a> { pub tags: &'a str, pub comment: &'a str, pub snippet: &'a str, + pub file_index: &'a usize, } diff --git a/src/tldr.rs b/src/tldr.rs index f5801b5..d7d985a 100644 --- a/src/tldr.rs +++ b/src/tldr.rs @@ -66,6 +66,7 @@ fn read_all(query: &str, markdown: &str, stdin: &mut std::process::ChildStdin, w parser::read_lines( markdown_lines(query, markdown), "markdown", + 0, &mut variables, &mut visited_lines, writer, @@ -144,7 +145,7 @@ impl Fetcher { } impl fetcher::Fetcher for Fetcher { - fn fetch(&self, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer) -> Result, Error> { + fn fetch(&self, stdin: &mut std::process::ChildStdin, writer: &mut dyn Writer, _files: &mut Vec) -> Result, Error> { let markdown = fetch(&self.query)?; read_all(&self.query, &markdown, stdin, writer) } diff --git a/src/welcome.rs b/src/welcome.rs index 617c253..5be067f 100644 --- a/src/welcome.rs +++ b/src/welcome.rs @@ -7,6 +7,7 @@ fn add_msg(tags: &str, comment: &str, snippet: &str, writer: &mut dyn Writer, st tags: &tags, comment: &comment, snippet: &snippet, + file_index: &0, }; stdin.write_all(writer.write(item).as_bytes()).expect("Could not write to fzf's stdin"); }