Allow tag filtering (#508)

This commit is contained in:
Denis Isidoro 2021-04-15 10:49:23 -03:00 committed by GitHub
parent ab7289e3aa
commit 86b0ab67db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 179 additions and 113 deletions

View file

@ -52,6 +52,8 @@ Output:
&mut visited_lines,
writer,
stdin,
None,
None,
)?;
Ok(Some(variables))
}

View file

@ -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")?

View file

@ -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

View file

@ -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() {

View file

@ -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(())

View file

@ -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())
}
}

View file

@ -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,
}
}
}

View file

@ -75,6 +75,8 @@ fn read_all(
&mut visited_lines,
writer,
stdin,
None,
None,
)?;
Ok(Some(variables))
}

View file

@ -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");
}

View file

@ -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"}}}}"#,

View file

@ -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;
}

View file

@ -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,