mirror of
https://github.com/denisidoro/navi
synced 2024-11-24 20:43:06 +00:00
Allow tag filtering (#508)
This commit is contained in:
parent
ab7289e3aa
commit
86b0ab67db
12 changed files with 179 additions and 113 deletions
|
@ -52,6 +52,8 @@ Output:
|
|||
&mut visited_lines,
|
||||
writer,
|
||||
stdin,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
Ok(Some(variables))
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ pub fn main(config: Config) -> Result<(), Error> {
|
|||
|
||||
writer::alfred::print_items_start(None);
|
||||
|
||||
let fetcher = filesystem::Fetcher::new(config.path);
|
||||
let fetcher = filesystem::Fetcher::new(config.path, None);
|
||||
fetcher
|
||||
.fetch(stdin, &mut writer, &mut Vec::new())
|
||||
.context("Failed to parse variables intended for finder")?;
|
||||
|
@ -61,7 +61,7 @@ pub fn suggestions(config: Config, dry_run: bool) -> Result<(), Error> {
|
|||
let stdin = child.stdin.as_mut().context("Unable to get stdin")?;
|
||||
let mut writer = writer::alfred::Writer::new();
|
||||
|
||||
let fetcher = filesystem::Fetcher::new(config.path);
|
||||
let fetcher = filesystem::Fetcher::new(config.path, None);
|
||||
let variables = fetcher
|
||||
.fetch(stdin, &mut writer, &mut Vec::new())
|
||||
.context("Failed to parse variables intended for finder")?
|
||||
|
|
|
@ -54,7 +54,7 @@ pub fn main(config: Config) -> Result<(), Error> {
|
|||
let fetcher: Box<dyn Fetcher> = match config.source() {
|
||||
Source::Cheats(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)),
|
||||
Source::Filesystem(path, rules) => Box::new(filesystem::Fetcher::new(path, rules)),
|
||||
};
|
||||
|
||||
let res = fetcher
|
||||
|
|
|
@ -25,19 +25,6 @@ fn paths_from_path_param(env_var: &str) -> impl Iterator<Item = &str> {
|
|||
env_var.split(':').filter(|folder| folder != &"")
|
||||
}
|
||||
|
||||
// TODO: move
|
||||
fn read_file(
|
||||
path: &str,
|
||||
file_index: usize,
|
||||
variables: &mut VariableMap,
|
||||
visited_lines: &mut HashSet<u64>,
|
||||
writer: &mut dyn Writer,
|
||||
stdin: &mut std::process::ChildStdin,
|
||||
) -> Result<(), Error> {
|
||||
let lines = read_lines(path)?;
|
||||
parser::read_lines(lines, path, file_index, variables, visited_lines, writer, stdin)
|
||||
}
|
||||
|
||||
pub fn default_cheat_pathbuf() -> Result<PathBuf, Error> {
|
||||
let base_dirs = BaseDirs::new().ok_or_else(|| anyhow!("Unable to get base dirs"))?;
|
||||
|
||||
|
@ -55,57 +42,60 @@ pub fn cheat_paths(path: Option<String>) -> Result<String, Error> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn read_all(
|
||||
path: Option<String>,
|
||||
files: &mut Vec<String>,
|
||||
stdin: &mut std::process::ChildStdin,
|
||||
writer: &mut dyn Writer,
|
||||
) -> Result<Option<VariableMap>, Error> {
|
||||
let mut variables = VariableMap::new();
|
||||
let mut found_something = false;
|
||||
let mut visited_lines = HashSet::new();
|
||||
let paths = cheat_paths(path);
|
||||
|
||||
if paths.is_err() {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let paths = paths.expect("Unable to get paths");
|
||||
let folders = paths_from_path_param(&paths);
|
||||
|
||||
for folder in folders {
|
||||
let folder_pathbuf = PathBuf::from(folder);
|
||||
for file in all_cheat_files(&folder_pathbuf) {
|
||||
files.push(file.clone());
|
||||
let index = files.len() - 1;
|
||||
if read_file(&file, index, &mut variables, &mut visited_lines, writer, stdin).is_ok()
|
||||
&& !found_something
|
||||
{
|
||||
found_something = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_something {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(variables))
|
||||
}
|
||||
|
||||
pub fn tmp_pathbuf() -> Result<PathBuf, Error> {
|
||||
let mut root = default_cheat_pathbuf()?;
|
||||
root.push("tmp");
|
||||
Ok(root)
|
||||
}
|
||||
|
||||
fn without_first(string: &str) -> String {
|
||||
string
|
||||
.char_indices()
|
||||
.next()
|
||||
.and_then(|(i, _)| string.get(i + 1..))
|
||||
.expect("Should have at least one char")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn gen_lists(tag_rules: Option<String>) -> (Option<Vec<String>>, Option<Vec<String>>) {
|
||||
let mut allowlist = None;
|
||||
let mut denylist: Option<Vec<String>> = None;
|
||||
|
||||
if let Some(rules) = tag_rules {
|
||||
let words: Vec<_> = rules.split(',').collect();
|
||||
allowlist = Some(
|
||||
words
|
||||
.iter()
|
||||
.filter(|w| !w.starts_with('!'))
|
||||
.map(|w| w.to_string())
|
||||
.collect(),
|
||||
);
|
||||
denylist = Some(
|
||||
words
|
||||
.iter()
|
||||
.filter(|w| w.starts_with('!'))
|
||||
.map(|w| without_first(w))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
(allowlist, denylist)
|
||||
}
|
||||
|
||||
pub struct Fetcher {
|
||||
path: Option<String>,
|
||||
allowlist: Option<Vec<String>>,
|
||||
denylist: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Fetcher {
|
||||
pub fn new(path: Option<String>) -> Self {
|
||||
Self { path }
|
||||
pub fn new(path: Option<String>, tag_rules: Option<String>) -> Self {
|
||||
let (allowlist, denylist) = gen_lists(tag_rules);
|
||||
Self {
|
||||
path,
|
||||
allowlist,
|
||||
denylist,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,13 +106,60 @@ impl fetcher::Fetcher for Fetcher {
|
|||
writer: &mut dyn Writer,
|
||||
files: &mut Vec<String>,
|
||||
) -> Result<Option<VariableMap>, Error> {
|
||||
read_all(self.path.clone(), files, stdin, writer)
|
||||
let mut variables = VariableMap::new();
|
||||
let mut found_something = false;
|
||||
let mut visited_lines = HashSet::new();
|
||||
|
||||
let path = self.path.clone();
|
||||
let paths = cheat_paths(path);
|
||||
|
||||
if paths.is_err() {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let paths = paths.expect("Unable to get paths");
|
||||
let folders = paths_from_path_param(&paths);
|
||||
|
||||
for folder in folders {
|
||||
let folder_pathbuf = PathBuf::from(folder);
|
||||
for file in all_cheat_files(&folder_pathbuf) {
|
||||
files.push(file.clone());
|
||||
let index = files.len() - 1;
|
||||
let read_file_result = {
|
||||
let lines = read_lines(&file)?;
|
||||
parser::read_lines(
|
||||
lines,
|
||||
&file,
|
||||
index,
|
||||
&mut variables,
|
||||
&mut visited_lines,
|
||||
writer,
|
||||
stdin,
|
||||
self.allowlist.as_ref(),
|
||||
self.denylist.as_ref(),
|
||||
)
|
||||
};
|
||||
|
||||
if read_file_result.is_ok() && !found_something {
|
||||
found_something = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_something {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(variables))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/* TODO
|
||||
|
||||
use crate::finder::structures::{Opts as FinderOpts, SuggestionType};
|
||||
use crate::writer;
|
||||
use std::process::{Command, Stdio};
|
||||
|
@ -161,6 +198,7 @@ mod tests {
|
|||
let actual_suggestion = variables.get_suggestion("ssh", "user");
|
||||
assert_eq!(Some(&expected_suggestion), actual_suggestion);
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn splitting_of_dirs_param_may_not_contain_empty_items() {
|
||||
|
|
|
@ -103,26 +103,32 @@ fn parse_variable_line(line: &str) -> Result<(&str, &str, Option<FinderOpts>), E
|
|||
}
|
||||
|
||||
fn write_cmd(
|
||||
tags: &str,
|
||||
comment: &str,
|
||||
snippet: &str,
|
||||
file_index: &usize,
|
||||
item: &Item,
|
||||
writer: &mut dyn Writer,
|
||||
stdin: &mut std::process::ChildStdin,
|
||||
allowlist: Option<&Vec<String>>,
|
||||
denylist: Option<&Vec<String>>,
|
||||
) -> Result<(), Error> {
|
||||
if snippet.len() <= 1 {
|
||||
Ok(())
|
||||
} else {
|
||||
let item = Item {
|
||||
tags: &tags,
|
||||
comment: &comment,
|
||||
snippet: &snippet,
|
||||
file_index: &file_index,
|
||||
};
|
||||
stdin
|
||||
.write_all(writer.write(item).as_bytes())
|
||||
.context("Failed to write command to finder's stdin")
|
||||
if item.snippet.len() <= 1 {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(list) = allowlist {
|
||||
for v in list {
|
||||
if !item.tags.contains(v) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(list) = denylist {
|
||||
for v in list {
|
||||
if item.tags.contains(v) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
return stdin
|
||||
.write_all(writer.write(item).as_bytes())
|
||||
.context("Failed to write command to finder's stdin");
|
||||
}
|
||||
|
||||
fn without_prefix(line: &str) -> String {
|
||||
|
@ -133,6 +139,7 @@ fn without_prefix(line: &str) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn read_lines(
|
||||
lines: impl Iterator<Item = Result<String, Error>>,
|
||||
id: &str,
|
||||
|
@ -141,10 +148,12 @@ pub fn read_lines(
|
|||
visited_lines: &mut HashSet<u64>,
|
||||
writer: &mut dyn Writer,
|
||||
stdin: &mut std::process::ChildStdin,
|
||||
allowlist: Option<&Vec<String>>,
|
||||
denylist: Option<&Vec<String>>,
|
||||
) -> Result<(), Error> {
|
||||
let mut tags = String::from("");
|
||||
let mut comment = String::from("");
|
||||
let mut snippet = String::from("");
|
||||
let mut item = Item::new();
|
||||
item.file_index = file_index;
|
||||
|
||||
let mut should_break = false;
|
||||
|
||||
for (line_nr, line_result) in lines.enumerate() {
|
||||
|
@ -156,34 +165,34 @@ pub fn read_lines(
|
|||
}
|
||||
|
||||
// duplicate
|
||||
if !tags.is_empty() && !comment.is_empty() {}
|
||||
if !item.tags.is_empty() && !item.comment.is_empty() {}
|
||||
// blank
|
||||
if line.is_empty() {
|
||||
}
|
||||
// tag
|
||||
else if line.starts_with('%') {
|
||||
should_break = write_cmd(&tags, &comment, &snippet, &file_index, writer, stdin).is_err();
|
||||
snippet = String::from("");
|
||||
tags = without_prefix(&line);
|
||||
should_break = write_cmd(&item, writer, stdin, allowlist, denylist).is_err();
|
||||
item.snippet = String::from("");
|
||||
item.tags = without_prefix(&line);
|
||||
}
|
||||
// dependency
|
||||
else if line.starts_with('@') {
|
||||
let tags_dependency = without_prefix(&line);
|
||||
variables.insert_dependency(&tags, &tags_dependency);
|
||||
variables.insert_dependency(&item.tags, &tags_dependency);
|
||||
}
|
||||
// metacomment
|
||||
else if line.starts_with(';') {
|
||||
}
|
||||
// comment
|
||||
else if line.starts_with('#') {
|
||||
should_break = write_cmd(&tags, &comment, &snippet, &file_index, writer, stdin).is_err();
|
||||
snippet = String::from("");
|
||||
comment = without_prefix(&line);
|
||||
should_break = write_cmd(&item, writer, stdin, allowlist, denylist).is_err();
|
||||
item.snippet = String::from("");
|
||||
item.comment = without_prefix(&line);
|
||||
}
|
||||
// variable
|
||||
else if line.starts_with('$') {
|
||||
should_break = write_cmd(&tags, &comment, &snippet, &file_index, writer, stdin).is_err();
|
||||
snippet = String::from("");
|
||||
should_break = write_cmd(&item, writer, stdin, allowlist, denylist).is_err();
|
||||
item.snippet = String::from("");
|
||||
let (variable, command, opts) = parse_variable_line(&line).with_context(|| {
|
||||
format!(
|
||||
"Failed to parse variable line. See line number {} in cheatsheet `{}`",
|
||||
|
@ -191,25 +200,25 @@ pub fn read_lines(
|
|||
id
|
||||
)
|
||||
})?;
|
||||
variables.insert_suggestion(&tags, &variable, (String::from(command), opts));
|
||||
variables.insert_suggestion(&item.tags, &variable, (String::from(command), opts));
|
||||
}
|
||||
// snippet
|
||||
else {
|
||||
let hash = fnv(&format!("{}{}", &comment, &line));
|
||||
let hash = fnv(&format!("{}{}", &item.comment, &line));
|
||||
if visited_lines.contains(&hash) {
|
||||
continue;
|
||||
}
|
||||
visited_lines.insert(hash);
|
||||
|
||||
if !(&snippet).is_empty() {
|
||||
snippet.push_str(writer::LINE_SEPARATOR);
|
||||
if !(&item.snippet).is_empty() {
|
||||
item.snippet.push_str(writer::LINE_SEPARATOR);
|
||||
}
|
||||
snippet.push_str(&line);
|
||||
item.snippet.push_str(&line);
|
||||
}
|
||||
}
|
||||
|
||||
if !should_break {
|
||||
let _ = write_cmd(&tags, &comment, &snippet, &file_index, writer, stdin);
|
||||
let _ = write_cmd(&item, writer, stdin, allowlist, denylist);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -111,6 +111,10 @@ pub struct Config {
|
|||
#[clap(long)]
|
||||
tldr: Option<String>,
|
||||
|
||||
/// [Experimental] Comma-separated list that acts as filter for tags. Parts starting with ! represent negation
|
||||
#[clap(long)]
|
||||
tag_rules: Option<String>,
|
||||
|
||||
/// Search for cheatsheets using the cheat.sh repository
|
||||
#[clap(long)]
|
||||
cheatsh: Option<String>,
|
||||
|
@ -222,7 +226,7 @@ pub enum AlfredCommand {
|
|||
}
|
||||
|
||||
pub enum Source {
|
||||
Filesystem(Option<String>),
|
||||
Filesystem(Option<String>, Option<String>),
|
||||
Tldr(String),
|
||||
Cheats(String),
|
||||
}
|
||||
|
@ -240,7 +244,7 @@ impl Config {
|
|||
} else if let Some(query) = self.cheatsh.clone() {
|
||||
Source::Cheats(query)
|
||||
} else {
|
||||
Source::Filesystem(self.path.clone())
|
||||
Source::Filesystem(self.path.clone(), self.tag_rules.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
pub struct Item<'a> {
|
||||
pub tags: &'a str,
|
||||
pub comment: &'a str,
|
||||
pub snippet: &'a str,
|
||||
pub file_index: &'a usize,
|
||||
pub struct Item {
|
||||
pub tags: String,
|
||||
pub comment: String,
|
||||
pub snippet: String,
|
||||
pub file_index: usize,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tags: "".to_string(),
|
||||
comment: "".to_string(),
|
||||
snippet: "".to_string(),
|
||||
file_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,6 +75,8 @@ fn read_all(
|
|||
&mut visited_lines,
|
||||
writer,
|
||||
stdin,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
Ok(Some(variables))
|
||||
}
|
||||
|
|
|
@ -10,13 +10,13 @@ fn add_msg(
|
|||
stdin: &mut std::process::ChildStdin,
|
||||
) {
|
||||
let item = Item {
|
||||
tags: &tags,
|
||||
comment: &comment,
|
||||
snippet: &snippet,
|
||||
file_index: &0,
|
||||
tags: tags.to_string(),
|
||||
comment: comment.to_string(),
|
||||
snippet: snippet.to_string(),
|
||||
file_index: 0,
|
||||
};
|
||||
stdin
|
||||
.write_all(writer.write(item).as_bytes())
|
||||
.write_all(writer.write(&item).as_bytes())
|
||||
.expect("Could not write to fzf's stdin");
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ pub fn print_items_end() {
|
|||
}
|
||||
|
||||
impl writer::Writer for Writer {
|
||||
fn write(&mut self, item: Item) -> String {
|
||||
fn write(&mut self, item: &Item) -> String {
|
||||
let prefix = if self.is_first {
|
||||
self.is_first = false;
|
||||
""
|
||||
|
@ -34,9 +34,9 @@ impl writer::Writer for Writer {
|
|||
","
|
||||
};
|
||||
|
||||
let tags = escape_for_json(item.tags);
|
||||
let comment = escape_for_json(item.comment);
|
||||
let snippet = escape_for_json(item.snippet);
|
||||
let tags = escape_for_json(&item.tags);
|
||||
let comment = escape_for_json(&item.comment);
|
||||
let snippet = escape_for_json(&item.snippet);
|
||||
|
||||
format!(
|
||||
r#"{prefix}{{"type":"file","title":"{comment}","match":"{comment} {tags} {snippet}","subtitle":"{tags} :: {snippet}","variables":{{"tags":"{tags}","comment":"{comment}","snippet":"{snippet}"}},"icon":{{"path":"icon.png"}}}}"#,
|
||||
|
|
|
@ -28,5 +28,5 @@ pub fn fix_newlines(txt: &str) -> String {
|
|||
}
|
||||
|
||||
pub trait Writer {
|
||||
fn write(&mut self, item: Item) -> String;
|
||||
fn write(&mut self, item: &Item) -> String;
|
||||
}
|
||||
|
|
|
@ -144,12 +144,12 @@ impl Writer {
|
|||
}
|
||||
|
||||
impl writer::Writer for Writer {
|
||||
fn write(&mut self, item: Item) -> String {
|
||||
fn write(&mut self, item: &Item) -> String {
|
||||
format!(
|
||||
"{tags_short}{delimiter}{comment_short}{delimiter}{snippet_short}{delimiter}{tags}{delimiter}{comment}{delimiter}{snippet}{delimiter}{file_index}{delimiter}\n",
|
||||
tags_short = style(limit_str(item.tags, self.tag_width)).with(*TAG_COLOR),
|
||||
comment_short = style(limit_str(item.comment, self.comment_width)).with(*COMMENT_COLOR),
|
||||
snippet_short = style(writer::fix_newlines(item.snippet)).with(*SNIPPET_COLOR),
|
||||
tags_short = style(limit_str(&item.tags, self.tag_width)).with(*TAG_COLOR),
|
||||
comment_short = style(limit_str(&item.comment, self.comment_width)).with(*COMMENT_COLOR),
|
||||
snippet_short = style(writer::fix_newlines(&item.snippet)).with(*SNIPPET_COLOR),
|
||||
tags = item.tags,
|
||||
comment = item.comment,
|
||||
delimiter = writer::DELIMITER,
|
||||
|
|
Loading…
Reference in a new issue