Add support for Alfred (#347)

This commit is contained in:
Denis Isidoro 2020-04-19 15:51:04 -03:00 committed by GitHub
parent 019adf82cf
commit c7f919eaa0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 850 additions and 54 deletions

View file

@ -1,3 +1,4 @@
use crate::structures::item::Item;
use crate::terminal;
use regex::Regex;
use std::cmp::max;
@ -12,11 +13,10 @@ pub const LINE_SEPARATOR: &str = " \x15 ";
pub const DELIMITER: &str = r" ";
lazy_static! {
pub static ref WIDTHS: (usize, usize) = get_widths();
pub static ref NEWLINE_REGEX: Regex = Regex::new(r"\\\s+").expect("Invalid regex");
}
fn get_widths() -> (usize, usize) {
pub fn get_widths() -> (usize, usize) {
let width = terminal::width();
let tag_width = max(4, width * 20 / 100);
let comment_width = max(4, width * 40 / 100);
@ -57,23 +57,61 @@ fn limit_str(text: &str, length: usize) -> String {
}
}
pub fn format_line(
tags: &str,
comment: &str,
snippet: &str,
tag_width: usize,
comment_width: usize,
) -> String {
format!(
pub trait Writer {
fn write(&mut self, item: Item) -> String;
}
pub struct FinderWriter {
pub tag_width: usize,
pub comment_width: usize,
}
pub struct AlfredWriter {
pub is_first: bool,
}
impl Writer for FinderWriter {
fn write(&mut self, item: Item) -> String {
format!(
"{tag_color}{tags_short}{delimiter}{comment_color}{comment_short}{delimiter}{snippet_color}{snippet_short}{delimiter}{tags}{delimiter}{comment}{delimiter}{snippet}{delimiter}\n",
tags_short = limit_str(tags, tag_width),
comment_short = limit_str(comment, comment_width),
snippet_short = fix_newlines(snippet),
tags_short = limit_str(item.tags, self.tag_width),
comment_short = limit_str(item.comment, self.comment_width),
snippet_short = fix_newlines(item.snippet),
comment_color = color::Fg(COMMENT_COLOR),
tag_color = color::Fg(TAG_COLOR),
snippet_color = color::Fg(SNIPPET_COLOR),
tags = tags,
comment = comment,
tags = item.tags,
comment = item.comment,
delimiter = DELIMITER,
snippet = &snippet)
snippet = &item.snippet)
}
}
fn escape_for_json(txt: &str) -> String {
txt.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace(NEWLINE_ESCAPE_CHAR, " ")
}
impl Writer for AlfredWriter {
fn write(&mut self, item: Item) -> String {
let prefix = if self.is_first {
self.is_first = false;
""
} else {
","
};
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}"}},"autocomplete":"Desktop","icon":{{"type":"fileicon","path":"~/Desktop"}}}}"#,
prefix = prefix,
tags = tags,
comment = comment,
snippet = snippet
)
}
}

134
src/flows/alfred.rs Normal file
View file

@ -0,0 +1,134 @@
use crate::parser;
use crate::structures::cheat::Suggestion;
use crate::structures::{error::command::BashSpawnError, option::Config};
use anyhow::Context;
use anyhow::Error;
use regex::Regex;
use std::env;
use std::process::{Command, Stdio};
lazy_static! {
pub static ref VAR_REGEX: Regex = Regex::new(r"<(\w[\w\d\-_]*)>").expect("Invalid regex");
}
pub fn main(config: Config) -> Result<(), Error> {
let mut child = Command::new("cat").stdin(Stdio::piped()).spawn().unwrap();
let stdin = child.stdin.as_mut().unwrap();
println!(r#"{{"items": ["#);
parser::read_all(&config, stdin).context("Failed to parse variables intended for finder")?;
let _ = child.wait_with_output().context("Failed to wait for fzf")?;
println!(r#"]}}"#);
Ok(())
}
fn prompt_with_suggestions(
_variable_name: &str,
_config: &Config,
suggestion: &Suggestion,
) -> Result<String, Error> {
let (suggestion_command, _suggestion_opts) = suggestion;
let child = Command::new("bash")
.stdout(Stdio::piped())
.arg("-c")
.arg(&suggestion_command)
.spawn()
.map_err(|e| BashSpawnError::new(suggestion_command, e))?;
let suggestions = String::from_utf8(
child
.wait_with_output()
.context("Failed to wait and collect output from bash")?
.stdout,
)
.context("Suggestions are invalid utf8")?;
Ok(suggestions)
}
pub fn suggestions(config: Config) -> Result<(), Error> {
let mut child = Command::new("cat")
.stdin(Stdio::piped())
.stdout(Stdio::null())
.spawn()
.unwrap();
let stdin = child.stdin.as_mut().unwrap();
let variables = parser::read_all(&config, stdin)
.context("Failed to parse variables intended for finder")?;
let tags = env::var("tags").unwrap();
let _comment = env::var("comment").unwrap();
let snippet = env::var("snippet").unwrap();
let varname = VAR_REGEX.captures_iter(&snippet).next();
if let Some(varname) = varname {
let varname = &varname[0];
let varname = &varname[1..varname.len() - 1];
println!(
r#"{{"variables": {{"varname": "{varname}"}}, "items": ["#,
varname = varname
);
let lines = variables
.get(&tags, &varname)
.ok_or_else(|| anyhow!("No suggestions"))
.and_then(|suggestion| {
Ok(prompt_with_suggestions(&varname, &config, suggestion).unwrap())
})?;
let mut is_first = true;
for line in lines.split('\n') {
if line.len() < 3 {
continue;
}
let prefix = if is_first {
is_first = false;
""
} else {
","
};
println!(
r#"{prefix}{{
"type": "file",
"title": "{value}",
"subtitle": "{snippet}",
"autocomplete": "Desktop",
"variables": {{
"{varname}": "{value}"
}},
"icon": {{
"type": "fileicon",
"path": "~/Desktop"
}}
}}"#,
prefix = prefix,
snippet = snippet,
varname = varname,
value = line
);
}
} else {
println!(r#"{{"items": ["#);
}
println!(r#"]}}"#);
Ok(())
}
pub fn transform(_config: Config) -> Result<(), Error> {
Ok(())
}

View file

@ -213,8 +213,6 @@ fn with_new_lines(txt: String) -> String {
}
pub fn main(variant: Variant, config: Config, contains_key: bool) -> Result<(), Error> {
let _ = display::WIDTHS;
let opts =
gen_core_finder_opts(variant, &config).context("Failed to generate finder options")?;
let (raw_selection, variables) = config

View file

@ -1,3 +1,4 @@
pub mod alfred;
pub mod aux;
pub mod best;
pub mod core;

View file

@ -1,7 +1,7 @@
use crate::flows;
use crate::flows::core::Variant;
use crate::structures::option::Command::{Best, Fn, Preview, Query, Repo, Search, Widget};
use crate::structures::option::{Config, RepoCommand};
use crate::structures::option::Command::{Alfred, Best, Fn, Preview, Query, Repo, Search, Widget};
use crate::structures::option::{AlfredCommand, Config, RepoCommand};
use anyhow::Context;
use anyhow::Error;
@ -39,6 +39,12 @@ pub fn handle_config(config: Config) -> Result<(), Error> {
RepoCommand::Browse => flows::repo::browse(&config.finder)
.context("Failed to browse featured cheatsheets"),
},
Alfred { cmd } => match cmd {
AlfredCommand::Start => flows::alfred::main(config),
AlfredCommand::Suggestions => flows::alfred::suggestions(config),
AlfredCommand::Transform => flows::alfred::transform(config),
},
},
}
}

View file

@ -1,9 +1,10 @@
use crate::display;
use crate::display::{self, Writer};
use crate::filesystem;
use crate::structures::cheat::VariableMap;
use crate::structures::finder::{Opts as FinderOpts, SuggestionType};
use crate::structures::fnv::HashLine;
use crate::structures::{error::filesystem::InvalidPath, option::Config};
use crate::structures::option::Command::Alfred;
use crate::structures::{error::filesystem::InvalidPath, item::Item, option::Config};
use crate::welcome;
use anyhow::{Context, Error};
use regex::Regex;
@ -115,18 +116,19 @@ fn write_cmd(
tags: &str,
comment: &str,
snippet: &str,
tag_width: usize,
comment_width: usize,
writer: &mut Box<dyn Writer>,
stdin: &mut std::process::ChildStdin,
) -> Result<(), Error> {
if snippet.len() <= 1 {
Ok(())
} else {
let item = Item {
tags: &tags,
comment: &comment,
snippet: &snippet,
};
stdin
.write_all(
display::format_line(&tags, &comment, &snippet, tag_width, comment_width)
.as_bytes(),
)
.write_all(writer.write(item).as_bytes())
.context("Failed to write command to finder's stdin")
}
}
@ -135,6 +137,7 @@ fn read_file(
path: &str,
variables: &mut VariableMap,
visited_lines: &mut HashSet<u64>,
writer: &mut Box<dyn Writer>,
stdin: &mut std::process::ChildStdin,
) -> Result<(), Error> {
let mut tags = String::from("");
@ -142,8 +145,6 @@ fn read_file(
let mut snippet = String::from("");
let mut should_break = false;
let (tag_width, comment_width) = *display::WIDTHS;
for (line_nr, line_result) in filesystem::read_lines(path)?.enumerate() {
let line = line_result
.with_context(|| format!("Failed to read line nr.{} from `{}`", line_nr, path))?;
@ -160,7 +161,7 @@ fn read_file(
}
// tag
else if line.starts_with('%') {
if write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin).is_err() {
if write_cmd(&tags, &comment, &snippet, writer, stdin).is_err() {
should_break = true
}
snippet = String::from("");
@ -175,7 +176,7 @@ fn read_file(
}
// comment
else if line.starts_with('#') {
if write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin).is_err() {
if write_cmd(&tags, &comment, &snippet, writer, stdin).is_err() {
should_break = true
}
snippet = String::from("");
@ -187,13 +188,13 @@ fn read_file(
}
// variable
else if line.starts_with('$') {
if write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin).is_err() {
if write_cmd(&tags, &comment, &snippet, writer, stdin).is_err() {
should_break = true
}
snippet = String::from("");
let (variable, command, opts) = parse_variable_line(&line).with_context(|| {
format!(
"Failed to parse variable line. See line nr.{} in cheatsheet `{}`",
"Failed to parse variable line. See line number {} in cheatsheet `{}`",
line_nr + 1,
path
)
@ -216,7 +217,7 @@ fn read_file(
}
if !should_break {
let _ = write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin);
let _ = write_cmd(&tags, &comment, &snippet, writer, stdin);
}
Ok(())
@ -233,10 +234,19 @@ pub fn read_all(
let mut variables = VariableMap::new();
let mut found_something = false;
let mut visited_lines = HashSet::new();
let mut writer: Box<dyn Writer> = if let Some(Alfred { .. }) = &config.cmd {
Box::new(display::AlfredWriter { is_first: true })
} else {
let (tag_width, comment_width) = display::get_widths();
Box::new(display::FinderWriter {
tag_width,
comment_width,
})
};
let paths = filesystem::cheat_paths(config);
if paths.is_err() {
welcome::cheatsheet(stdin);
welcome::cheatsheet(&mut writer, stdin);
return Ok(variables);
}
@ -252,7 +262,14 @@ pub fn read_all(
.to_str()
.ok_or_else(|| InvalidPath(path.to_path_buf()))?;
if path_str.ends_with(".cheat")
&& read_file(path_str, &mut variables, &mut visited_lines, stdin).is_ok()
&& read_file(
path_str,
&mut variables,
&mut visited_lines,
&mut writer,
stdin,
)
.is_ok()
&& !found_something
{
found_something = true;
@ -263,7 +280,7 @@ pub fn read_all(
}
if !found_something {
welcome::cheatsheet(stdin);
welcome::cheatsheet(&mut writer, stdin);
}
Ok(variables)
@ -300,7 +317,18 @@ mod tests {
let mut child = Command::new("cat").stdin(Stdio::piped()).spawn().unwrap();
let child_stdin = child.stdin.as_mut().unwrap();
let mut visited_lines: HashSet<u64> = HashSet::new();
read_file(path, &mut variables, &mut visited_lines, child_stdin).unwrap();
let mut writer: Box<dyn Writer> = Box::new(display::FinderWriter {
comment_width: 20,
tag_width: 30,
});
read_file(
path,
&mut variables,
&mut visited_lines,
&mut writer,
child_stdin,
)
.unwrap();
let expected_suggestion = (
r#" echo -e "$(whoami)\nroot" "#.to_string(),
Some(FinderOpts {

View file

@ -3,7 +3,7 @@ use thiserror::Error;
#[derive(Error, Debug)]
#[error(
"\rHey listen! Navi encountered a problem.
"\rHey, listen! Navi encountered a problem.
Do you think this is a bug? File an issue at https://github.com/denisidoro/navi."
)]
pub struct FileAnIssue {

5
src/structures/item.rs Normal file
View file

@ -0,0 +1,5 @@
pub struct Item<'a> {
pub tags: &'a str,
pub comment: &'a str,
pub snippet: &'a str,
}

View file

@ -2,4 +2,5 @@ pub mod cheat;
pub mod error;
pub mod finder;
pub mod fnv;
pub mod item;
pub mod option;

View file

@ -106,6 +106,11 @@ pub enum Command {
/// bash, zsh or fish
shell: String,
},
/// Alfred
Alfred {
#[structopt(subcommand)]
cmd: AlfredCommand,
},
}
#[derive(Debug, StructOpt)]
@ -119,6 +124,16 @@ pub enum RepoCommand {
Browse,
}
#[derive(Debug, StructOpt)]
pub enum AlfredCommand {
/// Start
Start,
/// Suggestions
Suggestions,
/// Transform
Transform,
}
pub fn config_from_env() -> Config {
Config::from_args()
}

View file

@ -1,24 +1,44 @@
use crate::display;
use crate::display::Writer;
use crate::structures::item::Item;
use std::io::Write;
fn add_msg(tag: &str, comment: &str, snippet: &str, stdin: &mut std::process::ChildStdin) {
fn add_msg(
tags: &str,
comment: &str,
snippet: &str,
writer: &mut Box<dyn Writer>,
stdin: &mut std::process::ChildStdin,
) {
let item = Item {
tags: &tags,
comment: &comment,
snippet: &snippet,
};
stdin
.write_all(display::format_line(tag, comment, snippet, 20, 60).as_bytes())
.write_all(writer.write(item).as_bytes())
.expect("Could not write to fzf's stdin");
}
pub fn cheatsheet(stdin: &mut std::process::ChildStdin) {
pub fn cheatsheet(writer: &mut Box<dyn Writer>, stdin: &mut std::process::ChildStdin) {
add_msg(
"cheatsheets",
"Download default cheatsheets",
"navi repo add denisidoro/cheats",
writer,
stdin,
);
add_msg(
"cheatsheets",
"Browse for cheatsheet repos",
"navi repo browse",
writer,
stdin,
);
add_msg(
"more info",
"Read --help message",
"navi --help",
writer,
stdin,
);
add_msg("more info", "Read --help message", "navi --help", stdin);
}