Add support for config file (#518)

This commit is contained in:
Denis Isidoro 2021-04-17 10:17:22 -03:00 committed by GitHub
parent d8d0d81368
commit 7fb2b53463
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 718 additions and 331 deletions

55
Cargo.lock generated
View file

@ -169,6 +169,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "dtoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
[[package]]
name = "edit"
version = "0.1.3"
@ -251,6 +257,12 @@ version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714"
[[package]]
name = "linked-hash-map"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]]
name = "lock_api"
version = "0.4.3"
@ -318,6 +330,8 @@ dependencies = [
"lazy_static",
"regex",
"remove_dir_all 0.7.0",
"serde",
"serde_yaml",
"shellwords",
"strip-ansi-escapes",
"thiserror",
@ -560,6 +574,38 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_yaml"
version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23"
dependencies = [
"dtoa",
"linked-hash-map",
"serde",
"yaml-rust",
]
[[package]]
name = "shellwords"
version = "1.1.0"
@ -776,3 +822,12 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]

View file

@ -27,6 +27,8 @@ thiserror = "1.0.24"
strip-ansi-escapes = "0.1.0"
edit = "0.1.3"
remove_dir_all = "0.7.0"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.8"
[lib]
name = "navi"

View file

@ -1,4 +1,7 @@
use crate::clipboard;
use crate::config::Action;
use crate::config::CONFIG;
use crate::env_var;
use crate::extractor;
use crate::finder::structures::{Opts as FinderOpts, SuggestionType};
@ -6,8 +9,6 @@ use crate::finder::Finder;
use crate::shell;
use crate::shell::ShellSpawnError;
use crate::structures::cheat::{Suggestion, VariableMap};
use crate::structures::config::Action;
use crate::structures::config::Config;
use crate::writer;
use anyhow::Context;
use anyhow::Result;
@ -17,7 +18,6 @@ use std::process::Stdio;
fn prompt_finder(
variable_name: &str,
config: &Config,
suggestion: Option<&Suggestion>,
variable_count: usize,
) -> Result<String> {
@ -66,7 +66,7 @@ fn prompt_finder(
};
let overrides = {
let mut o = config.fzf_overrides.clone();
let mut o = CONFIG.fzf_overrides_var();
if let Some(io) = initial_opts {
if io.overrides.is_some() {
o = io.overrides.clone()
@ -110,8 +110,8 @@ NAVIEOF
opts.suggestion_type = SuggestionType::Disabled;
};
let (output, _, _) = config
.finder
let (output, _, _) = CONFIG
.finder()
.call(opts, |stdin, _| {
stdin
.write_all(suggestions.as_bytes())
@ -130,12 +130,7 @@ fn unique_result_count(results: &[&str]) -> usize {
vars.len()
}
fn replace_variables_from_snippet(
snippet: &str,
tags: &str,
variables: VariableMap,
config: &Config,
) -> Result<String> {
fn replace_variables_from_snippet(snippet: &str, tags: &str, variables: VariableMap) -> Result<String> {
let mut interpolated_snippet = String::from(snippet);
let variables_found: Vec<&str> = writer::VAR_REGEX.find_iter(snippet).map(|m| m.as_str()).collect();
let variable_count = unique_result_count(&variables_found);
@ -150,11 +145,10 @@ fn replace_variables_from_snippet(
e
} else if let Some(suggestion) = variables.get_suggestion(&tags, &variable_name) {
let mut new_suggestion = suggestion.clone();
new_suggestion.0 =
replace_variables_from_snippet(&new_suggestion.0, tags, variables.clone(), config)?;
prompt_finder(variable_name, &config, Some(&new_suggestion), variable_count)?
new_suggestion.0 = replace_variables_from_snippet(&new_suggestion.0, tags, variables.clone())?;
prompt_finder(variable_name, Some(&new_suggestion), variable_count)?
} else {
prompt_finder(variable_name, &config, None, variable_count)?
prompt_finder(variable_name, None, variable_count)?
};
env_var::set(env_variable_name, &value);
@ -172,7 +166,6 @@ fn replace_variables_from_snippet(
// TODO: make it depend on less inputs
pub fn act(
extractions: Result<extractor::Output>,
config: Config,
files: Vec<String>,
variables: Option<VariableMap>,
) -> Result<()> {
@ -180,7 +173,7 @@ pub fn act(
if key == "ctrl-o" {
edit::edit_file(Path::new(&files[file_index.expect("No files found")]))
.expect("Cound not open file in external editor");
.expect("Could not open file in external editor");
return Ok(());
}
@ -193,12 +186,11 @@ pub fn act(
snippet,
tags,
variables.expect("No variables received from finder"),
&config,
)
.context("Failed to replace variables from snippet")?,
);
match config.action() {
match CONFIG.action() {
Action::Print => {
println!("{}", interpolated_snippet);
}

View file

@ -25,5 +25,5 @@ impl FileAnIssue {
}
fn main() -> Result<(), anyhow::Error> {
navi::handle_config(navi::config_from_env()).map_err(|e| FileAnIssue::new(e).into())
navi::handle().map_err(|e| FileAnIssue::new(e).into())
}

14
src/cheat_variable.rs Normal file
View file

@ -0,0 +1,14 @@
use crate::shell::{self, ShellSpawnError};
use anyhow::Result;
pub 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(())
}

View file

@ -3,25 +3,15 @@ use crate::finder::FinderChoice;
use crate::handler::func::Func;
use crate::handler::info::Info;
use crate::shell::Shell;
use clap::{crate_version, AppSettings, Clap};
use std::str::FromStr;
const FINDER_POSSIBLE_VALUES: &[&str] = &[&"fzf", &"skim"];
const SHELL_POSSIBLE_VALUES: &[&str] = &[&"bash", &"zsh", &"fish"];
const WIDGET_POSSIBLE_VALUES: &[&str] = &[&"bash", &"zsh", &"fish"];
const FUNC_POSSIBLE_VALUES: &[&str] = &[&"url::open", &"welcome", &"widget::last_command", &"map::expand"];
const INFO_POSSIBLE_VALUES: &[&str] = &[&"cheats-path"];
impl FromStr for FinderChoice {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"fzf" => Ok(FinderChoice::Fzf),
"skim" => Ok(FinderChoice::Skim),
_ => Err("no match"),
}
}
}
const INFO_POSSIBLE_VALUES: &[&str] = &[&"cheats-path", "config-path"];
impl FromStr for Shell {
type Err = &'static str;
@ -56,6 +46,7 @@ impl FromStr for Info {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"cheats-path" => Ok(Info::CheatsPath),
"config-path" => Ok(Info::ConfigPath),
_ => Err("no match"),
}
}
@ -94,14 +85,14 @@ EXAMPLES:
#[clap(setting = AppSettings::ColoredHelp)]
#[clap(setting = AppSettings::AllowLeadingHyphen)]
#[clap(version = crate_version!())]
pub struct Config {
/// List of :-separated paths containing .cheat files
pub(super) struct ClapConfig {
/// Colon-separated list of paths containing .cheat files
#[clap(short, long, env = env_var::PATH)]
pub path: Option<String>,
/// Instead of executing a snippet, prints it to stdout
#[clap(long)]
print: bool,
pub print: bool,
/// Returns the best match
#[clap(long)]
@ -109,36 +100,42 @@ pub struct Config {
/// Search for cheatsheets using the tldr-pages repository
#[clap(long)]
tldr: Option<String>,
pub tldr: Option<String>,
/// [Experimental] Comma-separated list that acts as filter for tags. Parts starting with ! represent negation
#[clap(long)]
tag_rules: Option<String>,
pub tag_rules: Option<String>,
/// Search for cheatsheets using the cheat.sh repository
#[clap(long)]
cheatsh: Option<String>,
pub cheatsh: Option<String>,
/// Query
#[clap(short, long)]
query: Option<String>,
pub query: Option<String>,
/// finder overrides for cheat selection
#[clap(long, env = env_var::FZF_OVERRIDES)]
/// Finder overrides for snippet selection
#[clap(long)]
pub fzf_overrides: Option<String>,
/// finder overrides for variable selection
#[clap(long, env = env_var::FZF_OVERRIDES_VAR)]
/// Finder overrides for variable selection
#[clap(long)]
pub fzf_overrides_var: Option<String>,
/// which finder application to use
#[clap(long, env = env_var::FINDER, default_value = "fzf", possible_values = FINDER_POSSIBLE_VALUES, case_insensitive = true)]
pub finder: FinderChoice,
/// Finder application to use
#[clap(long, possible_values = FINDER_POSSIBLE_VALUES, case_insensitive = true)]
pub finder: Option<FinderChoice>,
#[clap(subcommand)]
pub cmd: Option<Command>,
}
impl ClapConfig {
pub fn new() -> Self {
Self::parse()
}
}
#[derive(Debug, Clap)]
pub enum Command {
/// [Experimental] Performs ad-hoc, internal functions provided by navi
@ -172,7 +169,7 @@ pub enum Command {
},
/// Outputs shell widget source code
Widget {
#[clap(possible_values = SHELL_POSSIBLE_VALUES, case_insensitive = true, default_value = "bash")]
#[clap(possible_values = WIDGET_POSSIBLE_VALUES, case_insensitive = true, default_value = "bash")]
shell: Shell,
},
/// Shows info
@ -204,57 +201,13 @@ pub enum Action {
Execute,
}
impl Config {
pub fn source(&self) -> Source {
if let Some(query) = self.tldr.clone() {
Source::Tldr(query)
} else if let Some(query) = self.cheatsh.clone() {
Source::Cheats(query)
} else {
Source::Filesystem(self.path.clone(), self.tag_rules.clone())
}
}
pub fn action(&self) -> Action {
if self.print {
Action::Print
} else {
Action::Execute
}
}
pub fn get_query(&self) -> Option<String> {
let q = self.query.clone();
if q.is_some() {
return q;
}
if self.best_match {
match self.source() {
Source::Tldr(q) => Some(q),
Source::Cheats(q) => Some(q),
_ => Some(String::from("")),
}
} else {
None
}
}
}
pub fn config_from_env() -> Config {
Config::parse()
}
pub fn config_from_iter(args: Vec<&str>) -> Config {
Config::parse_from(args)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shell_possible_values() {
for v in SHELL_POSSIBLE_VALUES {
fn test_widget_possible_values() {
for v in WIDGET_POSSIBLE_VALUES {
assert_eq!(true, Shell::from_str(v).is_ok())
}
}

136
src/config/mod.rs Normal file
View file

@ -0,0 +1,136 @@
mod cli;
mod yaml;
use crate::finder::FinderChoice;
use crate::terminal::style::Color;
pub use cli::*;
use std::process;
use yaml::YamlConfig;
lazy_static! {
pub static ref CONFIG: Config = Config::new();
}
pub struct Config {
yaml: YamlConfig,
clap: ClapConfig,
}
impl Config {
pub fn new() -> Self {
match YamlConfig::get() {
Ok(yaml) => Self {
yaml,
clap: ClapConfig::new(),
},
Err(e) => {
eprintln!("Error parsing config file: {}", e);
process::exit(42)
}
}
}
pub fn best_match(&self) -> bool {
self.clap.best_match
}
pub fn cmd(&self) -> Option<&Command> {
self.clap.cmd.as_ref()
}
pub fn source(&self) -> Source {
if let Some(query) = self.clap.tldr.clone() {
Source::Tldr(query)
} else if let Some(query) = self.clap.cheatsh.clone() {
Source::Cheats(query)
} else {
Source::Filesystem(self.path(), self.tag_rules())
}
}
pub fn path(&self) -> Option<String> {
self.clap.path.clone().or_else(|| self.yaml.cheats.path.clone())
}
pub fn finder(&self) -> FinderChoice {
self.clap.finder.unwrap_or(self.yaml.finder.command)
}
pub fn fzf_overrides(&self) -> Option<String> {
self.clap
.fzf_overrides
.clone()
.or_else(|| self.yaml.finder.overrides.clone())
}
pub fn fzf_overrides_var(&self) -> Option<String> {
self.clap
.fzf_overrides_var
.clone()
.or_else(|| self.yaml.finder.overrides_var.clone())
}
pub fn shell(&self) -> String {
self.yaml.shell.command.clone()
}
pub fn tag_rules(&self) -> Option<String> {
self.clap
.tag_rules
.clone()
.or_else(|| self.yaml.search.tags.clone())
}
pub fn tag_color(&self) -> Color {
self.yaml.style.tag.color.get()
}
pub fn comment_color(&self) -> Color {
self.yaml.style.comment.color.get()
}
pub fn snippet_color(&self) -> Color {
self.yaml.style.snippet.color.get()
}
pub fn tag_width_percentage(&self) -> u16 {
self.yaml.style.tag.width_percentage
}
pub fn comment_width_percentage(&self) -> u16 {
self.yaml.style.comment.width_percentage
}
pub fn tag_min_width(&self) -> u16 {
self.yaml.style.tag.min_width
}
pub fn comment_min_width(&self) -> u16 {
self.yaml.style.comment.min_width
}
pub fn action(&self) -> Action {
if self.clap.print {
Action::Print
} else {
Action::Execute
}
}
pub fn get_query(&self) -> Option<String> {
let q = self.clap.query.clone();
if q.is_some() {
return q;
}
if self.best_match() {
match self.source() {
Source::Tldr(q) => Some(q),
Source::Cheats(q) => Some(q),
_ => Some(String::from("")),
}
} else {
None
}
}
}

176
src/config/yaml.rs Normal file
View file

@ -0,0 +1,176 @@
use crate::env_var;
use crate::filesystem::default_config_pathbuf;
use crate::finder::FinderChoice;
use crate::fs;
use crate::terminal::style;
use anyhow::Result;
use serde::{de, Deserialize};
use std::io::BufReader;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Deserialize)]
pub struct Color(#[serde(deserialize_with = "color_deserialize")] style::Color);
impl Color {
pub fn from_str(color: &str) -> Self {
Self(style::Color::from_str(color).unwrap_or(style::Color::White))
}
pub fn get(&self) -> style::Color {
self.0
}
}
fn color_deserialize<'de, D>(deserializer: D) -> Result<style::Color, D::Error>
where
D: de::Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
style::Color::from_str(&s).map_err(|_| de::Error::custom(format!("Failed to deserialize color: {}", s)))
}
#[derive(Deserialize)]
#[serde(default)]
pub struct ColorWidth {
pub color: Color,
pub width_percentage: u16,
pub min_width: u16,
}
#[derive(Deserialize)]
#[serde(default)]
pub struct Style {
pub tag: ColorWidth,
pub comment: ColorWidth,
pub snippet: ColorWidth,
}
#[derive(Deserialize)]
#[serde(default)]
pub struct Finder {
pub command: FinderChoice,
pub overrides: Option<String>,
pub overrides_var: Option<String>,
}
#[derive(Deserialize)]
#[serde(default)]
pub struct Cheats {
pub path: Option<String>,
}
#[derive(Deserialize)]
#[serde(default)]
pub struct Search {
pub tags: Option<String>,
}
#[derive(Deserialize)]
#[serde(default)]
pub struct Shell {
pub command: String,
}
#[derive(Deserialize, Default)]
#[serde(default)]
pub struct YamlConfig {
pub style: Style,
pub finder: Finder,
pub cheats: Cheats,
pub search: Search,
pub shell: Shell,
}
impl YamlConfig {
fn from_str(text: &str) -> Result<Self> {
serde_yaml::from_str(&text).map_err(|e| e.into())
}
fn from_path(path: &Path) -> Result<Self> {
let file = fs::open(path)?;
let reader = BufReader::new(file);
serde_yaml::from_reader(reader).map_err(|e| e.into())
}
pub fn get() -> Result<Self> {
if let Ok(yaml) = env_var::get(env_var::CONFIG_YAML) {
return Self::from_str(&yaml);
}
if let Ok(path_str) = env_var::get(env_var::CONFIG) {
let p = PathBuf::from(path_str);
return YamlConfig::from_path(&p);
}
if let Ok(p) = default_config_pathbuf() {
if p.exists() {
return YamlConfig::from_path(&p);
}
}
Ok(YamlConfig::default())
}
}
impl Default for ColorWidth {
fn default() -> Self {
Self {
color: Color::from_str("white"),
width_percentage: 26,
min_width: 20,
}
}
}
impl Default for Style {
fn default() -> Self {
Self {
tag: ColorWidth {
color: Color::from_str("cyan"),
width_percentage: 26,
min_width: 20,
},
comment: ColorWidth {
color: Color::from_str("blue"),
width_percentage: 42,
min_width: 45,
},
snippet: Default::default(),
}
}
}
impl Default for Finder {
fn default() -> Self {
Self {
command: env_var::get(env_var::FINDER)
.ok()
.and_then(|x| FinderChoice::from_str(&x).ok())
.unwrap_or(FinderChoice::Fzf),
overrides: env_var::get(env_var::FZF_OVERRIDES).ok(),
overrides_var: env_var::get(env_var::FZF_OVERRIDES_VAR).ok(),
}
}
}
impl Default for Cheats {
fn default() -> Self {
Self {
path: env_var::get(env_var::PATH).ok(),
}
}
}
impl Default for Search {
fn default() -> Self {
Self { tags: None }
}
}
impl Default for Shell {
fn default() -> Self {
Self {
command: env_var::get(env_var::SHELL)
.ok()
.unwrap_or_else(|| "bash".to_string()),
}
}
}

View file

@ -11,13 +11,6 @@ pub const PREVIEW_COLUMN: &str = "NAVI_PREVIEW_COLUMN";
pub const PREVIEW_DELIMITER: &str = "NAVI_PREVIEW_DELIMITER";
pub const PREVIEW_MAP: &str = "NAVI_PREVIEW_MAP";
pub const TAG_COLOR: &str = "NAVI_TAG_COLOR";
pub const COMMENT_COLOR: &str = "NAVI_COMMENT_COLOR";
pub const SNIPPET_COLOR: &str = "NAVI_SNIPPET_COLOR";
pub const TAG_WIDTH: &str = "NAVI_TAG_WIDTH";
pub const COMMENT_WIDTH: &str = "NAVI_COMMENT_WIDTH";
pub const PATH: &str = "NAVI_PATH";
pub const FZF_OVERRIDES: &str = "NAVI_FZF_OVERRIDES";
pub const FZF_OVERRIDES_VAR: &str = "NAVI_FZF_OVERRIDES_VAR";
@ -25,6 +18,9 @@ pub const FINDER: &str = "NAVI_FINDER";
pub const SHELL: &str = "NAVI_SHELL";
pub const CONFIG: &str = "NAVI_CONFIG";
pub const CONFIG_YAML: &str = "NAVI_CONFIG_YAML";
pub fn parse<T: FromStr>(varname: &str) -> Option<T> {
if let Ok(x) = env::var(varname) {
x.parse::<T>().ok()

View file

@ -33,6 +33,15 @@ pub fn default_cheat_pathbuf() -> Result<PathBuf> {
Ok(pathbuf)
}
pub fn default_config_pathbuf() -> Result<PathBuf> {
let base_dirs = BaseDirs::new().ok_or_else(|| anyhow!("Unable to get base dirs"))?;
let mut pathbuf = PathBuf::from(base_dirs.config_dir());
pathbuf.push("navi");
pathbuf.push("config.yaml");
Ok(pathbuf)
}
pub fn cheat_paths(path: Option<String>) -> Result<String> {
if let Some(p) = path {
Ok(p)
@ -124,7 +133,8 @@ impl fetcher::Fetcher for Fetcher {
files.push(file.clone());
let index = files.len() - 1;
let read_file_result = {
let lines = read_lines(&file)?;
let path = PathBuf::from(&file);
let lines = read_lines(&path)?;
parser::read_lines(
lines,
&file,

View file

@ -1,24 +1,36 @@
use crate::shell;
use crate::config::CONFIG;
use crate::structures::cheat::VariableMap;
use crate::writer;
use anyhow::Context;
use anyhow::Result;
use std::process::{self, Output};
use std::process::{Command, Stdio};
mod post;
pub mod structures;
pub use post::process;
use serde::Deserialize;
use std::str::FromStr;
use structures::Opts;
use structures::SuggestionType;
#[derive(Debug)]
#[derive(Debug, Clone, Copy, Deserialize)]
pub enum FinderChoice {
Fzf,
Skim,
}
impl FromStr for FinderChoice {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"fzf" => Ok(FinderChoice::Fzf),
"skim" => Ok(FinderChoice::Skim),
_ => Err("no match"),
}
}
}
pub trait Finder {
fn call<F>(&self, opts: Opts, stdin_fn: F) -> Result<(String, Option<VariableMap>, Vec<String>)>
where
@ -142,7 +154,7 @@ impl Finder for FinderChoice {
}
let child = command
.env("SHELL", &*shell::SHELL)
.env("SHELL", CONFIG.shell())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn();

View file

@ -1,3 +1,7 @@
use crate::config::Config;
use crate::filesystem;
use anyhow::Result;
#[derive(Debug, PartialEq, Clone)]
pub struct Opts {
pub query: Option<String>,
@ -46,3 +50,26 @@ pub enum SuggestionType {
/// initial snippet selection
SnippetSelection,
}
impl Opts {
pub fn from_config(config: &Config) -> Result<Opts> {
let opts = Opts {
preview: Some(format!("{} preview {{}}", filesystem::exe_string()?)),
overrides: config.fzf_overrides(),
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)
}
}

View file

@ -1,5 +1,5 @@
use anyhow::{Context, Error, Result};
use core::fmt::Display;
use remove_dir_all::remove_dir_all;
use std::fmt::Debug;
use std::fs::{self, create_dir_all, File};
@ -19,11 +19,15 @@ pub struct UnreadableDir {
source: anyhow::Error,
}
pub fn read_lines<P>(filename: P) -> Result<impl Iterator<Item = Result<String>>>
where
P: AsRef<Path> + Display + Copy,
{
let file = File::open(filename).with_context(|| format!("Failed to open file {}", filename))?;
pub fn open(filename: &Path) -> Result<File> {
File::open(filename).with_context(|| {
let x = pathbuf_to_string(filename).unwrap_or_else(|e| format!("Unable to get path string: {}", e));
format!("Failed to open file {}", &x)
})
}
pub fn read_lines(filename: &Path) -> Result<impl Iterator<Item = Result<String>>> {
let file = open(filename)?;
Ok(io::BufReader::new(file)
.lines()
.map(|line| line.map_err(Error::from)))

View file

@ -1,44 +1,24 @@
use crate::actor;
use crate::cheatsh;
use crate::config::Source;
use crate::config::CONFIG;
use crate::extractor;
use crate::filesystem;
use crate::finder::structures::{Opts as FinderOpts, SuggestionType};
use crate::finder::structures::Opts as FinderOpts;
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")?;
pub fn main() -> Result<()> {
let config = &CONFIG;
let opts = FinderOpts::from_config(&config)?;
let (raw_selection, variables, files) = config
.finder
.finder()
.call(opts, |stdin, files| {
let fetcher: Box<dyn Fetcher> = match config.source() {
Source::Cheats(query) => Box::new(cheatsh::Fetcher::new(query)),
@ -59,13 +39,13 @@ pub fn main(config: Config) -> Result<()> {
})
.context("Failed getting selection and variables from finder")?;
let extractions = extractor::extract_from_selections(&raw_selection, config.best_match);
let extractions = extractor::extract_from_selections(&raw_selection, config.best_match());
if extractions.is_err() {
return main(config);
return main();
}
actor::act(extractions, config, files, variables)?;
actor::act(extractions, files, variables)?;
Ok(())
}

View file

@ -1,9 +1,11 @@
use crate::handler;
use crate::shell::{self, ShellSpawnError};
use crate::structures::config;
use crate::cheat_variable;
use crate::shell::{self};
use crate::url;
use crate::welcome;
use anyhow::Result;
use std::io::{self, Read};
#[derive(Debug)]
pub enum Func {
@ -16,58 +18,8 @@ pub enum Func {
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(),
Func::Welcome => welcome::main(),
Func::WidgetLastCommand => shell::widget_last_command(),
Func::MapExpand => cheat_variable::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(())
}

View file

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

View file

@ -3,27 +3,28 @@ pub mod func;
pub mod info;
pub mod preview;
pub mod preview_var;
pub mod repo;
pub mod repo_add;
pub mod repo_browse;
pub mod shell;
use crate::config::Command::{Fn, Info, Preview, PreviewVar, Repo, Widget};
use crate::config::{RepoCommand, CONFIG};
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),
pub fn handle() -> Result<()> {
match CONFIG.cmd() {
None => handler::core::main(),
Some(c) => match c {
Preview { line } => handler::preview::main(&line),
Preview { line } => handler::preview::main(line),
PreviewVar {
selection,
query,
variable,
} => handler::preview_var::main(&selection, &query, &variable),
} => handler::preview_var::main(selection, query, variable),
Widget { shell } => handler::shell::main(shell).context("Failed to print shell widget code"),
@ -36,13 +37,16 @@ pub fn handle_config(config: Config) -> Result<()> {
Repo { cmd } => match cmd {
RepoCommand::Add { uri } => {
handler::repo::add(uri.clone(), &config.finder)
handler::repo_add::main(uri.clone())
.with_context(|| format!("Failed to import cheatsheets from `{}`", uri))?;
handler::core::main(config)
handler::core::main()
}
RepoCommand::Browse => {
handler::repo::browse(&config.finder).context("Failed to browse featured cheatsheets")?;
handler::core::main(config)
let repo =
handler::repo_browse::main().context("Failed to browse featured cheatsheets")?;
handler::repo_add::main(repo.clone())
.with_context(|| format!("Failed to import cheatsheets from `{}`", repo))?;
handler::core::main()
}
},
},

View file

@ -1,3 +1,4 @@
use crate::config::CONFIG;
use crate::ui;
use crate::writer;
use anyhow::Result;
@ -12,13 +13,15 @@ fn extract_elements(argstr: &str) -> (&str, &str, &str) {
}
pub fn main(line: &str) -> Result<()> {
// dbg!(CONFIG.comment_color());
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),
comment = ui::style(comment).with(CONFIG.comment_color()),
tags = ui::style(format!("[{}]", tags)).with(CONFIG.tag_color()),
snippet = ui::style(writer::fix_newlines(snippet)).with(CONFIG.snippet_color()),
);
process::exit(0)

View file

@ -1,11 +1,9 @@
use crate::config::CONFIG;
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;
@ -18,8 +16,8 @@ pub fn main(selection: &str, query: &str, variable: &str) -> Result<()> {
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 active_color = CONFIG.tag_color();
let inactive_color = CONFIG.comment_color();
let mut colored_snippet = String::from(&snippet);
let mut visited_vars: HashSet<&str> = HashSet::new();
@ -28,8 +26,8 @@ pub fn main(selection: &str, query: &str, variable: &str) -> Result<()> {
println!(
"{comment} {tags}",
comment = style(comment).with(*ui::COMMENT_COLOR),
tags = style(format!("[{}]", tags)).with(*ui::TAG_COLOR),
comment = style(comment).with(CONFIG.comment_color()),
tags = style(format!("[{}]", tags)).with(CONFIG.tag_color()),
);
let bracketed_current_variable = format!("<{}>", variable);

View file

@ -1,3 +1,4 @@
use crate::config::CONFIG;
use crate::filesystem;
use crate::finder::structures::{Opts as FinderOpts, SuggestionType};
use crate::finder::{Finder, FinderChoice};
@ -9,51 +10,7 @@ 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> {
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()),
@ -76,8 +33,10 @@ pub fn ask_if_should_import_all(finder: &FinderChoice) -> Result<bool> {
}
}
pub fn add(uri: String, finder: &FinderChoice) -> Result<()> {
let should_import_all = ask_if_should_import_all(finder).unwrap_or(false);
pub fn main(uri: String) -> Result<()> {
let finder = CONFIG.finder();
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()?;

View file

@ -0,0 +1,56 @@
use crate::config::CONFIG;
use crate::filesystem;
use crate::finder::structures::{Opts as FinderOpts, SuggestionType};
use crate::finder::Finder;
use crate::fs::pathbuf_to_string;
use crate::git;
use anyhow::Context;
use anyhow::Result;
use std::fs;
use std::io::Write;
pub fn main() -> Result<String> {
let finder = CONFIG.finder();
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)?;
Ok(repo)
}

View file

@ -4,8 +4,10 @@ extern crate lazy_static;
extern crate anyhow;
mod actor;
mod cheat_variable;
mod cheatsh;
mod clipboard;
mod config;
mod env_var;
mod extractor;
mod filesystem;
@ -24,5 +26,4 @@ mod url;
mod welcome;
mod writer;
pub use handler::handle_config;
pub use structures::config::{config_from_env, config_from_iter};
pub use handler::handle;

View file

@ -1,15 +1,10 @@
use crate::env_var;
use crate::config::CONFIG;
use anyhow::Result;
use std::fmt::Debug;
use std::io::{self, Read};
use std::process::Command;
use thiserror::Error;
lazy_static! {
pub static ref IS_FISH: bool = env_var::get("SHELL")
.unwrap_or_else(|_| "".to_string())
.contains(&"fish");
pub static ref SHELL: String = env_var::get(env_var::SHELL).unwrap_or_else(|_| "bash".to_string());
}
#[derive(Debug)]
pub enum Shell {
Bash,
@ -38,5 +33,42 @@ impl ShellSpawnError {
}
pub fn command() -> Command {
Command::new(&*SHELL)
Command::new(CONFIG.shell())
}
pub 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(())
}

View file

@ -1,4 +1,3 @@
pub mod cheat;
pub mod config;
pub mod fetcher;
pub mod item;

View file

@ -1,9 +1,11 @@
use anyhow::Result;
pub use crossterm::style;
use crossterm::terminal;
use std::str::FromStr;
const FALLBACK_WIDTH: u16 = 80;
fn width_with_shell_out() -> u16 {
fn width_with_shell_out() -> Result<u16> {
use std::process::Command;
use std::process::Stdio;
@ -13,40 +15,53 @@ fn width_with_shell_out() -> u16 {
.arg("/dev/stderr")
.arg("size")
.stderr(Stdio::inherit())
.output()
.expect("Failed to execute stty")
.output()?
} else {
Command::new("stty")
.arg("size")
.arg("-F")
.arg("/dev/stderr")
.stderr(Stdio::inherit())
.output()
.expect("Failed to execute stty")
.output()?
};
match output.status.code() {
Some(0) => {
let stdout = String::from_utf8(output.stdout).expect("Invalid utf8 output from stty");
let mut data = stdout.split_whitespace();
data.next();
data.next()
.expect("Not enough data")
.parse::<u16>()
.expect("Invalid base-10 number")
}
_ => FALLBACK_WIDTH,
if let Some(0) = output.status.code() {
let stdout = String::from_utf8(output.stdout).expect("Invalid utf8 output from stty");
let mut data = stdout.split_whitespace();
data.next();
return data
.next()
.expect("Not enough data")
.parse::<u16>()
.map_err(|_| anyhow!("Invalid width"));
}
Err(anyhow!("Invalid status code"))
}
pub fn width() -> u16 {
if let Ok((w, _)) = terminal::size() {
w
} else {
width_with_shell_out()
width_with_shell_out().unwrap_or(FALLBACK_WIDTH)
}
}
pub fn parse_ansi(ansi: &str) -> Option<style::Color> {
style::Color::parse_ansi(&format!("5;{}", ansi))
}
#[derive(Debug, Clone)]
pub struct Color(pub style::Color);
impl FromStr for Color {
type Err = &'static str;
fn from_str(ansi: &str) -> Result<Self, Self::Err> {
if let Some(c) = parse_ansi(ansi) {
Ok(Color(c))
} else {
Err("Invalid color")
}
}
}

View file

@ -1,32 +1,20 @@
use crate::env_var;
use crate::config::CONFIG;
use crate::terminal;
pub use crate::terminal::style::style;
use crate::terminal::style::Color;
use std::cmp::max;
fn parse_ansi(varname: &str, default: Color) -> Color {
let value: Option<String> = env_var::parse(varname);
if let Some(v) = value {
if let Some(a) = terminal::parse_ansi(&v) {
return a;
}
}
default
}
lazy_static! {
pub static ref TAG_COLOR: Color = parse_ansi(env_var::TAG_COLOR, Color::Cyan);
pub static ref COMMENT_COLOR: Color = parse_ansi(env_var::COMMENT_COLOR, Color::Blue);
pub static ref SNIPPET_COLOR: Color = parse_ansi(env_var::SNIPPET_COLOR, Color::White);
pub static ref TAG_WIDTH_PERCENTAGE: u16 = env_var::parse(env_var::TAG_WIDTH).unwrap_or(26);
pub static ref COMMENT_WIDTH_PERCENTAGE: u16 = env_var::parse(env_var::COMMENT_WIDTH).unwrap_or(42);
}
pub fn get_widths() -> (usize, usize) {
let width = terminal::width();
let tag_width = max(20, width * *TAG_WIDTH_PERCENTAGE / 100);
let comment_width = max(45, width * *COMMENT_WIDTH_PERCENTAGE / 100);
(usize::from(tag_width), usize::from(comment_width))
let tag_width_percentage = max(
CONFIG.tag_min_width(),
width * CONFIG.tag_width_percentage() / 100,
);
let comment_width_percentage = max(
CONFIG.comment_min_width(),
width * CONFIG.comment_width_percentage() / 100,
);
(
usize::from(tag_width_percentage),
usize::from(comment_width_percentage),
)
}

View file

@ -1,7 +1,27 @@
use crate::config::CONFIG;
use crate::finder::structures::Opts as FinderOpts;
use crate::finder::Finder;
use crate::structures::cheat::VariableMap;
use crate::structures::item::Item;
use crate::writer;
use anyhow::Context;
use anyhow::Result;
use std::io::Write;
pub fn main() -> Result<()> {
let config = &CONFIG;
let opts = FinderOpts::from_config(&config)?;
let _ = config
.finder()
.call(opts, |stdin, _| {
populate_cheatsheet(stdin);
Ok(Some(VariableMap::new()))
})
.context("Failed getting selection and variables from finder")?;
Ok(())
}
fn add_msg(tags: &str, comment: &str, snippet: &str, stdin: &mut std::process::ChildStdin) {
let item = Item {
tags: tags.to_string(),

View file

@ -1,3 +1,4 @@
use crate::config::CONFIG;
use crate::structures::item::Item;
use crate::ui;
use regex::Regex;
@ -35,12 +36,12 @@ fn limit_str(text: &str, length: usize) -> String {
}
pub fn write(item: &Item) -> String {
let (tag_width, comment_width) = *COLUMN_WIDTHS;
let (tag_width_percentage, comment_width_percentage) = *COLUMN_WIDTHS;
format!(
"{tags_short}{delimiter}{comment_short}{delimiter}{snippet_short}{delimiter}{tags}{delimiter}{comment}{delimiter}{snippet}{delimiter}{file_index}{delimiter}\n",
tags_short = ui::style(limit_str(&item.tags, tag_width)).with(*ui::TAG_COLOR),
comment_short = ui::style(limit_str(&item.comment, comment_width)).with(*ui::COMMENT_COLOR),
snippet_short = ui::style(fix_newlines(&item.snippet)).with(*ui::SNIPPET_COLOR),
tags_short = ui::style(limit_str(&item.tags, tag_width_percentage)).with(CONFIG.tag_color()),
comment_short = ui::style(limit_str(&item.comment, comment_width_percentage)).with(CONFIG.comment_color()),
snippet_short = ui::style(fix_newlines(&item.snippet)).with(CONFIG.snippet_color()),
tags = item.tags,
comment = item.comment,
delimiter = DELIMITER,