mirror of
https://github.com/denisidoro/navi.git
synced 2026-08-01 14:33:52 +00:00
Refactor writer packages (#517)
This commit is contained in:
parent
599b0c95bd
commit
d8d0d81368
22 changed files with 252 additions and 300 deletions
71
src/handler/core.rs
Normal file
71
src/handler/core.rs
Normal 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
73
src/handler/func.rs
Normal 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
15
src/handler/info.rs
Normal 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
50
src/handler/mod.rs
Normal 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
25
src/handler/preview.rs
Normal 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)
|
||||
}
|
||||
92
src/handler/preview_var.rs
Normal file
92
src/handler/preview_var.rs
Normal 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
158
src/handler/repo.rs
Normal 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
14
src/handler/shell.rs
Normal 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(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue