mirror of
https://github.com/denisidoro/navi.git
synced 2026-07-26 03:24:35 +00:00
Add support for Alfred (#347)
This commit is contained in:
parent
019adf82cf
commit
c7f919eaa0
19 changed files with 850 additions and 54 deletions
|
|
@ -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
134
src/flows/alfred.rs
Normal 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(())
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod alfred;
|
||||
pub mod aux;
|
||||
pub mod best;
|
||||
pub mod core;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
5
src/structures/item.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub struct Item<'a> {
|
||||
pub tags: &'a str,
|
||||
pub comment: &'a str,
|
||||
pub snippet: &'a str,
|
||||
}
|
||||
|
|
@ -2,4 +2,5 @@ pub mod cheat;
|
|||
pub mod error;
|
||||
pub mod finder;
|
||||
pub mod fnv;
|
||||
pub mod item;
|
||||
pub mod option;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue