diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 870f45d..1402dc9 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -6,8 +6,27 @@ on: - '*' jobs: - publish: - name: Publish for ${{ matrix.os }} + + alfred: + name: Publish Alfred workflow + os: macos-latest + steps: + - uses: actions/checkout@v1 + - name: Zip + run: scripts/action workflow + - name: Get the version + id: get_version + run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} + - name: Upload workflow to release + uses: svenstaro/upload-release-action@v1-release + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: target/workflow/navi.zip + tag: ${{ github.ref }} + asset_name: navi-${{ steps.get_version.outputs.VERSION }}.alfredworkflow + + binary: + name: Publish binary for ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: diff --git a/Cargo.lock b/Cargo.lock index 3798412..4cced7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -174,7 +174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "navi" -version = "2.5.2" +version = "2.6.0" dependencies = [ "anyhow 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index fb77258..78f519a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "navi" -version = "2.5.2" +version = "2.6.0" authors = ["Denis Isidoro "] edition = "2018" description = "An interactive cheatsheet tool for the command-line" diff --git a/README.md b/README.md index 44cd333..e8ce66b 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ > :information_source: If you're here because you upgraded **navi** and are having some issues, please check [this thread](https://github.com/denisidoro/navi/issues/201). -An interactive cheatsheet tool for the command-line. +An interactive cheatsheet tool for the command-line and application launchers. -![Demo](https://user-images.githubusercontent.com/3226564/76437136-ddc35900-6397-11ea-823c-d2da7615fe60.gif) +![Terminal demo](https://user-images.githubusercontent.com/3226564/76437136-ddc35900-6397-11ea-823c-d2da7615fe60.gif) **navi** allows you to browse through cheatsheets (that you may write yourself or download from maintainers) and execute commands. Argument suggestions are prompted to you. @@ -13,7 +13,9 @@ An interactive cheatsheet tool for the command-line. - it will spare you from knowing CLIs by heart; - it will teach you new one-liners. -It uses [fzf](https://github.com/junegunn/fzf) or [skim](https://github.com/lotabout/skim) under the hood and it can be either used as a command or as a shell widget (*à la* Ctrl-R). +It uses [fzf](https://github.com/junegunn/fzf), [skim](https://github.com/lotabout/skim), or [Alfred](https://www.alfredapp.com/) under the hood and it can be either used as a command or as a shell widget (*à la* Ctrl-R). + +![Alfred demo](https://user-images.githubusercontent.com/3226564/79696281-35d06380-8252-11ea-87d5-dc619d869e81.gif) Table of contents ----------------- @@ -27,6 +29,7 @@ Table of contents * [Other package managers](#other-package-managers) * [Usage](#usage) * [Shell widget](#shell-widget) + * [Alfred](#alfred) * [More options](#more-options) * [Trying out online](#trying-out-online) * [Cheatsheets](#cheatsheets) @@ -112,6 +115,8 @@ navi widget fish | source By default, `Ctrl+G` is assigned to launching **navi**. +### Alfred + ### More options Please refer to `navi --help` for more details. @@ -248,7 +253,7 @@ $ jsons: find . -iname '*.json' -type f -print --- --multi List customization ------------------ -Lists can be stylized with the [$FZF_DEFAULT_OPTS](https://github.com/junegunn/fzf#layout) environment variable or similar variables or parameters ( please refer to `navi --help`). This way, you can change the [color scheme](https://github.com/junegunn/fzf/wiki/Color-schemes), for example. +Lists can be stylized with the [$FZF_DEFAULT_OPTS](https://github.com/junegunn/fzf#layout) environment variable or similar variables/parameters (please refer to `navi --help`). This way, you can change the [color scheme](https://github.com/junegunn/fzf/wiki/Color-schemes), for example. Related projects diff --git a/alfred/alfred.bash b/alfred/alfred.bash new file mode 100755 index 0000000..1fe2cf9 --- /dev/null +++ b/alfred/alfred.bash @@ -0,0 +1,9 @@ +#!/bin/bash + +source "${HOME}/.bashrc" + +if [ -n "${snippet:-}" ]; then + navi alfred suggestions +else + navi alfred start +fi diff --git a/alfred/alfred2.bash b/alfred/alfred2.bash new file mode 100755 index 0000000..16e38f1 --- /dev/null +++ b/alfred/alfred2.bash @@ -0,0 +1,15 @@ +#!/bin/bash + +_interpolate() { + local -r snippet="$1" + local -r varname="$2" + local -r value="${!varname}" + + echo "$snippet" | sed "s/<${varname}>/${value}/g" +} + +if [ -n "${varname:-}" ]; then + echo -n "$(_interpolate "$snippet" "$varname" || echo "")" +else + echo -n "$snippet" +fi diff --git a/alfred/info.plist b/alfred/info.plist new file mode 100644 index 0000000..6d06884 --- /dev/null +++ b/alfred/info.plist @@ -0,0 +1,485 @@ + + + + + bundleid + com.github.denisidoro.navi + category + Tools + connections + + 3CE4BEC1-BEBE-4A11-B182-E63693AE2FE6 + + + destinationuid + 98658166-32FC-4F9D-8247-52BEDE6394EE + modifiers + 0 + modifiersubtext + + vitoclose + + + + 55C46852-4807-4374-95AB-CC055F4ECB7C + + + destinationuid + 7DD3BDE5-A157-42E5-9376-F681FB50A4EE + modifiers + 0 + modifiersubtext + + vitoclose + + + + 7DD3BDE5-A157-42E5-9376-F681FB50A4EE + + + destinationuid + 3CE4BEC1-BEBE-4A11-B182-E63693AE2FE6 + modifiers + 0 + modifiersubtext + + vitoclose + + + + 98658166-32FC-4F9D-8247-52BEDE6394EE + + + destinationuid + B55D209F-7FF3-4C23-AA39-BA8E37B91452 + modifiers + 0 + modifiersubtext + + vitoclose + + + + B55D209F-7FF3-4C23-AA39-BA8E37B91452 + + + destinationuid + 93437EDA-0308-467D-BE3F-6855C638D49D + modifiers + 0 + modifiersubtext + + sourceoutputuid + F78DD511-9EC2-4272-8946-C6E3E269DE2E + vitoclose + + + + destinationuid + 529BC67C-C77D-41CA-B095-996CC1317556 + modifiers + 0 + modifiersubtext + + vitoclose + + + + F05BFEA8-AEE7-486A-9A54-B3162A233BAC + + + destinationuid + 7DD3BDE5-A157-42E5-9376-F681FB50A4EE + modifiers + 0 + modifiersubtext + + vitoclose + + + + + createdby + Denis Isidoro + description + Integration with navi, the interactive cheatsheet tool for the command-line + disabled + + name + navi + objects + + + config + + externaltriggerid + play + passinputasargument + + passvariables + + workflowbundleid + self + + type + alfred.workflow.output.callexternaltrigger + uid + 93437EDA-0308-467D-BE3F-6855C638D49D + version + 1 + + + config + + alfredfiltersresults + + alfredfiltersresultsmatchmode + 0 + argumenttreatemptyqueryasnil + + argumenttrimmode + 0 + argumenttype + 1 + escaping + 102 + keyword + n + queuedelaycustom + 3 + queuedelayimmediatelyinitially + + queuedelaymode + 0 + queuemode + 1 + runningsubtext + Generating results... + script + #!/bin/bash +# +# Alfred Script Filter JSON format +# +# This example demonstrates all fields available for populating results. +# +# For an in-depth explanation, use the (?) help button to the bottom left. +# + +_print_snippets() { + + IFS=$'\n' + + items="{\"items\": [" + uid="$RANDOM" + + snippets="$(cat uber.cheat | grep '^#' | sed 's/^# //')" + + for line in $snippets; do + + # title="$(echo "$line" | tr -d '"')" + title="$(echo "$line" | tr -d '"')" + uid="$title" + + item=$(cat <<EOF +{ + "uid": "$uid", + "type": "file", + "title": "$title", + "subtitle": "subtitle", + "arg": "echo <foo> <bar>", + "autocomplete": "Desktop", + "icon": { + "type": "fileicon", + "path": "~/Desktop" + } +}, +EOF + ) + + items="$items $item" + + done + + echo "${items%?} ]}" + +} + +_interpolate() { + + snippet="$1" + + IFS=$'\n' + + items="{\"items\": [" + uid="$RANDOM" + + snippets="foo + bar" + + for line in $snippets; do + + # title="$(echo "$line" | tr -d '"')" + title="$(echo "$line" | tr -d '"')" + uid="$title" + + item=$(cat <<EOF +{ + "uid": "$uid", + "type": "file", + "title": "lorem", + "subtitle": "subtitle", + "arg": "ipsum", + "autocomplete": "Desktop", + "icon": { + "type": "fileicon", + "path": "~/Desktop" + } +}, +EOF + ) + + items="$items $item" + + done + + echo "${items%?} ]}" + +} + +if [ -n "${snippet:-} "]; then + _interpolate "$snippet" +elif [ -n "${query:-} "]; then + _interpolate "$query" +else + _print_snippets +fi + + scriptargtype + 1 + scriptfile + alfred.bash + subtext + + title + navi + type + 8 + withspace + + + type + alfred.workflow.input.scriptfilter + uid + 7DD3BDE5-A157-42E5-9376-F681FB50A4EE + version + 3 + + + config + + triggerid + play + + type + alfred.workflow.trigger.external + uid + 55C46852-4807-4374-95AB-CC055F4ECB7C + version + 1 + + + config + + concurrently + + escaping + 102 + script + echo asdsds + scriptargtype + 1 + scriptfile + alfred2.bash + type + 8 + + type + alfred.workflow.action.script + uid + 3CE4BEC1-BEBE-4A11-B182-E63693AE2FE6 + version + 2 + + + config + + conditions + + + inputstring + {var:snippet} + matchcasesensitive + + matchmode + 4 + matchstring + <[\w\d_\-]+> + outputlabel + variable left + uid + F78DD511-9EC2-4272-8946-C6E3E269DE2E + + + elselabel + no variables + + type + alfred.workflow.utility.conditional + uid + B55D209F-7FF3-4C23-AA39-BA8E37B91452 + version + 1 + + + config + + argument + + variables + + snippet + {query} + + + type + alfred.workflow.utility.argument + uid + 98658166-32FC-4F9D-8247-52BEDE6394EE + version + 1 + + + config + + concurrently + + escaping + 0 + script + source $HOME/.bashrc +eval "$snippet" + scriptargtype + 0 + scriptfile + navi + type + 0 + + type + alfred.workflow.action.script + uid + 529BC67C-C77D-41CA-B095-996CC1317556 + version + 2 + + + config + + action + 0 + argument + 0 + focusedappvariable + + focusedappvariablename + + hotkey + 6 + hotmod + 1966080 + hotstring + Z + leftcursor + + modsmode + 0 + relatedAppsMode + 0 + + type + alfred.workflow.trigger.hotkey + uid + F05BFEA8-AEE7-486A-9A54-B3162A233BAC + version + 2 + + + readme + + uidata + + 3CE4BEC1-BEBE-4A11-B182-E63693AE2FE6 + + xpos + 355 + ypos + 170 + + 529BC67C-C77D-41CA-B095-996CC1317556 + + xpos + 835 + ypos + 285 + + 55C46852-4807-4374-95AB-CC055F4ECB7C + + xpos + 10 + ypos + 170 + + 7DD3BDE5-A157-42E5-9376-F681FB50A4EE + + xpos + 175 + ypos + 170 + + 93437EDA-0308-467D-BE3F-6855C638D49D + + xpos + 830 + ypos + 155 + + 98658166-32FC-4F9D-8247-52BEDE6394EE + + xpos + 535 + ypos + 200 + + B55D209F-7FF3-4C23-AA39-BA8E37B91452 + + xpos + 640 + ypos + 190 + + F05BFEA8-AEE7-486A-9A54-B3162A233BAC + + xpos + 10 + ypos + 310 + + + variablesdontexport + + version + 0.1.0 + webaddress + https://github.com/denisidoro + + diff --git a/scripts/action b/scripts/action index ac1eba8..b846b32 100755 --- a/scripts/action +++ b/scripts/action @@ -35,14 +35,31 @@ release() { cp "$bin_path" "$TAR_DIR" - cd "${NAVI_HOME}/target/tar" + cd "$TAR_DIR" tar -czf navi.tar.gz * } +workflow() { + + WORKFLOW_DIR="${NAVI_HOME}/target/workflow" + + cd "$NAVI_HOME" + + rm -rf "${NAVI_HOME}/target" 2> /dev/null || true + + mkdir -p "$WORKFLOW_DIR" 2> /dev/null || true + + cp -r "${NAVI_HOME}/alfred" "$WORKFLOW_DIR" + cd "$WORKFLOW_DIR" + zip -r "navi.zip" "." + +} + cmd="$1" shift case "$cmd" in "release") release "$@" ;; + "workflow") workflow "$@" ;; esac diff --git a/src/display.rs b/src/display.rs index a7973f9..3c08551 100644 --- a/src/display.rs +++ b/src/display.rs @@ -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 + ) + } } diff --git a/src/flows/alfred.rs b/src/flows/alfred.rs new file mode 100644 index 0000000..0c94c38 --- /dev/null +++ b/src/flows/alfred.rs @@ -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 { + 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(()) +} diff --git a/src/flows/core.rs b/src/flows/core.rs index 9d40275..a63bfc8 100644 --- a/src/flows/core.rs +++ b/src/flows/core.rs @@ -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 diff --git a/src/flows/mod.rs b/src/flows/mod.rs index 6b0925d..d6b5f45 100644 --- a/src/flows/mod.rs +++ b/src/flows/mod.rs @@ -1,3 +1,4 @@ +pub mod alfred; pub mod aux; pub mod best; pub mod core; diff --git a/src/handler.rs b/src/handler.rs index 9d6e86a..57e3cb4 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -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), + }, }, } } diff --git a/src/parser.rs b/src/parser.rs index 89f01cc..aa1023b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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, 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, + writer: &mut Box, 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 = 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 = HashSet::new(); - read_file(path, &mut variables, &mut visited_lines, child_stdin).unwrap(); + let mut writer: Box = 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 { diff --git a/src/structures/error/file_issue.rs b/src/structures/error/file_issue.rs index a0d963f..007319a 100644 --- a/src/structures/error/file_issue.rs +++ b/src/structures/error/file_issue.rs @@ -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 { diff --git a/src/structures/item.rs b/src/structures/item.rs new file mode 100644 index 0000000..64fe432 --- /dev/null +++ b/src/structures/item.rs @@ -0,0 +1,5 @@ +pub struct Item<'a> { + pub tags: &'a str, + pub comment: &'a str, + pub snippet: &'a str, +} diff --git a/src/structures/mod.rs b/src/structures/mod.rs index 815b296..ac41d76 100644 --- a/src/structures/mod.rs +++ b/src/structures/mod.rs @@ -2,4 +2,5 @@ pub mod cheat; pub mod error; pub mod finder; pub mod fnv; +pub mod item; pub mod option; diff --git a/src/structures/option.rs b/src/structures/option.rs index f16faed..41518ef 100644 --- a/src/structures/option.rs +++ b/src/structures/option.rs @@ -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() } diff --git a/src/welcome.rs b/src/welcome.rs index c7bd99b..726e630 100644 --- a/src/welcome.rs +++ b/src/welcome.rs @@ -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, + 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, 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); }