Refactor writer packages (#517)

This commit is contained in:
Denis Isidoro 2021-04-16 09:23:53 -03:00 committed by GitHub
parent 599b0c95bd
commit d8d0d81368
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 252 additions and 300 deletions

71
src/handler/core.rs Normal file
View file

@ -0,0 +1,71 @@
use crate::actor;
use crate::cheatsh;
use crate::extractor;
use crate::filesystem;
use crate::finder::structures::{Opts as FinderOpts, SuggestionType};
use crate::finder::Finder;
use crate::structures::cheat::VariableMap;
use crate::structures::config::Config;
use crate::structures::config::Source;
use crate::structures::fetcher::Fetcher;
use crate::tldr;
use crate::welcome;
use anyhow::Context;
use anyhow::Result;
fn gen_core_finder_opts(config: &Config) -> Result<FinderOpts> {
let opts = FinderOpts {
preview: Some(format!("{} preview {{}}", filesystem::exe_string()?)),
overrides: config.fzf_overrides.clone(),
suggestion_type: SuggestionType::SnippetSelection,
query: if config.best_match {
None
} else {
config.get_query()
},
filter: if config.best_match {
config.get_query()
} else {
None
},
..Default::default()
};
Ok(opts)
}
pub fn main(config: Config) -> Result<()> {
let opts = gen_core_finder_opts(&config).context("Failed to generate finder options")?;
let (raw_selection, variables, files) = config
.finder
.call(opts, |stdin, files| {
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, rules) => Box::new(filesystem::Fetcher::new(path, rules)),
};
let res = fetcher
.fetch(stdin, files)
.context("Failed to parse variables intended for finder")?;
if let Some(variables) = res {
Ok(Some(variables))
} else {
welcome::populate_cheatsheet(stdin);
Ok(Some(VariableMap::new()))
}
})
.context("Failed getting selection and variables from finder")?;
let extractions = extractor::extract_from_selections(&raw_selection, config.best_match);
if extractions.is_err() {
return main(config);
}
actor::act(extractions, config, files, variables)?;
Ok(())
}

73
src/handler/func.rs Normal file
View file

@ -0,0 +1,73 @@
use crate::handler;
use crate::shell::{self, ShellSpawnError};
use crate::structures::config;
use crate::url;
use anyhow::Result;
use std::io::{self, Read};
#[derive(Debug)]
pub enum Func {
UrlOpen,
Welcome,
WidgetLastCommand,
MapExpand,
}
pub fn main(func: &Func, args: Vec<String>) -> Result<()> {
match func {
Func::UrlOpen => url::open(args),
Func::Welcome => handler::handle_config(config::config_from_iter(
"navi --path /tmp/navi/irrelevant".split(' ').collect(),
)),
Func::WidgetLastCommand => widget_last_command(),
Func::MapExpand => map_expand(),
}
}
fn map_expand() -> Result<()> {
let cmd = r#"sed -e 's/^.*$/"&"/' | tr '\n' ' '"#;
shell::command()
.arg("-c")
.arg(cmd)
.spawn()
.map_err(|e| ShellSpawnError::new(cmd, e))?
.wait()?;
Ok(())
}
fn widget_last_command() -> Result<()> {
let mut text = String::new();
io::stdin().read_to_string(&mut text)?;
let replacements = vec![("|", ""), ("||", ""), ("&&", "")];
let parts = shellwords::split(&text).unwrap_or_else(|_| text.split('|').map(|s| s.to_string()).collect());
for p in parts {
for (pattern, escaped) in replacements.clone() {
if p.contains(pattern) && p != pattern {
let replacement = p.replace(pattern, escaped);
text = text.replace(&p, &replacement);
}
}
}
let mut extracted = text.clone();
for (pattern, _) in replacements.clone() {
let mut new_parts = text.rsplit(pattern);
if let Some(extracted_attempt) = new_parts.next() {
if extracted_attempt.len() <= extracted.len() {
extracted = extracted_attempt.to_string();
}
}
}
for (pattern, escaped) in replacements.clone() {
text = text.replace(&escaped, &pattern);
extracted = extracted.replace(&escaped, &pattern);
}
println!("{}", extracted.trim_start());
Ok(())
}

15
src/handler/info.rs Normal file
View file

@ -0,0 +1,15 @@
use crate::filesystem::default_cheat_pathbuf;
use crate::fs::pathbuf_to_string;
use anyhow::Result;
#[derive(Debug)]
pub enum Info {
CheatsPath,
}
pub fn main(info: &Info) -> Result<()> {
match info {
Info::CheatsPath => println!("{}", pathbuf_to_string(&default_cheat_pathbuf()?)?),
}
Ok(())
}

50
src/handler/mod.rs Normal file
View file

@ -0,0 +1,50 @@
pub mod core;
pub mod func;
pub mod info;
pub mod preview;
pub mod preview_var;
pub mod repo;
pub mod shell;
use crate::handler;
use crate::structures::config::Command::{Fn, Info, Preview, PreviewVar, Repo, Widget};
use crate::structures::config::{Config, RepoCommand};
use anyhow::Context;
use anyhow::Result;
pub fn handle_config(config: Config) -> Result<()> {
match config.cmd.as_ref() {
None => handler::core::main(config),
Some(c) => match c {
Preview { line } => handler::preview::main(&line),
PreviewVar {
selection,
query,
variable,
} => handler::preview_var::main(&selection, &query, &variable),
Widget { shell } => handler::shell::main(shell).context("Failed to print shell widget code"),
Fn { func, args } => handler::func::main(func, args.to_vec())
.with_context(|| format!("Failed to execute function `{:#?}`", func)),
Info { info } => {
handler::info::main(info).with_context(|| format!("Failed to fetch info `{:#?}`", info))
}
Repo { cmd } => match cmd {
RepoCommand::Add { uri } => {
handler::repo::add(uri.clone(), &config.finder)
.with_context(|| format!("Failed to import cheatsheets from `{}`", uri))?;
handler::core::main(config)
}
RepoCommand::Browse => {
handler::repo::browse(&config.finder).context("Failed to browse featured cheatsheets")?;
handler::core::main(config)
}
},
},
}
}

25
src/handler/preview.rs Normal file
View file

@ -0,0 +1,25 @@
use crate::ui;
use crate::writer;
use anyhow::Result;
use std::process;
fn extract_elements(argstr: &str) -> (&str, &str, &str) {
let mut parts = argstr.split(writer::DELIMITER).skip(3);
let tags = parts.next().expect("No `tags` element provided.");
let comment = parts.next().expect("No `comment` element provided.");
let snippet = parts.next().expect("No `snippet` element provided.");
(tags, comment, snippet)
}
pub fn main(line: &str) -> Result<()> {
let (tags, comment, snippet) = extract_elements(line);
println!(
"{comment} {tags} \n{snippet}",
comment = ui::style(comment).with(*ui::COMMENT_COLOR),
tags = ui::style(format!("[{}]", tags)).with(*ui::TAG_COLOR),
snippet = ui::style(writer::fix_newlines(snippet)).with(*ui::SNIPPET_COLOR),
);
process::exit(0)
}

View file

@ -0,0 +1,92 @@
use crate::env_var;
use crate::finder;
use crate::terminal::style::style;
use crate::ui;
use crate::writer;
use anyhow::Result;
use std::collections::HashSet;
use std::iter;
use std::process;
pub fn main(selection: &str, query: &str, variable: &str) -> Result<()> {
let snippet = env_var::must_get(env_var::PREVIEW_INITIAL_SNIPPET);
let tags = env_var::must_get(env_var::PREVIEW_TAGS);
let comment = env_var::must_get(env_var::PREVIEW_COMMENT);
let column = env_var::parse(env_var::PREVIEW_COLUMN);
let delimiter = env_var::get(env_var::PREVIEW_DELIMITER).ok();
let map = env_var::get(env_var::PREVIEW_MAP).ok();
let active_color = *ui::TAG_COLOR;
let inactive_color = *ui::COMMENT_COLOR;
let mut colored_snippet = String::from(&snippet);
let mut visited_vars: HashSet<&str> = HashSet::new();
let mut variables = String::from("");
println!(
"{comment} {tags}",
comment = style(comment).with(*ui::COMMENT_COLOR),
tags = style(format!("[{}]", tags)).with(*ui::TAG_COLOR),
);
let bracketed_current_variable = format!("<{}>", variable);
let bracketed_variables: Vec<&str> = {
if snippet.contains(&bracketed_current_variable) {
writer::VAR_REGEX
.find_iter(&snippet)
.map(|m| m.as_str())
.collect()
} else {
iter::once(&bracketed_current_variable)
.map(|s| s.as_str())
.collect()
}
};
for bracketed_variable_name in bracketed_variables {
let variable_name = &bracketed_variable_name[1..bracketed_variable_name.len() - 1];
if visited_vars.contains(variable_name) {
continue;
} else {
visited_vars.insert(variable_name);
}
let is_current = variable_name == variable;
let variable_color = if is_current { active_color } else { inactive_color };
let env_variable_name = env_var::escape(variable_name);
let value = if is_current {
let v = selection.trim_matches('\'');
if v.is_empty() { query.trim_matches('\'') } else { v }.to_string()
} else if let Ok(v) = env_var::get(&env_variable_name) {
v
} else {
"".to_string()
};
let replacement = format!(
"{variable}",
variable = style(bracketed_variable_name).with(variable_color),
);
colored_snippet = colored_snippet.replace(bracketed_variable_name, &replacement);
variables = format!(
"{variables}\n{variable} = {value}",
variables = variables,
variable = style(variable_name).with(variable_color),
value = finder::process(value, column, delimiter.as_deref(), map.clone())
.expect("Unable to process value"),
);
}
println!("{snippet}", snippet = writer::fix_newlines(&colored_snippet));
println!("{variables}", variables = variables);
process::exit(0)
}

158
src/handler/repo.rs Normal file
View file

@ -0,0 +1,158 @@
use crate::filesystem;
use crate::finder::structures::{Opts as FinderOpts, SuggestionType};
use crate::finder::{Finder, FinderChoice};
use crate::fs::pathbuf_to_string;
use crate::git;
use anyhow::Context;
use anyhow::Result;
use std::fs;
use std::io::Write;
use std::path;
pub fn browse(finder: &FinderChoice) -> Result<()> {
let repo_pathbuf = {
let mut p = filesystem::tmp_pathbuf()?;
p.push("featured");
p
};
let repo_path_str = pathbuf_to_string(&repo_pathbuf)?;
let _ = filesystem::remove_dir(&repo_pathbuf);
filesystem::create_dir(&repo_pathbuf)?;
let (repo_url, _, _) = git::meta("denisidoro/cheats");
git::shallow_clone(repo_url.as_str(), &repo_path_str)
.with_context(|| format!("Failed to clone `{}`", repo_url))?;
let feature_repos_file = {
let mut p = repo_pathbuf.clone();
p.push("featured_repos.txt");
p
};
let repos = fs::read_to_string(&feature_repos_file).context("Unable to fetch featured repositories")?;
let opts = FinderOpts {
column: Some(1),
suggestion_type: SuggestionType::SingleSelection,
..Default::default()
};
let (repo, _, _) = finder
.call(opts, |stdin, _| {
stdin
.write_all(repos.as_bytes())
.context("Unable to prompt featured repositories")?;
Ok(None)
})
.context("Failed to get repo URL from finder")?;
filesystem::remove_dir(&repo_pathbuf)?;
add(repo, finder)
}
pub fn ask_if_should_import_all(finder: &FinderChoice) -> Result<bool> {
let opts = FinderOpts {
column: Some(1),
header: Some("Do you want to import all files from this repo?".to_string()),
..Default::default()
};
let (response, _, _) = finder
.call(opts, |stdin, _| {
stdin
.write_all(b"Yes\nNo")
.context("Unable to writer alternatives")?;
Ok(None)
})
.context("Unable to get response")?;
if response.to_lowercase().starts_with('y') {
Ok(true)
} else {
Ok(false)
}
}
pub fn add(uri: String, finder: &FinderChoice) -> Result<()> {
let should_import_all = ask_if_should_import_all(finder).unwrap_or(false);
let (actual_uri, user, repo) = git::meta(uri.as_str());
let cheat_pathbuf = filesystem::default_cheat_pathbuf()?;
let tmp_pathbuf = filesystem::tmp_pathbuf()?;
let tmp_path_str = pathbuf_to_string(&tmp_pathbuf)?;
let _ = filesystem::remove_dir(&tmp_pathbuf);
filesystem::create_dir(&tmp_pathbuf)?;
eprintln!("Cloning {} into {}...\n", &actual_uri, &tmp_path_str);
git::shallow_clone(actual_uri.as_str(), &tmp_path_str)
.with_context(|| format!("Failed to clone `{}`", actual_uri))?;
let all_files = filesystem::all_cheat_files(&tmp_pathbuf).join("\n");
let opts = FinderOpts {
suggestion_type: SuggestionType::MultipleSelections,
preview: Some(format!("cat '{}/{{}}'", tmp_path_str)),
header: Some("Select the cheatsheets you want to import with <TAB> then hit <Enter>\nUse Ctrl-R for (de)selecting all".to_string()),
preview_window: Some("right:30%".to_string()),
..Default::default()
};
let files = if should_import_all {
all_files
} else {
let (files, _, _) = finder
.call(opts, |stdin, _| {
stdin
.write_all(all_files.as_bytes())
.context("Unable to prompt cheats to import")?;
Ok(None)
})
.context("Failed to get cheatsheet files from finder")?;
files
};
let to_folder = {
let mut p = cheat_pathbuf;
p.push(format!("{}__{}", user, repo));
p
};
for file in files.split('\n') {
let from = {
let mut p = tmp_pathbuf.clone();
p.push(file);
p
};
let filename = file
.replace(&format!("{}{}", &tmp_path_str, path::MAIN_SEPARATOR), "")
.replace(path::MAIN_SEPARATOR, "__");
let to = {
let mut p = to_folder.clone();
p.push(filename);
p
};
fs::create_dir_all(&to_folder).unwrap_or(());
fs::copy(&from, &to).with_context(|| {
format!(
"Failed to copy `{}` to `{}`",
pathbuf_to_string(&from).expect("unable to parse {from}"),
pathbuf_to_string(&to).expect("unable to parse {to}")
)
})?;
}
filesystem::remove_dir(&tmp_pathbuf)?;
eprintln!(
"The following .cheat files were imported successfully:\n{}\n\nThey are now located at {}",
files,
pathbuf_to_string(&to_folder)?
);
Ok(())
}

14
src/handler/shell.rs Normal file
View file

@ -0,0 +1,14 @@
use crate::shell::Shell;
use anyhow::Result;
pub fn main(shell: &Shell) -> Result<()> {
let content = match shell {
Shell::Bash => include_str!("../../shell/navi.plugin.bash"),
Shell::Zsh => include_str!("../../shell/navi.plugin.zsh"),
Shell::Fish => include_str!("../../shell/navi.plugin.fish"),
};
println!("{}", content);
Ok(())
}