From ebb02e28ec338d55c89c5b2d321d038f982538c5 Mon Sep 17 00:00:00 2001 From: Denis Isidoro Date: Thu, 28 Jul 2022 20:11:42 -0300 Subject: [PATCH] Refactor code base (#760) --- .github/workflows/cd.yml | 2 +- .github/workflows/ci.yml | 100 +++--- Cargo.lock | 26 +- Cargo.toml | 10 +- rust-toolchain.toml | 2 +- scripts/dot | 27 ++ scripts/fix | 38 -- scripts/release | 93 ----- scripts/run | 16 - scripts/tag | 12 - src/{ => clients}/cheatsh.rs | 64 ++-- src/clients/mod.rs | 2 + src/{ => clients}/tldr.rs | 37 +- src/{ => commands/core}/actor.rs | 32 +- src/{ => commands/core}/extractor.rs | 8 +- src/commands/core/mod.rs | 41 +++ .../func/map.rs} | 7 +- src/commands/func/mod.rs | 65 ++++ src/commands/func/widget.rs | 40 +++ src/commands/info.rs | 48 +++ src/commands/mod.rs | 39 +++ src/commands/preview/mod.rs | 39 +++ src/commands/preview/var.rs | 106 ++++++ src/commands/preview/var_stdin.rs | 43 +++ .../repo_add.rs => commands/repo/add.rs} | 43 +-- .../repo/browse.rs} | 20 +- src/commands/repo/mod.rs | 41 +++ src/commands/shell.rs | 43 +++ src/commands/temp.rs | 64 ++++ src/{ => common}/clipboard.rs | 4 +- src/common/component.rs.bkp | 21 ++ src/common/deps.rs.bkp | 7 + src/common/deser.rs.bkp | 50 +++ src/{ => common}/fs.rs | 34 +- src/{ => common}/git.rs | 4 +- src/{ => common}/hash.rs | 0 src/common/mod.rs | 13 + src/common/prelude.rs | 29 ++ src/common/shell.rs | 45 +++ src/common/system.rs.bkp | 57 +++ src/common/terminal.rs | 65 ++++ src/common/tracing.rs.bkp | 36 ++ src/{ => common}/url.rs | 3 +- src/config/cli.rs | 111 +----- src/config/env.rs | 4 +- src/config/mod.rs | 29 +- src/config/yaml.rs | 13 +- src/env_var.rs | 2 +- src/filesystem.rs | 102 ++---- src/finder/mod.rs | 35 +- src/finder/post.rs | 6 +- src/finder/structures.rs | 2 +- src/handler/core.rs | 51 --- src/handler/func.rs | 25 -- src/handler/info.rs | 21 -- src/handler/mod.rs | 60 ---- src/handler/preview.rs | 27 -- src/handler/preview_var.rs | 91 ----- src/handler/preview_var_stdin.rs | 29 -- src/handler/shell.rs | 15 - src/lib.rs | 25 +- src/parser.rs | 324 +++++++++++------- src/prelude.rs | 9 + src/serializer.rs | 121 +++++++ src/shell.rs | 86 ----- src/structures/cheat.rs | 13 +- src/structures/fetcher.rs | 14 +- src/structures/item.rs | 18 +- src/terminal.rs | 66 ---- src/ui.rs | 25 -- src/welcome.rs | 58 ++-- src/writer.rs | 52 --- 72 files changed, 1556 insertions(+), 1354 deletions(-) create mode 100755 scripts/dot delete mode 100755 scripts/fix delete mode 100755 scripts/release delete mode 100755 scripts/run delete mode 100755 scripts/tag rename src/{ => clients}/cheatsh.rs (63%) create mode 100644 src/clients/mod.rs rename src/{ => clients}/tldr.rs (81%) rename src/{ => commands/core}/actor.rs (93%) rename src/{ => commands/core}/extractor.rs (88%) create mode 100644 src/commands/core/mod.rs rename src/{cheat_variable.rs => commands/func/map.rs} (64%) create mode 100644 src/commands/func/mod.rs create mode 100644 src/commands/func/widget.rs create mode 100644 src/commands/info.rs create mode 100644 src/commands/mod.rs create mode 100644 src/commands/preview/mod.rs create mode 100644 src/commands/preview/var.rs create mode 100755 src/commands/preview/var_stdin.rs rename src/{handler/repo_add.rs => commands/repo/add.rs} (74%) rename src/{handler/repo_browse.rs => commands/repo/browse.rs} (76%) create mode 100644 src/commands/repo/mod.rs create mode 100644 src/commands/shell.rs create mode 100644 src/commands/temp.rs rename src/{ => common}/clipboard.rs (89%) create mode 100644 src/common/component.rs.bkp create mode 100644 src/common/deps.rs.bkp create mode 100644 src/common/deser.rs.bkp rename src/{ => common}/fs.rs (78%) rename src/{ => common}/git.rs (96%) rename src/{ => common}/hash.rs (100%) create mode 100644 src/common/mod.rs create mode 100644 src/common/prelude.rs create mode 100644 src/common/shell.rs create mode 100644 src/common/system.rs.bkp create mode 100644 src/common/terminal.rs create mode 100644 src/common/tracing.rs.bkp rename src/{ => common}/url.rs (91%) delete mode 100644 src/handler/core.rs delete mode 100644 src/handler/func.rs delete mode 100644 src/handler/info.rs delete mode 100644 src/handler/mod.rs delete mode 100644 src/handler/preview.rs delete mode 100644 src/handler/preview_var.rs delete mode 100755 src/handler/preview_var_stdin.rs delete mode 100644 src/handler/shell.rs create mode 100644 src/prelude.rs create mode 100644 src/serializer.rs delete mode 100644 src/shell.rs delete mode 100644 src/ui.rs delete mode 100644 src/writer.rs diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index ab14df4..0eecae4 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -48,7 +48,7 @@ jobs: - uses: actions/checkout@v1 - name: Build id: build - run: scripts/release ${{ matrix.target }} + run: scripts/dot rust release ${{ matrix.target }} - name: Get the version id: get_version run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31ff4b9..9e2f261 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,25 +9,25 @@ on: [push] name: CI jobs: - check: - name: Check - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v2 + # check: + # name: Check + # runs-on: ubuntu-latest + # steps: + # - name: Checkout sources + # uses: actions/checkout@v2 - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + # - name: Install stable toolchain + # uses: actions-rs/toolchain@v1 + # with: + # profile: minimal + # toolchain: stable + # override: true - - name: Run cargo check - uses: actions-rs/cargo@v1 - continue-on-error: false - with: - command: check + # - name: Run cargo check + # uses: actions-rs/cargo@v1 + # continue-on-error: false + # with: + # command: check test: name: Tests @@ -64,21 +64,7 @@ jobs: command: test - name: Install deps - run: | - _install() { - if command -v "$1"; then - return 0; - fi - sudo apt-get install "$1" || sudo apt install "$1" || brew install "$1" - }; - - _install_many() { - for dep in $@; do - _install "$dep" - done - } - - _install_many git bash npm tmux + run: ./scripts/dot pkg add git bash npm tmux - name: Install fzf run: git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf; yes | ~/.fzf/install; @@ -89,31 +75,31 @@ jobs: - name: Run bash tests run: ./tests/run - # lints: - # name: Lints - # runs-on: ubuntu-latest - # steps: - # - name: Checkout sources - # uses: actions/checkout@v2 + lints: + name: Lints + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 - # - name: Install stable toolchain - # uses: actions-rs/toolchain@v1 - # with: - # profile: minimal - # toolchain: stable - # override: true - # components: rustfmt, clippy + # - name: Install stable toolchain + # uses: actions-rs/toolchain@v1 + # with: + # profile: minimal + # toolchain: stable + # override: true + # components: rustfmt, clippy - # - name: Run cargo fmt - # uses: actions-rs/cargo@v1 - # continue-on-error: false - # with: - # command: fmt - # args: --all -- --check + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + continue-on-error: false + with: + command: fmt + args: --all -- --check - # - name: Run cargo clippy - # uses: actions-rs/cargo@v1 - # continue-on-error: false - # with: - # command: clippy - # args: -- -D warnings + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + continue-on-error: false + with: + command: clippy + args: -- -D warnings diff --git a/Cargo.lock b/Cargo.lock index 856a777..18f4ae8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,9 +54,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.2.8" +version = "3.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83" +checksum = "54635806b078b7925d6e36810b1755f2a4b5b4d57560432c1ecf60bcbe10602b" dependencies = [ "atty", "bitflags", @@ -304,7 +304,7 @@ dependencies = [ [[package]] name = "navi" -version = "2.20.1" +version = "2.21.0" dependencies = [ "anyhow", "clap", @@ -501,9 +501,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.6" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "aho-corasick", "memchr", @@ -512,9 +512,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.26" +version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "remove_dir_all" @@ -561,18 +561,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.138" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47" +checksum = "fc855a42c7967b7c369eb5860f7164ef1f6f81c20c7cc1141f2a604e18723b03" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.138" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c" +checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da" dependencies = [ "proc-macro2", "quote", @@ -581,9 +581,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.8.24" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707d15895415db6628332b737c838b88c598522e4dc70647e59b72312924aebc" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ "indexmap", "ryu", diff --git a/Cargo.toml b/Cargo.toml index e8ca6fb..534aac3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "navi" -version = "2.20.1" +version = "2.21.0" authors = ["Denis Isidoro "] edition = "2021" description = "An interactive cheatsheet tool for the command-line" @@ -19,8 +19,8 @@ disable-repo-management = [] travis-ci = { repository = "denisidoro/navi", branch = "master" } [dependencies] -regex = { version = "1.5.6", default-features = false, features = ["std", "unicode-perl"] } -clap = { version = "3.2.8", features = ["derive", "cargo"] } +regex = { version = "1.6.0", default-features = false, features = ["std", "unicode-perl"] } +clap = { version = "3.2.14", features = ["derive", "cargo"] } crossterm = "0.24.0" lazy_static = "1.4.0" directories-next = "2.0.0" @@ -31,8 +31,8 @@ thiserror = "1.0.31" strip-ansi-escapes = "0.1.1" edit = "0.1.4" remove_dir_all = "0.7.0" -serde = { version = "1.0.138", features = ["derive"] } -serde_yaml = "0.8.24" +serde = { version = "1.0.140", features = ["derive"] } +serde_yaml = "0.8.26" [lib] name = "navi" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index bb6a5c9..af43bca 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.56.0" +channel = "1.62.0" components = [ "rustfmt", "clippy" ] \ No newline at end of file diff --git a/scripts/dot b/scripts/dot new file mode 100755 index 0000000..c9fad08 --- /dev/null +++ b/scripts/dot @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)" +export PROJ_HOME="$NAVI_HOME" +export PROJ_NAME="navi" +export CARGO_PATH="${NAVI_HOME}/core/Cargo.toml" + +# TODO: bump dotfiles + remove this fn +log::note() { log::info "$@"; } +export -f log::note + +dot::clone() { + git clone 'https://github.com/denisidoro/dotfiles' "$DOTFILES" + cd "$DOTFILES" + git checkout 'v2022.07.16' +} + +dot::clone_if_necessary() { + [ -n "${DOTFILES:-}" ] && [ -x "${DOTFILES}/bin/dot" ] && return + export DOTFILES="${NAVI_HOME}/target/dotfiles" + dot::clone +} + +dot::clone_if_necessary + +"${DOTFILES}/bin/dot" "$@" \ No newline at end of file diff --git a/scripts/fix b/scripts/fix deleted file mode 100755 index 71f60cb..0000000 --- a/scripts/fix +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)" -source "${NAVI_HOME}/scripts/install" - -_commit() { - if [ -n "${DOTFILES:-}" ]; then - git add --all || true - dot git commit am || true - fi -} - -cd "$NAVI_HOME" - -_commit -log::note "cargo clippy fix..." -cargo +nightly clippy --fix -Z unstable-options || true - -_commit -log::note "cargo fix..." -cargo fix || true - -_commit -log::note "cargo fmt..." -cargo fmt || true - -_commit -log::note "clippy..." -cargo clippy || true - -_commit -log::note "dot code beautify..." -find scripts -type f | xargs -I% dot code beautify % || true -dot code beautify "${NAVI_HOME}/shell/navi.plugin.bash" || true -dot code beautify "${NAVI_HOME}/shell/navi.plugin.zsh" || true -dot code beautify "${NAVI_HOME}/tests/core.bash" || true -dot code beautify "${NAVI_HOME}/tests/run" || true diff --git a/scripts/release b/scripts/release deleted file mode 100755 index 62e304e..0000000 --- a/scripts/release +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -##? release - -export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)" -source "${NAVI_HOME}/scripts/install" - -is_windows() { - local -r target="$1" - echo "$target" | grep -q "windows" -} - -get_env_target() { - eval $(rustc --print cfg | grep target) - local r raw="${target_arch}-${target_vendor}-${target_os}-${target_env}" - log::note "env target raw: $raw" - if echo "$raw" | grep -q "x86_64-apple-macos"; then - echo "x86_64-apple-darwin" - else - echo "$raw" - fi -} - -_tap() { - log::note "$@" - "$@" -} - -release() { - local -r env_target="$(get_env_target)" - log::note "env target: $env_target" - - local -r cross_target="${1:-"$env_target"}" - log::note "desired target: $cross_target" - - TAR_DIR="${NAVI_HOME}/target/tar" - local use_zip=false - local cross=true - - if [[ $cross_target == $env_target ]]; then - cross=false - fi - - cd "$NAVI_HOME" - - rm -rf "${NAVI_HOME}/target" 2> /dev/null || true - - if $cross; then - cargo install cross 2> /dev/null || true - _tap cross build --release --locked --target "$cross_target" - local -r bin_folder="${cross_target}/release" - else - _tap cargo build --release --locked - local -r bin_folder="release" - fi - - _ls "${bin_folder}" - - if is_windows "$cross_target"; then - local -r exe_ext=".exe" - use_zip=true - else - local -r exe_ext="" - fi - - bin_path="${NAVI_HOME}/target/${bin_folder}/navi${exe_ext}" - - chmod +x "$bin_path" - mkdir -p "$TAR_DIR" 2> /dev/null || true - - cp "$bin_path" "$TAR_DIR" - - cd "$TAR_DIR" - - if $use_zip; then - zip -r navi.zip * - echo ::set-output name=EXTENSION::zip - else - tar -czf navi.tar.gz * - echo ::set-output name=EXTENSION::tar.gz - fi - - _ls "${bin_path}" - _ls "${TAR_DIR}" -} - -_ls() { - log::note "contents from $@:" - ls -la "$@" || true -} - -release "$@" diff --git a/scripts/run b/scripts/run deleted file mode 100755 index 53f1238..0000000 --- a/scripts/run +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)" - -cd "$NAVI_HOME" - -if command_exists navi; then - navi "$@" -elif [ -f "./target/release/navi" ]; then - "./target/release/navi" "$@" -elif [ -f "./target/debug/navi" ]; then - "./target/debug/navi" "$@" -else - cargo run -- "$@" -fi diff --git a/scripts/tag b/scripts/tag deleted file mode 100755 index 678da5b..0000000 --- a/scripts/tag +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)" -source "${NAVI_HOME}/scripts/install" - -version="${1:-$(version_from_toml)}" -log::note "version: $version..." -sleep 2 - -git tag -a "v${version}" -git push origin --tags diff --git a/src/cheatsh.rs b/src/clients/cheatsh.rs similarity index 63% rename from src/cheatsh.rs rename to src/clients/cheatsh.rs index d5ac039..f039b0c 100644 --- a/src/cheatsh.rs +++ b/src/clients/cheatsh.rs @@ -1,10 +1,8 @@ -use crate::parser; -use crate::structures::cheat::VariableMap; +use crate::parser::Parser; +use crate::prelude::*; + use crate::structures::fetcher; -use anyhow::Context; -use anyhow::Result; -use std::collections::HashSet; -use std::process::{self, Command, Stdio}; +use std::process::{self, Command}; fn map_line(line: &str) -> String { line.trim().trim_end_matches(':').to_string() @@ -22,35 +20,6 @@ fn lines(query: &str, markdown: &str) -> impl Iterator> { .into_iter() } -fn read_all(query: &str, cheat: &str, stdin: &mut std::process::ChildStdin) -> Result> { - let mut variables = VariableMap::new(); - let mut visited_lines = HashSet::new(); - - if cheat.starts_with("Unknown topic.") { - eprintln!( - "`{}` not found in cheatsh. - -Output: -{} -", - query, cheat - ); - process::exit(35) - } - - parser::read_lines( - lines(query, cheat), - "cheat.sh", - 0, - &mut variables, - &mut visited_lines, - stdin, - None, - None, - )?; - Ok(Some(variables)) -} - pub fn fetch(query: &str) -> Result { let args = ["-qO-", &format!("cheat.sh/{}", query)]; @@ -109,12 +78,23 @@ impl Fetcher { } impl fetcher::Fetcher for Fetcher { - fn fetch( - &self, - stdin: &mut std::process::ChildStdin, - _files: &mut Vec, - ) -> Result> { - let cheat = fetch(&self.query)?; - read_all(&self.query, &cheat, stdin) + fn fetch(&self, parser: &mut Parser) -> Result { + let cheat = &fetch(&self.query)?; + + if cheat.starts_with("Unknown topic.") { + eprintln!( + "`{}` not found in cheatsh. + +Output: +{} +", + &self.query, cheat + ); + process::exit(35) + } + + parser.read_lines(lines(&self.query, cheat), "cheat.sh", None)?; + + Ok(true) } } diff --git a/src/clients/mod.rs b/src/clients/mod.rs new file mode 100644 index 0000000..1f0dbcd --- /dev/null +++ b/src/clients/mod.rs @@ -0,0 +1,2 @@ +pub mod cheatsh; +pub mod tldr; diff --git a/src/tldr.rs b/src/clients/tldr.rs similarity index 81% rename from src/tldr.rs rename to src/clients/tldr.rs index adbf5da..f164534 100644 --- a/src/tldr.rs +++ b/src/clients/tldr.rs @@ -1,10 +1,6 @@ -use crate::parser; -use crate::structures::cheat::VariableMap; +use crate::parser::Parser; +use crate::prelude::*; use crate::structures::fetcher; -use anyhow::{Context, Result}; -use regex::Regex; -use std::collections::HashSet; - use std::process::{self, Command, Stdio}; lazy_static! { @@ -58,26 +54,6 @@ fn markdown_lines(query: &str, markdown: &str) -> impl Iterator Result> { - let mut variables = VariableMap::new(); - let mut visited_lines = HashSet::new(); - parser::read_lines( - markdown_lines(query, markdown), - "markdown", - 0, - &mut variables, - &mut visited_lines, - stdin, - None, - None, - )?; - Ok(Some(variables)) -} - pub fn fetch(query: &str) -> Result { let args = [query, "--markdown"]; @@ -148,12 +124,9 @@ impl Fetcher { } impl fetcher::Fetcher for Fetcher { - fn fetch( - &self, - stdin: &mut std::process::ChildStdin, - _files: &mut Vec, - ) -> Result> { + fn fetch(&self, parser: &mut Parser) -> Result { let markdown = fetch(&self.query)?; - read_all(&self.query, &markdown, stdin) + parser.read_lines(markdown_lines(&self.query, &markdown), "markdown", None)?; + Ok(true) } } diff --git a/src/actor.rs b/src/commands/core/actor.rs similarity index 93% rename from src/actor.rs rename to src/commands/core/actor.rs index 306acce..e423afe 100644 --- a/src/actor.rs +++ b/src/commands/core/actor.rs @@ -1,20 +1,15 @@ -use crate::clipboard; +use super::extractor; +use crate::common::clipboard; +use crate::common::fs; +use crate::common::shell; +use crate::common::shell::ShellSpawnError; use crate::config::Action; -use crate::config::CONFIG; use crate::env_var; -use crate::extractor; use crate::finder::structures::{Opts as FinderOpts, SuggestionType}; -use crate::finder::Finder; -use crate::fs; -use crate::shell; -use crate::shell::ShellSpawnError; +use crate::prelude::*; +use crate::serializer; use crate::structures::cheat::{Suggestion, VariableMap}; -use crate::writer; -use anyhow::Context; -use anyhow::Result; use shell::EOF; -use std::io::Write; -use std::path::Path; use std::process::Stdio; fn prompt_finder( @@ -128,13 +123,13 @@ fn prompt_finder( opts.suggestion_type = SuggestionType::Disabled; }; - let (output, _, _) = CONFIG + let (output, _) = CONFIG .finder() - .call(opts, |stdin, _| { + .call(opts, |stdin| { stdin .write_all(suggestions.as_bytes()) .context("Could not write to finder's stdin")?; - Ok(None) + Ok(()) }) .context("finder was unable to prompt with suggestions")?; @@ -150,7 +145,10 @@ fn unique_result_count(results: &[&str]) -> usize { fn replace_variables_from_snippet(snippet: &str, tags: &str, variables: VariableMap) -> Result { 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 variables_found: Vec<&str> = serializer::VAR_REGEX + .find_iter(snippet) + .map(|m| m.as_str()) + .collect(); let variable_count = unique_result_count(&variables_found); for bracketed_variable_name in variables_found { @@ -213,7 +211,7 @@ pub fn act( ) .context("Failed to replace variables from snippet")?; s = with_absolute_path(s); - s = writer::with_new_lines(s); + s = serializer::with_new_lines(s); s }; diff --git a/src/extractor.rs b/src/commands/core/extractor.rs similarity index 88% rename from src/extractor.rs rename to src/commands/core/extractor.rs index 7b6ed3b..56758e8 100644 --- a/src/extractor.rs +++ b/src/commands/core/extractor.rs @@ -1,7 +1,5 @@ -use crate::writer; - -use anyhow::Context; -use anyhow::Result; +use crate::prelude::*; +use crate::serializer; pub type Output<'a> = (&'a str, &'a str, &'a str, &'a str, Option); @@ -18,7 +16,7 @@ pub fn extract_from_selections(raw_snippet: &str, is_single: bool) -> Result Result<()> { + let config = &CONFIG; + let opts = FinderOpts::snippet_default(); + + let (raw_selection, (variables, files)) = config + .finder() + .call(opts, |writer| { + let fetcher = config.fetcher(); + + let mut parser = Parser::new(writer, true); + + let found_something = fetcher + .fetch(&mut parser) + .context("Failed to parse variables intended for finder")?; + + if !found_something { + welcome::populate_cheatsheet(&mut parser)?; + } + + Ok((Some(parser.variables), fetcher.files())) + }) + .context("Failed getting selection and variables from finder")?; + + let extractions = extractor::extract_from_selections(&raw_selection, config.best_match()); + + if extractions.is_err() { + return main(); + } + + actor::act(extractions, files, variables)?; + + Ok(()) +} diff --git a/src/cheat_variable.rs b/src/commands/func/map.rs similarity index 64% rename from src/cheat_variable.rs rename to src/commands/func/map.rs index 3f6399d..89e22c5 100644 --- a/src/cheat_variable.rs +++ b/src/commands/func/map.rs @@ -1,8 +1,7 @@ -use crate::shell::{self, ShellSpawnError}; +use crate::common::shell::{self, ShellSpawnError}; +use crate::prelude::*; -use anyhow::Result; - -pub fn map_expand() -> Result<()> { +pub fn expand() -> Result<()> { let cmd = r#"sed -e 's/^.*$/"&"/' | tr '\n' ' '"#; shell::out() .arg(cmd) diff --git a/src/commands/func/mod.rs b/src/commands/func/mod.rs new file mode 100644 index 0000000..383b636 --- /dev/null +++ b/src/commands/func/mod.rs @@ -0,0 +1,65 @@ +mod map; +mod widget; + +use super::core; +use super::temp; +use crate::common::url; +use crate::prelude::*; +use clap::Args; +use clap::Parser; + +const FUNC_POSSIBLE_VALUES: &[&str] = &[ + "url::open", + "welcome", + "widget::last_command", + "map::expand", + "temp", +]; + +impl FromStr for Func { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "url::open" => Ok(Func::UrlOpen), + "welcome" => Ok(Func::Welcome), + "widget::last_command" => Ok(Func::WidgetLastCommand), + "map::expand" => Ok(Func::MapExpand), + "temp" => Ok(Func::Temp), + _ => Err("no match"), + } + } +} + +#[derive(Debug, Clone, Parser)] +pub enum Func { + UrlOpen, + Welcome, + WidgetLastCommand, + MapExpand, + Temp, +} + +#[derive(Debug, Clone, Args)] +pub struct Input { + /// Function name (example: "url::open") + #[clap(possible_values = FUNC_POSSIBLE_VALUES, ignore_case = true)] + pub func: Func, + /// List of arguments (example: "https://google.com") + pub args: Vec, +} + +impl Runnable for Input { + fn run(&self) -> Result<()> { + let func = &self.func; + let args = self.args.clone(); // TODO + + match func { + Func::UrlOpen => url::open(args), + Func::Welcome => core::main(), + Func::WidgetLastCommand => widget::last_command(), + Func::MapExpand => map::expand(), + Func::Temp => temp::main(), + } + } +} diff --git a/src/commands/func/widget.rs b/src/commands/func/widget.rs new file mode 100644 index 0000000..ff74d74 --- /dev/null +++ b/src/commands/func/widget.rs @@ -0,0 +1,40 @@ +use crate::prelude::*; +use std::io::{self, Read}; + +pub fn 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 && p != format!("{}{}", pattern, 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(()) +} diff --git a/src/commands/info.rs b/src/commands/info.rs new file mode 100644 index 0000000..6b69f80 --- /dev/null +++ b/src/commands/info.rs @@ -0,0 +1,48 @@ +use clap::Args; + +use crate::filesystem; +use crate::prelude::*; + +const INFO_POSSIBLE_VALUES: &[&str] = &["cheats-example", "cheats-path", "config-path", "config-example"]; + +impl FromStr for Info { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "cheats-example" => Ok(Info::CheatsExample), + "cheats-path" => Ok(Info::CheatsPath), + "config-example" => Ok(Info::ConfigExample), + "config-path" => Ok(Info::ConfigPath), + _ => Err("no match"), + } + } +} + +#[derive(Debug, Clone, Args)] +pub struct Input { + #[clap(possible_values = INFO_POSSIBLE_VALUES, ignore_case = true)] + pub info: Info, +} + +#[derive(Debug, Clone)] +pub enum Info { + CheatsExample, + CheatsPath, + ConfigPath, + ConfigExample, +} + +impl Runnable for Input { + fn run(&self) -> Result<()> { + let info = &self.info; + + match info { + Info::CheatsExample => println!("{}", include_str!("../../docs/cheat_example.cheat")), + Info::CheatsPath => println!("{}", &filesystem::default_cheat_pathbuf()?.to_string()), + Info::ConfigPath => println!("{}", &filesystem::default_config_pathbuf()?.to_string()), + Info::ConfigExample => println!("{}", include_str!("../../docs/config_file_example.yaml")), + } + Ok(()) + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..783cbad --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,39 @@ +pub mod core; +pub mod func; +pub mod info; +pub mod preview; +pub mod repo; +pub mod shell; +pub mod temp; + +use crate::commands; +use crate::prelude::*; + +pub fn handle() -> Result<()> { + use crate::config::Command::*; + + match CONFIG.cmd() { + None => commands::core::main(), + + Some(c) => match c { + Preview(input) => input.run(), + + PreviewVarStdin(input) => input.run(), + + PreviewVar(input) => input.run(), + + Widget(input) => input.run().context("Failed to print shell widget code"), + + Fn(input) => input + .run() + .with_context(|| format!("Failed to execute function `{:#?}`", input.func)), + + Info(input) => input + .run() + .with_context(|| format!("Failed to fetch info `{:#?}`", input.info)), + + #[cfg(not(feature = "disable-repo-management"))] + Repo(input) => input.run(), + }, + } +} diff --git a/src/commands/preview/mod.rs b/src/commands/preview/mod.rs new file mode 100644 index 0000000..11fa1fc --- /dev/null +++ b/src/commands/preview/mod.rs @@ -0,0 +1,39 @@ +use crate::prelude::*; +use crate::serializer; +use clap::Args; +use crossterm::style::{style, Stylize}; +use std::process; + +pub mod var; +pub mod var_stdin; + +#[derive(Debug, Clone, Args)] +pub struct Input { + /// Selection line + pub line: String, +} + +fn extract_elements(argstr: &str) -> Result<(&str, &str, &str)> { + let mut parts = argstr.split(serializer::DELIMITER).skip(3); + let tags = parts.next().context("No `tags` element provided.")?; + let comment = parts.next().context("No `comment` element provided.")?; + let snippet = parts.next().context("No `snippet` element provided.")?; + Ok((tags, comment, snippet)) +} + +impl Runnable for Input { + fn run(&self) -> Result<()> { + let line = &self.line; + + let (tags, comment, snippet) = extract_elements(line)?; + + println!( + "{comment} {tags} \n{snippet}", + comment = style(comment).with(CONFIG.comment_color()), + tags = style(format!("[{}]", tags)).with(CONFIG.tag_color()), + snippet = style(serializer::fix_newlines(snippet)).with(CONFIG.snippet_color()), + ); + + process::exit(0) + } +} diff --git a/src/commands/preview/var.rs b/src/commands/preview/var.rs new file mode 100644 index 0000000..e99dcce --- /dev/null +++ b/src/commands/preview/var.rs @@ -0,0 +1,106 @@ +use crate::env_var; +use crate::finder; +use crate::prelude::*; +use crate::serializer; +use clap::Args; +use crossterm::style::style; +use crossterm::style::Stylize; +use std::iter; +use std::process; + +#[derive(Debug, Clone, Args)] +pub struct Input { + /// Selection line + pub selection: String, + /// Query match + pub query: String, + /// Typed text + pub variable: String, +} + +impl Runnable for Input { + fn run(&self) -> Result<()> { + let selection = &self.selection; + let query = &self.query; + let variable = &self.variable; + + let snippet = env_var::must_get(env_var::PREVIEW_INITIAL_SNIPPET); + let tags = env_var::must_get(env_var::PREVIEW_TAGS); + let comment = env_var::must_get(env_var::PREVIEW_COMMENT); + let column = env_var::parse(env_var::PREVIEW_COLUMN); + let delimiter = env_var::get(env_var::PREVIEW_DELIMITER).ok(); + let map = env_var::get(env_var::PREVIEW_MAP).ok(); + + let active_color = CONFIG.tag_color(); + let inactive_color = CONFIG.comment_color(); + + let mut colored_snippet = String::from(&snippet); + let mut visited_vars: HashSet<&str> = HashSet::new(); + + let mut variables = String::from(""); + + println!( + "{comment} {tags}", + comment = style(comment).with(CONFIG.comment_color()), + tags = style(format!("[{}]", tags)).with(CONFIG.tag_color()), + ); + + let bracketed_current_variable = format!("<{}>", variable); + + let bracketed_variables: Vec<&str> = { + if snippet.contains(&bracketed_current_variable) { + serializer::VAR_REGEX + .find_iter(&snippet) + .map(|m| m.as_str()) + .collect() + } else { + iter::once(&bracketed_current_variable) + .map(|s| s.as_str()) + .collect() + } + }; + + for bracketed_variable_name in bracketed_variables { + let variable_name = &bracketed_variable_name[1..bracketed_variable_name.len() - 1]; + + if visited_vars.contains(variable_name) { + continue; + } else { + visited_vars.insert(variable_name); + } + + let is_current = variable_name == variable; + let variable_color = if is_current { active_color } else { inactive_color }; + let env_variable_name = env_var::escape(variable_name); + + let value = if is_current { + let v = selection.trim_matches('\''); + if v.is_empty() { query.trim_matches('\'') } else { v }.to_string() + } else if let Ok(v) = env_var::get(&env_variable_name) { + v + } else { + "".to_string() + }; + + let replacement = format!( + "{variable}", + variable = style(bracketed_variable_name).with(variable_color), + ); + + colored_snippet = colored_snippet.replace(bracketed_variable_name, &replacement); + + variables = format!( + "{variables}\n{variable} = {value}", + variables = variables, + variable = style(variable_name).with(variable_color), + value = finder::process(value, column, delimiter.as_deref(), map.clone()) + .expect("Unable to process value"), + ); + } + + println!("{snippet}", snippet = serializer::fix_newlines(&colored_snippet)); + println!("{variables}", variables = variables); + + process::exit(0) + } +} diff --git a/src/commands/preview/var_stdin.rs b/src/commands/preview/var_stdin.rs new file mode 100755 index 0000000..4b8ddfb --- /dev/null +++ b/src/commands/preview/var_stdin.rs @@ -0,0 +1,43 @@ +use clap::Args; + +use super::var; +use crate::common::shell::{self, ShellSpawnError, EOF}; +use crate::prelude::*; +use std::io::{self, Read}; + +#[derive(Debug, Clone, Args)] +pub struct Input {} + +impl Runnable for Input { + fn run(&self) -> Result<()> { + let mut text = String::new(); + io::stdin().read_to_string(&mut text)?; + + let mut parts = text.split(EOF); + let selection = parts.next().expect("Unable to get selection").to_owned(); + let query = parts.next().expect("Unable to get query").to_owned(); + let variable = parts.next().expect("Unable to get variable").trim().to_owned(); + + let input = var::Input { + selection, + query, + variable, + }; + + input.run()?; + + if let Some(extra) = parts.next() { + if !extra.is_empty() { + print!(""); + + shell::out() + .arg(extra) + .spawn() + .map_err(|e| ShellSpawnError::new(extra, e))? + .wait()?; + } + } + + Ok(()) + } +} diff --git a/src/handler/repo_add.rs b/src/commands/repo/add.rs similarity index 74% rename from src/handler/repo_add.rs rename to src/commands/repo/add.rs index 8125da2..20d481e 100644 --- a/src/handler/repo_add.rs +++ b/src/commands/repo/add.rs @@ -1,13 +1,9 @@ -use crate::config::CONFIG; +use crate::common::git; use crate::filesystem; use crate::finder::structures::{Opts as FinderOpts, SuggestionType}; -use crate::finder::{Finder, FinderChoice}; -use crate::fs::pathbuf_to_string; -use crate::git; -use anyhow::Context; -use anyhow::Result; +use crate::finder::FinderChoice; +use crate::prelude::*; use std::fs; -use std::io::Write; use std::path; fn ask_if_should_import_all(finder: &FinderChoice) -> Result { @@ -17,20 +13,16 @@ fn ask_if_should_import_all(finder: &FinderChoice) -> Result { ..Default::default() }; - let (response, _, _) = finder - .call(opts, |stdin, _| { + let (response, _) = finder + .call(opts, |stdin| { stdin .write_all(b"Yes\nNo") .context("Unable to writer alternatives")?; - Ok(None) + Ok(()) }) .context("Unable to get response")?; - if response.to_lowercase().starts_with('y') { - Ok(true) - } else { - Ok(false) - } + Ok(response.to_lowercase().starts_with('y')) } pub fn main(uri: String) -> Result<()> { @@ -41,14 +33,14 @@ pub fn main(uri: String) -> Result<()> { let cheat_pathbuf = filesystem::default_cheat_pathbuf()?; let tmp_pathbuf = filesystem::tmp_pathbuf()?; - let tmp_path_str = pathbuf_to_string(&tmp_pathbuf)?; + let tmp_path_str = &tmp_pathbuf.to_string(); let _ = filesystem::remove_dir(&tmp_pathbuf); filesystem::create_dir(&tmp_pathbuf)?; eprintln!("Cloning {} into {}...\n", &actual_uri, &tmp_path_str); - git::shallow_clone(actual_uri.as_str(), &tmp_path_str) + git::shallow_clone(actual_uri.as_str(), tmp_path_str) .with_context(|| format!("Failed to clone `{}`", actual_uri))?; let all_files = filesystem::all_cheat_files(&tmp_pathbuf).join("\n"); @@ -64,12 +56,12 @@ pub fn main(uri: String) -> Result<()> { let files = if should_import_all { all_files } else { - let (files, _, _) = finder - .call(opts, |stdin, _| { + let (files, _) = finder + .call(opts, |stdin| { stdin .write_all(all_files.as_bytes()) .context("Unable to prompt cheats to import")?; - Ok(None) + Ok(()) }) .context("Failed to get cheatsheet files from finder")?; files @@ -96,13 +88,8 @@ pub fn main(uri: String) -> Result<()> { p }; fs::create_dir_all(&to_folder).unwrap_or(()); - fs::copy(&from, &to).with_context(|| { - format!( - "Failed to copy `{}` to `{}`", - pathbuf_to_string(&from).expect("unable to parse {from}"), - pathbuf_to_string(&to).expect("unable to parse {to}") - ) - })?; + fs::copy(&from, &to) + .with_context(|| format!("Failed to copy `{}` to `{}`", &from.to_string(), &to.to_string()))?; } filesystem::remove_dir(&tmp_pathbuf)?; @@ -110,7 +97,7 @@ pub fn main(uri: String) -> Result<()> { eprintln!( "The following .cheat files were imported successfully:\n{}\n\nThey are now located at {}", files, - pathbuf_to_string(&to_folder)? + to_folder.to_string() ); Ok(()) diff --git a/src/handler/repo_browse.rs b/src/commands/repo/browse.rs similarity index 76% rename from src/handler/repo_browse.rs rename to src/commands/repo/browse.rs index aeb2aed..2540772 100644 --- a/src/handler/repo_browse.rs +++ b/src/commands/repo/browse.rs @@ -1,13 +1,9 @@ -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 crate::common::git; +use crate::prelude::*; use std::fs; -use std::io::Write; pub fn main() -> Result { let finder = CONFIG.finder(); @@ -18,13 +14,13 @@ pub fn main() -> Result { p }; - let repo_path_str = pathbuf_to_string(&repo_pathbuf)?; + let repo_path_str = &repo_pathbuf.to_string(); 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) + git::shallow_clone(repo_url.as_str(), repo_path_str) .with_context(|| format!("Failed to clone `{}`", repo_url))?; let feature_repos_file = { @@ -41,12 +37,12 @@ pub fn main() -> Result { ..Default::default() }; - let (repo, _, _) = finder - .call(opts, |stdin, _| { + let (repo, _) = finder + .call(opts, |stdin| { stdin .write_all(repos.as_bytes()) .context("Unable to prompt featured repositories")?; - Ok(None) + Ok(()) }) .context("Failed to get repo URL from finder")?; diff --git a/src/commands/repo/mod.rs b/src/commands/repo/mod.rs new file mode 100644 index 0000000..385eadf --- /dev/null +++ b/src/commands/repo/mod.rs @@ -0,0 +1,41 @@ +use crate::commands; +use crate::prelude::*; +use clap::{Args, Subcommand}; + +pub mod add; +pub mod browse; + +#[derive(Debug, Clone, Subcommand)] +pub enum RepoCommand { + /// Imports cheatsheets from a repo + Add { + /// A URI to a git repository containing .cheat files ("user/repo" will download cheats from github.com/user/repo) + uri: String, + }, + /// Browses for featured cheatsheet repos + Browse, +} + +#[derive(Debug, Clone, Args)] +pub struct Input { + #[clap(subcommand)] + pub cmd: RepoCommand, +} + +impl Runnable for Input { + fn run(&self) -> Result<()> { + match &self.cmd { + RepoCommand::Add { uri } => { + add::main(uri.clone()) + .with_context(|| format!("Failed to import cheatsheets from `{}`", uri))?; + commands::core::main() + } + RepoCommand::Browse => { + let repo = browse::main().context("Failed to browse featured cheatsheets")?; + add::main(repo.clone()) + .with_context(|| format!("Failed to import cheatsheets from `{}`", repo))?; + commands::core::main() + } + } + } +} diff --git a/src/commands/shell.rs b/src/commands/shell.rs new file mode 100644 index 0000000..60d70b2 --- /dev/null +++ b/src/commands/shell.rs @@ -0,0 +1,43 @@ +use clap::Args; + +use crate::common::shell::Shell; +use crate::prelude::*; + +const WIDGET_POSSIBLE_VALUES: &[&str] = &["bash", "zsh", "fish", "elvish"]; + +impl FromStr for Shell { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "bash" => Ok(Shell::Bash), + "zsh" => Ok(Shell::Zsh), + "fish" => Ok(Shell::Fish), + "elvish" => Ok(Shell::Elvish), + _ => Err("no match"), + } + } +} + +#[derive(Debug, Clone, Args)] +pub struct Input { + #[clap(possible_values = WIDGET_POSSIBLE_VALUES, ignore_case = true, default_value = "bash")] + pub shell: Shell, +} + +impl Runnable for Input { + fn run(&self) -> Result<()> { + let shell = &self.shell; + + let content = match shell { + Shell::Bash => include_str!("../../shell/navi.plugin.bash"), + Shell::Zsh => include_str!("../../shell/navi.plugin.zsh"), + Shell::Fish => include_str!("../../shell/navi.plugin.fish"), + Shell::Elvish => include_str!("../../shell/navi.plugin.elv"), + }; + + println!("{}", content); + + Ok(()) + } +} diff --git a/src/commands/temp.rs b/src/commands/temp.rs new file mode 100644 index 0000000..35a1ac3 --- /dev/null +++ b/src/commands/temp.rs @@ -0,0 +1,64 @@ +use crate::common::shell::{self, ShellSpawnError}; +use crate::finder::structures::Opts as FinderOpts; +use crate::parser::Parser; +use crate::{prelude::*, serializer}; +use std::io::{self, Write}; + +pub fn main() -> Result<()> { + let config = &CONFIG; + let _opts = FinderOpts::snippet_default(); + + let fetcher = config.fetcher(); + let hash: u64 = 2087294461664323320; + + let mut buf = vec![]; + let mut parser = Parser::new(&mut buf, false); + parser.set_hash(hash); + + let _res = fetcher + .fetch(&mut parser) + .context("Failed to parse variables intended for finder")?; + + let variables = parser.variables; + let item_str = String::from_utf8(buf)?; + let item = serializer::raycast_deser(&item_str)?; + dbg!(&item); + + let x = variables.get_suggestion(&item.tags, "local_branch").expect("foo"); + dbg!(&x); + + let suggestion_command = x.0.clone(); + let child = shell::out() + .stdout(Stdio::piped()) + .arg(&suggestion_command) + .spawn() + .map_err(|e| ShellSpawnError::new(suggestion_command, e))?; + + let text = String::from_utf8( + child + .wait_with_output() + .context("Failed to wait and collect output from bash")? + .stdout, + ) + .context("Suggestions are invalid utf8")?; + + dbg!(&text); + + Ok(()) +} + +pub fn _main0() -> Result<()> { + let config = &CONFIG; + + let fetcher = config.fetcher(); + + let mut stdout = io::stdout(); + let mut writer: Box<&mut dyn Write> = Box::new(&mut stdout); + let mut parser = Parser::new(&mut writer, false); + + let _res = fetcher + .fetch(&mut parser) + .context("Failed to parse variables intended for finder")?; + + Ok(()) +} diff --git a/src/clipboard.rs b/src/common/clipboard.rs similarity index 89% rename from src/clipboard.rs rename to src/common/clipboard.rs index 89d19e5..0e81c72 100644 --- a/src/clipboard.rs +++ b/src/common/clipboard.rs @@ -1,5 +1,5 @@ -use crate::shell::{self, ShellSpawnError, EOF}; -use anyhow::Result; +use crate::common::shell::{self, ShellSpawnError, EOF}; +use crate::prelude::*; pub fn copy(text: String) -> Result<()> { let cmd = r#" diff --git a/src/common/component.rs.bkp b/src/common/component.rs.bkp new file mode 100644 index 0000000..4ffa9d5 --- /dev/null +++ b/src/common/component.rs.bkp @@ -0,0 +1,21 @@ +use super::prelude::*; + +pub trait Component: Any + AsAny + Send + Sync {} + +pub trait AsAny: Any { + fn as_any(&self) -> &dyn Any; + fn as_mut_any(&mut self) -> &mut dyn Any; +} + +impl AsAny for T +where + T: Any, +{ + fn as_any(&self) -> &dyn Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn Any { + self + } +} diff --git a/src/common/deps.rs.bkp b/src/common/deps.rs.bkp new file mode 100644 index 0000000..e9624fc --- /dev/null +++ b/src/common/deps.rs.bkp @@ -0,0 +1,7 @@ +use super::prelude::*; + +pub trait HasDeps { + fn deps(&self) -> HashSet { + HashSet::new() + } +} diff --git a/src/common/deser.rs.bkp b/src/common/deser.rs.bkp new file mode 100644 index 0000000..37d69b6 --- /dev/null +++ b/src/common/deser.rs.bkp @@ -0,0 +1,50 @@ +use super::prelude::*; +use serde::de::DeserializeOwned; + +#[cfg(feature = "yaml")] +pub fn yaml_from_path(path: &Path) -> Result +where + T: DeserializeOwned, +{ + let file = File::open(path)?; + let reader = BufReader::new(file); + let config: T = serde_yaml::from_reader(reader)?; // TODO: show path + original error message? + Ok(config) +} + +#[cfg(feature = "json")] +pub fn json_from_path(path: &Path) -> Result +where + T: DeserializeOwned, +{ + let file = File::open(path)?; + let reader = BufReader::new(file); + let config: T = serde_json::from_reader(reader)?; // TODO: show path + original error message? + Ok(config) +} + +#[cfg(feature = "yaml")] +pub fn yaml_from_str(text: &str) -> Result +where + T: DeserializeOwned, +{ + serde_yaml::from_str(text) + .with_context(|| format!("Failed to deserialize into yaml:\n{}", text)) +} + +#[cfg(feature = "json")] +pub fn json_from_str(text: &str) -> Result +where + T: DeserializeOwned, +{ + serde_json::from_str(text) + .with_context(|| format!("Failed to deserialize into json:\n{}", text)) +} + +#[cfg(feature = "yaml")] +pub fn to_yaml_str(t: &T) -> Result +where + T: Serialize, +{ + serde_yaml::to_string(t).with_context(|| "Failed to serialize into yaml") // TODO: debug struct? +} diff --git a/src/fs.rs b/src/common/fs.rs similarity index 78% rename from src/fs.rs rename to src/common/fs.rs index 5fd2b4d..89f6275 100644 --- a/src/fs.rs +++ b/src/common/fs.rs @@ -1,12 +1,34 @@ -use anyhow::{Context, Error, Result}; - +use super::prelude::*; use remove_dir_all::remove_dir_all; -use std::fmt::Debug; +use std::ffi::OsStr; use std::fs::{self, create_dir_all, File}; -use std::io::{self, BufRead}; -use std::path::{Path, PathBuf}; +use std::io; use thiserror::Error; +pub trait ToStringExt { + fn to_string(&self) -> String; +} + +impl ToStringExt for Path { + fn to_string(&self) -> String { + self.to_string_lossy().to_string() + } +} + +impl ToStringExt for OsStr { + fn to_string(&self) -> String { + self.to_string_lossy().to_string() + } +} + +// pub fn config_dir(project_name: &str) -> Result { +// let base_dirs = BaseDirs::new().context("unable to get base dirs")?; +// +// let mut pathbuf = PathBuf::from(base_dirs.config_dir()); +// pathbuf.push(project_name); +// Ok(pathbuf) +// } + #[derive(Error, Debug)] #[error("Invalid path `{0}`")] pub struct InvalidPath(pub PathBuf); @@ -21,7 +43,7 @@ pub struct UnreadableDir { pub fn open(filename: &Path) -> Result { File::open(filename).with_context(|| { - let x = pathbuf_to_string(filename).unwrap_or_else(|e| format!("Unable to get path string: {}", e)); + let x = filename.to_string(); format!("Failed to open file {}", &x) }) } diff --git a/src/git.rs b/src/common/git.rs similarity index 96% rename from src/git.rs rename to src/common/git.rs index d239246..6c41692 100644 --- a/src/git.rs +++ b/src/common/git.rs @@ -1,5 +1,5 @@ -use crate::shell::ShellSpawnError; -use anyhow::{Context, Result}; +use crate::common::shell::ShellSpawnError; +use crate::prelude::*; use std::process::Command; pub fn shallow_clone(uri: &str, target: &str) -> Result<()> { diff --git a/src/hash.rs b/src/common/hash.rs similarity index 100% rename from src/hash.rs rename to src/common/hash.rs diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..1ea86d1 --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,13 @@ +// pub mod component; +// pub mod deps; +// pub mod deser; +pub mod clipboard; +pub mod fs; +pub mod git; +pub mod hash; +pub mod prelude; +pub mod shell; +pub mod terminal; +pub mod url; +// pub mod system; +// pub mod tracing; diff --git a/src/common/prelude.rs b/src/common/prelude.rs new file mode 100644 index 0000000..7393924 --- /dev/null +++ b/src/common/prelude.rs @@ -0,0 +1,29 @@ +// pub use super::component::Component; +// pub use super::deps::HasDeps; +pub use super::fs::ToStringExt; +pub use anyhow::{anyhow, Context, Error, Result}; +pub use serde::de::Deserializer; +pub use serde::ser::Serializer; +pub use serde::{Deserialize, Serialize}; +pub use std::any::{Any, TypeId}; +pub use std::collections::{HashMap, HashSet}; +pub use std::convert::{TryFrom, TryInto}; +pub use std::fmt::Debug; +pub use std::fs::File; +pub use std::io::{BufRead, BufReader}; +pub use std::path::{Path, PathBuf}; +pub use std::process::Stdio; +pub use std::str::FromStr; +pub use std::sync::{Arc, Mutex, RwLock}; +// pub use tracing::{self, debug, error, event, info, instrument, span, trace, warn}; +/* +pub extern crate anyhow; +pub extern crate serde; +pub extern crate tracing_subscriber; + +#[cfg(feature = "yaml")] +pub extern crate serde_yaml; + +#[cfg(feature = "json")] +pub extern crate serde_json; + */ diff --git a/src/common/shell.rs b/src/common/shell.rs new file mode 100644 index 0000000..e489f42 --- /dev/null +++ b/src/common/shell.rs @@ -0,0 +1,45 @@ +use crate::prelude::*; +use std::process::Command; +use thiserror::Error; + +pub const EOF: &str = "NAVIEOF"; + +#[derive(Debug, Clone)] +pub enum Shell { + Bash, + Zsh, + Fish, + Elvish, +} + +#[derive(Error, Debug)] +#[error("Failed to spawn child process `bash` to execute `{command}`")] +pub struct ShellSpawnError { + command: String, + #[source] + source: anyhow::Error, +} + +impl ShellSpawnError { + pub fn new(command: impl Into, source: SourceError) -> Self + where + SourceError: std::error::Error + Sync + Send + 'static, + { + ShellSpawnError { + command: command.into(), + source: source.into(), + } + } +} + +pub fn out() -> Command { + let words_str = CONFIG.shell(); + let mut words_vec = shellwords::split(&words_str).expect("empty shell command"); + let mut words = words_vec.iter_mut(); + let first_cmd = words.next().expect("absent shell binary"); + let mut cmd = Command::new(&first_cmd); + cmd.args(words); + let dash_c = if words_str.contains("cmd.exe") { "/c" } else { "-c" }; + cmd.arg(dash_c); + cmd +} diff --git a/src/common/system.rs.bkp b/src/common/system.rs.bkp new file mode 100644 index 0000000..c0813fd --- /dev/null +++ b/src/common/system.rs.bkp @@ -0,0 +1,57 @@ +use super::prelude::*; +use std::collections::hash_map::Entry; + +pub struct System { + pub config: Arc, + components: HashMap>, + type_ids: Option>, +} + +impl System { + pub fn new(config: C) -> Result { + Ok(System { + config: Arc::new(config), + components: HashMap::new(), + type_ids: None, + }) + } + + pub fn get(&self) -> Result<&T> + where + T: Component, + { + let type_id = TypeId::of::(); + let c = self.components.get(&type_id).unwrap(); + c.as_any().downcast_ref::().context("invalid component") + } + + pub fn set_type_ids(&mut self, type_ids: HashSet) { + self.type_ids = Some(type_ids); + } + + pub fn maybe_add Result>( + &mut self, + type_id: &TypeId, + f: F, + ) -> Result>> { + let should_init = self + .type_ids + .as_ref() + .context("system has no typeIds")? + .contains(type_id); + + if !should_init { + Ok(None) + } else { + let component = f(self)?; + let arc = Arc::new(component); + let entry = self.components.entry(*type_id); + if let Entry::Vacant(e) = entry { + e.insert(arc.clone()); + Ok(Some(arc)) + } else { + Err(anyhow!("typeId already included in component map")) + } + } + } +} diff --git a/src/common/terminal.rs b/src/common/terminal.rs new file mode 100644 index 0000000..bf7fb8c --- /dev/null +++ b/src/common/terminal.rs @@ -0,0 +1,65 @@ +use crate::prelude::*; +use crossterm::style; +use crossterm::terminal; + +use std::process::Command; + +const FALLBACK_WIDTH: u16 = 80; + +fn width_with_shell_out() -> Result { + let output = if cfg!(target_os = "macos") { + Command::new("stty") + .arg("-f") + .arg("/dev/stderr") + .arg("size") + .stderr(Stdio::inherit()) + .output()? + } else { + Command::new("stty") + .arg("size") + .arg("-F") + .arg("/dev/stderr") + .stderr(Stdio::inherit()) + .output()? + }; + + 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::() + .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().unwrap_or(FALLBACK_WIDTH) + } +} + +pub fn parse_ansi(ansi: &str) -> Option { + 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 { + if let Some(c) = parse_ansi(ansi) { + Ok(Color(c)) + } else { + Err("Invalid color") + } + } +} diff --git a/src/common/tracing.rs.bkp b/src/common/tracing.rs.bkp new file mode 100644 index 0000000..bf3f02c --- /dev/null +++ b/src/common/tracing.rs.bkp @@ -0,0 +1,36 @@ +use std::env; + +use super::prelude::*; + +static RUST_LOG: &str = "RUST_LOG"; + +#[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct TracingConfig { + pub time: bool, + pub level: String, +} + +pub fn init(config: Option<&TracingConfig>) { + if let Some(tracing) = config { + let level_is_set = match env::var(RUST_LOG) { + Err(_) => false, + Ok(v) => !v.is_empty(), + }; + + if !level_is_set { + env::set_var(RUST_LOG, &tracing.level); + } + + let t = tracing_subscriber::fmt(); + let res = if tracing.time { + t.try_init() + } else { + t.without_time().try_init() + }; + + if let Err(e) = res { + error!("unable to set tracing subscriber: {}", e); + } + } +} diff --git a/src/url.rs b/src/common/url.rs similarity index 91% rename from src/url.rs rename to src/common/url.rs index ceca51f..b5b820d 100644 --- a/src/url.rs +++ b/src/common/url.rs @@ -1,4 +1,5 @@ -use crate::shell::{self, ShellSpawnError}; +use crate::common::shell::{self, ShellSpawnError}; +use crate::prelude::*; use anyhow::Result; use shell::EOF; diff --git a/src/config/cli.rs b/src/config/cli.rs index 3a62aca..9a8c4db 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -1,58 +1,9 @@ +use crate::commands; use crate::finder::FinderChoice; -use crate::handler::func::Func; -use crate::handler::info::Info; -use crate::shell::Shell; - +use crate::prelude::*; use clap::{crate_version, AppSettings, Parser, Subcommand}; -use std::str::FromStr; - const FINDER_POSSIBLE_VALUES: &[&str] = &["fzf", "skim"]; -const WIDGET_POSSIBLE_VALUES: &[&str] = &["bash", "zsh", "fish", "elvish"]; -const FUNC_POSSIBLE_VALUES: &[&str] = &["url::open", "welcome", "widget::last_command", "map::expand"]; -const INFO_POSSIBLE_VALUES: &[&str] = &["cheats-example", "cheats-path", "config-path", "config-example"]; - -impl FromStr for Shell { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s { - "bash" => Ok(Shell::Bash), - "zsh" => Ok(Shell::Zsh), - "fish" => Ok(Shell::Fish), - "elvish" => Ok(Shell::Elvish), - _ => Err("no match"), - } - } -} - -impl FromStr for Func { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s { - "url::open" => Ok(Func::UrlOpen), - "welcome" => Ok(Func::Welcome), - "widget::last_command" => Ok(Func::WidgetLastCommand), - "map::expand" => Ok(Func::MapExpand), - _ => Err("no match"), - } - } -} - -impl FromStr for Info { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s { - "cheats-example" => Ok(Info::CheatsExample), - "cheats-path" => Ok(Info::CheatsPath), - "config-example" => Ok(Info::ConfigExample), - "config-path" => Ok(Info::ConfigPath), - _ => Err("no match"), - } - } -} #[derive(Debug, Parser)] #[clap(after_help = "\x1b[0;33mMORE INFO:\x1b[0;0m @@ -142,68 +93,34 @@ impl ClapConfig { } } -#[derive(Debug, Parser)] +// #[derive(Subcommand, Debug, Clone, Runnable, HasDeps)] +#[derive(Subcommand, Debug, Clone)] pub enum Command { /// [Experimental] Calls internal functions - Fn { - /// Function name (example: "url::open") - #[clap(possible_values = FUNC_POSSIBLE_VALUES, ignore_case = true)] - func: Func, - /// List of arguments (example: "https://google.com") - args: Vec, - }, + Fn(commands::func::Input), /// Manages cheatsheet repositories #[cfg(not(feature = "disable-repo-management"))] - Repo { - #[clap(subcommand)] - cmd: RepoCommand, - }, + Repo(commands::repo::Input), /// Used for fzf's preview window when selecting snippets #[clap(setting = AppSettings::Hidden)] - Preview { - /// Selection line - line: String, - }, + Preview(commands::preview::Input), /// Used for fzf's preview window when selecting variable suggestions #[clap(setting = AppSettings::Hidden)] - PreviewVar { - /// Selection line - selection: String, - /// Query match - query: String, - /// Typed text - variable: String, - }, + PreviewVar(commands::preview::var::Input), /// Used for fzf's preview window when selecting variable suggestions #[clap(setting = AppSettings::Hidden)] - PreviewVarStdin, + PreviewVarStdin(commands::preview::var_stdin::Input), /// Outputs shell widget source code - Widget { - #[clap(possible_values = WIDGET_POSSIBLE_VALUES, ignore_case = true, default_value = "bash")] - shell: Shell, - }, + Widget(commands::shell::Input), /// Shows info - Info { - #[clap(possible_values = INFO_POSSIBLE_VALUES, ignore_case = true)] - info: Info, - }, -} - -#[derive(Debug, Subcommand)] -pub enum RepoCommand { - /// Imports cheatsheets from a repo - Add { - /// A URI to a git repository containing .cheat files ("user/repo" will download cheats from github.com/user/repo) - uri: String, - }, - /// Browses for featured cheatsheet repos - Browse, + Info(commands::info::Input), } pub enum Source { - Filesystem(Option, Option), + Filesystem(Option), Tldr(String), Cheats(String), + Welcome, } pub enum Action { @@ -211,6 +128,7 @@ pub enum Action { Execute, } +/* #[cfg(test)] mod tests { use super::*; @@ -243,3 +161,4 @@ mod tests { } } } +*/ diff --git a/src/config/env.rs b/src/config/env.rs index cf1c017..7afd27e 100644 --- a/src/config/env.rs +++ b/src/config/env.rs @@ -1,8 +1,6 @@ use crate::env_var; - use crate::finder::FinderChoice; - -use std::str::FromStr; +use crate::prelude::*; pub struct EnvConfig { pub config_yaml: Option, diff --git a/src/config/mod.rs b/src/config/mod.rs index 7c3eb29..43f97ca 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -2,11 +2,17 @@ mod cli; mod env; mod yaml; +use crate::clients::cheatsh; +use crate::clients::tldr; + +use crate::commands::func::Func; +use crate::config::Source; +use crate::filesystem; use crate::finder::FinderChoice; - -use crate::terminal::style::Color; +use crate::structures::fetcher::Fetcher; +use crate::welcome; pub use cli::*; - +use crossterm::style::Color; use env::EnvConfig; use yaml::YamlConfig; @@ -45,8 +51,23 @@ impl Config { Source::Tldr(query) } else if let Some(query) = self.clap.cheatsh.clone() { Source::Cheats(query) + } else if let Some(Command::Fn(input)) = self.cmd() { + if let Func::Welcome = input.func { + Source::Welcome + } else { + Source::Filesystem(self.path()) + } } else { - Source::Filesystem(self.path(), self.tag_rules()) + Source::Filesystem(self.path()) + } + } + + pub fn fetcher(&self) -> Box { + match self.source() { + Source::Cheats(query) => Box::new(cheatsh::Fetcher::new(query)), + Source::Tldr(query) => Box::new(tldr::Fetcher::new(query)), + Source::Filesystem(path) => Box::new(filesystem::Fetcher::new(path)), + Source::Welcome => Box::new(welcome::Fetcher::new()), } } diff --git a/src/config/yaml.rs b/src/config/yaml.rs index 581787e..ac59549 100644 --- a/src/config/yaml.rs +++ b/src/config/yaml.rs @@ -1,15 +1,10 @@ use super::env::EnvConfig; +use crate::common::fs; use crate::filesystem::default_config_pathbuf; use crate::finder::FinderChoice; -use crate::fs; -use crate::terminal::style::Color as TerminalColor; -use anyhow::Result; -use serde::{de, Deserialize}; -use std::convert::TryFrom; -use std::io::BufReader; -use std::path::Path; -use std::path::PathBuf; -use std::str::FromStr; +use crate::prelude::*; +use crossterm::style::Color as TerminalColor; +use serde::de; #[derive(Deserialize)] pub struct Color(#[serde(deserialize_with = "color_deserialize")] TerminalColor); diff --git a/src/env_var.rs b/src/env_var.rs index 6aea2a3..cd1c3a0 100644 --- a/src/env_var.rs +++ b/src/env_var.rs @@ -1,8 +1,8 @@ +use crate::prelude::*; pub use env::remove_var as remove; pub use env::set_var as set; pub use env::var as get; use std::env; -use std::str::FromStr; pub const PREVIEW_INITIAL_SNIPPET: &str = "NAVI_PREVIEW_INITIAL_SNIPPET"; pub const PREVIEW_TAGS: &str = "NAVI_PREVIEW_TAGS"; diff --git a/src/filesystem.rs b/src/filesystem.rs index d6e9dc8..4a34ba9 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -1,15 +1,15 @@ +pub use crate::common::fs::{create_dir, exe_string, read_lines, remove_dir, InvalidPath, UnreadableDir}; use crate::env_var; -pub use crate::fs::{ - create_dir, exe_string, pathbuf_to_string, read_lines, remove_dir, InvalidPath, UnreadableDir, -}; -use crate::parser; -use crate::structures::cheat::VariableMap; +use crate::parser::Parser; +use crate::prelude::*; + use crate::structures::fetcher; -use anyhow::Result; use directories_next::BaseDirs; use regex::Regex; -use std::collections::HashSet; -use std::path::{Path, PathBuf, MAIN_SEPARATOR}; + +use std::cell::RefCell; +use std::path::MAIN_SEPARATOR; + use walkdir::WalkDir; pub fn all_cheat_files(path: &Path) -> Vec { @@ -76,7 +76,7 @@ pub fn cheat_paths(path: Option) -> Result { if let Some(p) = path { Ok(p) } else { - pathbuf_to_string(&default_cheat_pathbuf()?) + Ok(default_cheat_pathbuf()?.to_string()) } } @@ -86,15 +86,6 @@ pub fn tmp_pathbuf() -> Result { Ok(root) } -fn without_first(string: &str) -> String { - string - .char_indices() - .next() - .and_then(|(i, _)| string.get(i + 1..)) - .expect("Should have at least one char") - .to_string() -} - fn interpolate_paths(paths: String) -> String { let re = Regex::new(r#"\$\{?[a-zA-Z_][a-zA-Z_0-9]*"#).unwrap(); let mut newtext = paths.to_string(); @@ -111,63 +102,29 @@ fn interpolate_paths(paths: String) -> String { newtext } -fn gen_lists(tag_rules: Option) -> (Option>, Option>) { - let mut allowlist = None; - let mut denylist: Option> = None; - - if let Some(rules) = tag_rules { - let words: Vec<_> = rules.split(',').collect(); - allowlist = Some( - words - .iter() - .filter(|w| !w.starts_with('!')) - .map(|w| w.to_string()) - .collect(), - ); - denylist = Some( - words - .iter() - .filter(|w| w.starts_with('!')) - .map(|w| without_first(w)) - .collect(), - ); - } - - (allowlist, denylist) -} - pub struct Fetcher { path: Option, - allowlist: Option>, - denylist: Option>, + files: RefCell>, } impl Fetcher { - pub fn new(path: Option, tag_rules: Option) -> Self { - let (allowlist, denylist) = gen_lists(tag_rules); + pub fn new(path: Option) -> Self { Self { path, - allowlist, - denylist, + files: Default::default(), } } } impl fetcher::Fetcher for Fetcher { - fn fetch( - &self, - stdin: &mut std::process::ChildStdin, - files: &mut Vec, - ) -> Result> { - let mut variables = VariableMap::new(); + fn fetch(&self, parser: &mut Parser) -> Result { let mut found_something = false; - let mut visited_lines = HashSet::new(); let path = self.path.clone(); let paths = cheat_paths(path); if paths.is_err() { - return Ok(None); + return Ok(false); }; let paths = paths.expect("Unable to get paths"); @@ -175,7 +132,9 @@ impl fetcher::Fetcher for Fetcher { let folders = paths_from_path_param(&interpolated_paths); let home_regex = Regex::new(r"^~").unwrap(); - let home = BaseDirs::new().and_then(|b| pathbuf_to_string(b.home_dir()).ok()); + let home = BaseDirs::new().map(|b| b.home_dir().to_string()); + + // parser.filter = self.tag_rules.as_ref().map(|r| gen_lists(r.as_str())); for folder in folders { let interpolated_folder = match &home { @@ -184,21 +143,12 @@ impl fetcher::Fetcher for Fetcher { }; let folder_pathbuf = PathBuf::from(interpolated_folder); for file in all_cheat_files(&folder_pathbuf) { - files.push(file.clone()); - let index = files.len() - 1; + self.files.borrow_mut().push(file.clone()); + let index = self.files.borrow().len() - 1; let read_file_result = { let path = PathBuf::from(&file); let lines = read_lines(&path)?; - parser::read_lines( - lines, - &file, - index, - &mut variables, - &mut visited_lines, - stdin, - self.allowlist.as_ref(), - self.denylist.as_ref(), - ) + parser.read_lines(lines, &file, Some(index)) }; if read_file_result.is_ok() && !found_something { @@ -207,11 +157,11 @@ impl fetcher::Fetcher for Fetcher { } } - if !found_something { - return Ok(None); - } + Ok(found_something) + } - Ok(Some(variables)) + fn files(&self) -> Vec { + self.files.borrow().clone() } } @@ -279,7 +229,7 @@ mod tests { #[test] fn test_default_config_pathbuf() { let base_dirs = BaseDirs::new() - .ok_or(anyhow!("bad")) + .ok_or_else(|| anyhow!("bad")) .expect("could not determine base directories"); let expected = { @@ -297,7 +247,7 @@ mod tests { #[test] fn test_default_cheat_pathbuf() { let base_dirs = BaseDirs::new() - .ok_or(anyhow!("bad")) + .ok_or_else(|| anyhow!("bad")) .expect("could not determine base directories"); let expected = { diff --git a/src/finder/mod.rs b/src/finder/mod.rs index 0285d02..e31b005 100644 --- a/src/finder/mod.rs +++ b/src/finder/mod.rs @@ -1,18 +1,16 @@ -use crate::config::CONFIG; -use crate::structures::cheat::VariableMap; -use crate::writer; -use anyhow::Context; -use anyhow::Result; +use crate::prelude::*; +use crate::serializer; + +use std::io::Write; 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; +mod post; + #[derive(Debug, Clone, Copy, Deserialize)] pub enum FinderChoice { Fzf, @@ -31,12 +29,6 @@ impl FromStr for FinderChoice { } } -pub trait Finder { - fn call(&self, opts: Opts, stdin_fn: F) -> Result<(String, Option, Vec)> - where - F: Fn(&mut process::ChildStdin, &mut Vec) -> Result>; -} - fn parse(out: Output, opts: Opts) -> Result { let text = match out.status.code() { Some(0) | Some(1) | Some(2) => { @@ -54,10 +46,10 @@ fn parse(out: Output, opts: Opts) -> Result { post::process(output, opts.column, opts.delimiter.as_deref(), opts.map) } -impl Finder for FinderChoice { - fn call(&self, finder_opts: Opts, stdin_fn: F) -> Result<(String, Option, Vec)> +impl FinderChoice { + pub fn call(&self, finder_opts: Opts, stdin_fn: F) -> Result<(String, R)> where - F: Fn(&mut process::ChildStdin, &mut Vec) -> Result>, + F: Fn(&mut dyn Write) -> Result, { let finder_str = match self { Self::Fzf => "fzf", @@ -86,7 +78,7 @@ impl Finder for FinderChoice { "--with-nth", "1,2,3", "--delimiter", - writer::DELIMITER.to_string().as_str(), + serializer::DELIMITER.to_string().as_str(), "--ansi", "--bind", format!("ctrl-j:down,ctrl-k:up{}", bindings).as_str(), @@ -188,12 +180,13 @@ impl Finder for FinderChoice { .as_mut() .ok_or_else(|| anyhow!("Unable to acquire stdin of finder"))?; - let mut files = vec![]; - let result_map = stdin_fn(stdin, &mut files).context("Failed to pass data to finder")?; + let mut writer: Box<&mut dyn Write> = Box::new(stdin); + + let return_value = stdin_fn(&mut writer).context("Failed to pass data to finder")?; let out = child.wait_with_output().context("Failed to wait for finder")?; let output = parse(out, finder_opts).context("Unable to get output")?; - Ok((output, result_map, files)) + Ok((output, return_value)) } } diff --git a/src/finder/post.rs b/src/finder/post.rs index 83ae8f2..8d660ce 100644 --- a/src/finder/post.rs +++ b/src/finder/post.rs @@ -1,8 +1,6 @@ -use crate::config::CONFIG; +use crate::common::shell; use crate::finder::structures::SuggestionType; -use crate::shell; -use anyhow::Context; -use anyhow::Result; +use crate::prelude::*; use shell::EOF; use std::process::Stdio; diff --git a/src/finder/structures.rs b/src/finder/structures.rs index e2c98db..e0581ea 100644 --- a/src/finder/structures.rs +++ b/src/finder/structures.rs @@ -1,5 +1,5 @@ -use crate::config::CONFIG; use crate::filesystem; +use crate::prelude::*; #[derive(Debug, PartialEq, Clone)] pub struct Opts { diff --git a/src/handler/core.rs b/src/handler/core.rs deleted file mode 100644 index e816aba..0000000 --- a/src/handler/core.rs +++ /dev/null @@ -1,51 +0,0 @@ -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; -use crate::finder::Finder; -use crate::structures::cheat::VariableMap; -use crate::structures::fetcher::Fetcher; -use crate::tldr; -use crate::welcome; -use anyhow::Context; -use anyhow::Result; - -pub fn main() -> Result<()> { - let config = &CONFIG; - let opts = FinderOpts::snippet_default(); - - let (raw_selection, variables, files) = config - .finder() - .call(opts, |stdin, files| { - let fetcher: Box = match config.source() { - Source::Cheats(query) => Box::new(cheatsh::Fetcher::new(query)), - Source::Tldr(query) => Box::new(tldr::Fetcher::new(query)), - Source::Filesystem(path, rules) => Box::new(filesystem::Fetcher::new(path, rules)), - }; - - let res = fetcher - .fetch(stdin, files) - .context("Failed to parse variables intended for finder")?; - - if let Some(variables) = res { - Ok(Some(variables)) - } else { - welcome::populate_cheatsheet(stdin)?; - Ok(Some(VariableMap::new())) - } - }) - .context("Failed getting selection and variables from finder")?; - - let extractions = extractor::extract_from_selections(&raw_selection, config.best_match()); - - if extractions.is_err() { - return main(); - } - - actor::act(extractions, files, variables)?; - - Ok(()) -} diff --git a/src/handler/func.rs b/src/handler/func.rs deleted file mode 100644 index 686ee7d..0000000 --- a/src/handler/func.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::cheat_variable; - -use crate::shell::{self}; - -use crate::url; -use crate::welcome; - -use anyhow::Result; - -#[derive(Debug)] -pub enum Func { - UrlOpen, - Welcome, - WidgetLastCommand, - MapExpand, -} - -pub fn main(func: &Func, args: Vec) -> Result<()> { - match func { - Func::UrlOpen => url::open(args), - Func::Welcome => welcome::main(), - Func::WidgetLastCommand => shell::widget_last_command(), - Func::MapExpand => cheat_variable::map_expand(), - } -} diff --git a/src/handler/info.rs b/src/handler/info.rs deleted file mode 100644 index f893178..0000000 --- a/src/handler/info.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::filesystem; -use crate::fs::pathbuf_to_string; -use anyhow::Result; - -#[derive(Debug)] -pub enum Info { - CheatsExample, - CheatsPath, - ConfigPath, - ConfigExample, -} - -pub fn main(info: &Info) -> Result<()> { - match info { - Info::CheatsExample => println!("{}", include_str!("../../docs/cheat_example.cheat")), - Info::CheatsPath => println!("{}", pathbuf_to_string(&filesystem::default_cheat_pathbuf()?)?), - Info::ConfigPath => println!("{}", pathbuf_to_string(&filesystem::default_config_pathbuf()?)?), - Info::ConfigExample => println!("{}", include_str!("../../docs/config_file_example.yaml")), - } - Ok(()) -} diff --git a/src/handler/mod.rs b/src/handler/mod.rs deleted file mode 100644 index d43db58..0000000 --- a/src/handler/mod.rs +++ /dev/null @@ -1,60 +0,0 @@ -pub mod core; -pub mod func; -pub mod info; -pub mod preview; -pub mod preview_var; -pub mod preview_var_stdin; -pub mod repo_add; -pub mod repo_browse; -pub mod shell; - -#[cfg(not(feature = "disable-repo-management"))] -use crate::config::Command::Repo; -use crate::config::Command::{Fn, Info, Preview, PreviewVar, PreviewVarStdin, Widget}; -use crate::config::{RepoCommand, CONFIG}; -use crate::handler; -use anyhow::Context; -use anyhow::Result; - -pub fn handle() -> Result<()> { - match CONFIG.cmd() { - None => handler::core::main(), - - Some(c) => match c { - Preview { line } => handler::preview::main(line), - - PreviewVarStdin => handler::preview_var_stdin::main(), - - PreviewVar { - selection, - query, - variable, - } => handler::preview_var::main(selection, query, variable), - - Widget { shell } => handler::shell::main(shell).context("Failed to print shell widget code"), - - Fn { func, args } => handler::func::main(func, args.to_vec()) - .with_context(|| format!("Failed to execute function `{:#?}`", func)), - - Info { info } => { - handler::info::main(info).with_context(|| format!("Failed to fetch info `{:#?}`", info)) - } - - #[cfg(not(feature = "disable-repo-management"))] - Repo { cmd } => match cmd { - RepoCommand::Add { uri } => { - handler::repo_add::main(uri.clone()) - .with_context(|| format!("Failed to import cheatsheets from `{}`", uri))?; - handler::core::main() - } - RepoCommand::Browse => { - 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() - } - }, - }, - } -} diff --git a/src/handler/preview.rs b/src/handler/preview.rs deleted file mode 100644 index 1b3a801..0000000 --- a/src/handler/preview.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::config::CONFIG; -use crate::ui; -use crate::writer; -use anyhow::Result; -use crossterm::style::Stylize; -use std::process; - -fn extract_elements(argstr: &str) -> (&str, &str, &str) { - let mut parts = argstr.split(writer::DELIMITER).skip(3); - let tags = parts.next().expect("No `tags` element provided."); - let comment = parts.next().expect("No `comment` element provided."); - let snippet = parts.next().expect("No `snippet` element provided."); - (tags, comment, snippet) -} - -pub fn main(line: &str) -> Result<()> { - let (tags, comment, snippet) = extract_elements(line); - - println!( - "{comment} {tags} \n{snippet}", - comment = ui::style(comment).with(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) -} diff --git a/src/handler/preview_var.rs b/src/handler/preview_var.rs deleted file mode 100644 index 455ce39..0000000 --- a/src/handler/preview_var.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::config::CONFIG; -use crate::env_var; -use crate::finder; -use crate::terminal::style::style; -use crate::writer; -use anyhow::Result; -use crossterm::style::Stylize; -use std::collections::HashSet; -use std::iter; -use std::process; - -pub fn main(selection: &str, query: &str, variable: &str) -> Result<()> { - let snippet = env_var::must_get(env_var::PREVIEW_INITIAL_SNIPPET); - let tags = env_var::must_get(env_var::PREVIEW_TAGS); - let comment = env_var::must_get(env_var::PREVIEW_COMMENT); - let column = env_var::parse(env_var::PREVIEW_COLUMN); - let delimiter = env_var::get(env_var::PREVIEW_DELIMITER).ok(); - let map = env_var::get(env_var::PREVIEW_MAP).ok(); - - let active_color = CONFIG.tag_color(); - let inactive_color = CONFIG.comment_color(); - - let mut colored_snippet = String::from(&snippet); - let mut visited_vars: HashSet<&str> = HashSet::new(); - - let mut variables = String::from(""); - - println!( - "{comment} {tags}", - comment = style(comment).with(CONFIG.comment_color()), - tags = style(format!("[{}]", tags)).with(CONFIG.tag_color()), - ); - - let bracketed_current_variable = format!("<{}>", variable); - - let bracketed_variables: Vec<&str> = { - if snippet.contains(&bracketed_current_variable) { - writer::VAR_REGEX - .find_iter(&snippet) - .map(|m| m.as_str()) - .collect() - } else { - iter::once(&bracketed_current_variable) - .map(|s| s.as_str()) - .collect() - } - }; - - for bracketed_variable_name in bracketed_variables { - let variable_name = &bracketed_variable_name[1..bracketed_variable_name.len() - 1]; - - if visited_vars.contains(variable_name) { - continue; - } else { - visited_vars.insert(variable_name); - } - - let is_current = variable_name == variable; - let variable_color = if is_current { active_color } else { inactive_color }; - let env_variable_name = env_var::escape(variable_name); - - let value = if is_current { - let v = selection.trim_matches('\''); - if v.is_empty() { query.trim_matches('\'') } else { v }.to_string() - } else if let Ok(v) = env_var::get(&env_variable_name) { - v - } else { - "".to_string() - }; - - let replacement = format!( - "{variable}", - variable = style(bracketed_variable_name).with(variable_color), - ); - - colored_snippet = colored_snippet.replace(bracketed_variable_name, &replacement); - - variables = format!( - "{variables}\n{variable} = {value}", - variables = variables, - variable = style(variable_name).with(variable_color), - value = finder::process(value, column, delimiter.as_deref(), map.clone()) - .expect("Unable to process value"), - ); - } - - println!("{snippet}", snippet = writer::fix_newlines(&colored_snippet)); - println!("{variables}", variables = variables); - - process::exit(0) -} diff --git a/src/handler/preview_var_stdin.rs b/src/handler/preview_var_stdin.rs deleted file mode 100755 index ef096f1..0000000 --- a/src/handler/preview_var_stdin.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::shell::{self, ShellSpawnError, EOF}; -use anyhow::Result; -use std::io::{self, Read}; - -pub fn main() -> Result<()> { - let mut text = String::new(); - io::stdin().read_to_string(&mut text)?; - - let mut parts = text.split(EOF); - let selection = parts.next().expect("Unable to get selection"); - let query = parts.next().expect("Unable to get query"); - let variable = parts.next().expect("Unable to get variable").trim(); - - super::handler::preview_var::main(selection, query, variable)?; - - if let Some(extra) = parts.next() { - if !extra.is_empty() { - print!(""); - - shell::out() - .arg(extra) - .spawn() - .map_err(|e| ShellSpawnError::new(extra, e))? - .wait()?; - } - } - - Ok(()) -} diff --git a/src/handler/shell.rs b/src/handler/shell.rs deleted file mode 100644 index fc23eee..0000000 --- a/src/handler/shell.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::shell::Shell; -use anyhow::Result; - -pub fn main(shell: &Shell) -> Result<()> { - let content = match shell { - Shell::Bash => include_str!("../../shell/navi.plugin.bash"), - Shell::Zsh => include_str!("../../shell/navi.plugin.zsh"), - Shell::Fish => include_str!("../../shell/navi.plugin.fish"), - Shell::Elvish => include_str!("../../shell/navi.plugin.elv"), - }; - - println!("{}", content); - - Ok(()) -} diff --git a/src/lib.rs b/src/lib.rs index 93dec3b..af26108 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,29 +1,20 @@ #[macro_use] extern crate lazy_static; -#[macro_use] -extern crate anyhow; +// #[macro_use] +// extern crate anyhow; -mod actor; -mod cheat_variable; -mod cheatsh; -mod clipboard; +mod clients; +mod commands; +mod common; mod config; mod env_var; -mod extractor; mod filesystem; mod finder; -mod fs; -mod git; -mod handler; -mod hash; mod parser; -mod shell; +mod prelude; +mod serializer; mod structures; mod terminal; -mod tldr; -mod ui; -mod url; mod welcome; -mod writer; -pub use handler::handle; +pub use commands::handle; diff --git a/src/parser.rs b/src/parser.rs index 18d33cc..557b6a5 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,12 +1,9 @@ +use crate::common::fs; use crate::finder::structures::{Opts as FinderOpts, SuggestionType}; -use crate::fs; -use crate::hash::fnv; +use crate::prelude::*; +use crate::serializer; use crate::structures::cheat::VariableMap; use crate::structures::item::Item; -use crate::writer; -use anyhow::{Context, Result}; -use regex::Regex; -use std::collections::HashSet; use std::io::Write; lazy_static! { @@ -108,49 +105,6 @@ fn parse_variable_line(line: &str) -> Result<(&str, &str, Option)> { Ok((variable, command, command_options)) } -fn write_cmd( - item: &Item, - stdin: &mut std::process::ChildStdin, - allowlist: Option<&Vec>, - denylist: Option<&Vec>, - visited_lines: &mut HashSet, -) -> Result<()> { - if item.comment.is_empty() || item.snippet.trim().is_empty() { - return Ok(()); - } - - let hash = fnv(&format!("{}{}", &item.comment, &item.snippet)); - if visited_lines.contains(&hash) { - return Ok(()); - } - visited_lines.insert(hash); - - if let Some(list) = denylist { - for v in list { - if item.tags.contains(v) { - return Ok(()); - } - } - } - - if let Some(list) = allowlist { - let mut should_allow = false; - for v in list { - if item.tags.contains(v) { - should_allow = true; - break; - } - } - if !should_allow { - return Ok(()); - } - } - - return stdin - .write_all(writer::write(item).as_bytes()) - .context("Failed to write command to finder's stdin"); -} - fn without_prefix(line: &str) -> String { if line.len() > 2 { String::from(line[2..].trim()) @@ -159,96 +113,214 @@ fn without_prefix(line: &str) -> String { } } -#[allow(clippy::too_many_arguments)] -pub fn read_lines( - lines: impl Iterator>, - id: &str, - file_index: usize, - variables: &mut VariableMap, - visited_lines: &mut HashSet, - stdin: &mut std::process::ChildStdin, - allowlist: Option<&Vec>, - denylist: Option<&Vec>, -) -> Result<()> { - let mut item = Item::new(); - item.file_index = file_index; +#[derive(Clone, Default)] +pub struct FilterOpts { + pub allowlist: Vec, + pub denylist: Vec, + pub hash: Option, +} - let mut should_break = false; +pub struct Parser<'a> { + pub variables: VariableMap, + visited_lines: HashSet, + filter: FilterOpts, + writer: &'a mut dyn Write, + write_fn: fn(&Item) -> String, +} - let mut variable_cmd = String::from(""); +fn without_first(string: &str) -> String { + string + .char_indices() + .next() + .and_then(|(i, _)| string.get(i + 1..)) + .expect("Should have at least one char") + .to_string() +} - for (line_nr, line_result) in lines.enumerate() { - let line = line_result - .with_context(|| format!("Failed to read line number {} in cheatsheet `{}`", line_nr, id))?; +fn gen_lists(tag_rules: &str) -> FilterOpts { + let words: Vec<_> = tag_rules.split(',').collect(); - if should_break { - break; - } + let allowlist = words + .iter() + .filter(|w| !w.starts_with('!')) + .map(|w| w.to_string()) + .collect(); - // duplicate - if !item.tags.is_empty() && !item.comment.is_empty() {} - // blank - if line.is_empty() { - if !(&item.snippet).is_empty() { - item.snippet.push_str(writer::LINE_SEPARATOR); - } - } - // tag - else if line.starts_with('%') { - should_break = write_cmd(&item, stdin, allowlist, denylist, visited_lines).is_err(); - item.snippet = String::from(""); - item.tags = without_prefix(&line); - } - // dependency - else if line.starts_with('@') { - let tags_dependency = without_prefix(&line); - variables.insert_dependency(&item.tags, &tags_dependency); - } - // metacomment - else if line.starts_with(';') { - } - // comment - else if line.starts_with('#') { - should_break = write_cmd(&item, stdin, allowlist, denylist, visited_lines).is_err(); - item.snippet = String::from(""); - item.comment = without_prefix(&line); - } - // variable - else if !variable_cmd.is_empty() || (line.starts_with('$') && line.contains(':')) { - should_break = write_cmd(&item, stdin, allowlist, denylist, visited_lines).is_err(); + let denylist = words + .iter() + .filter(|w| w.starts_with('!')) + .map(|w| without_first(w)) + .collect(); - item.snippet = String::from(""); + FilterOpts { + allowlist, + denylist, + ..Default::default() + } +} - variable_cmd.push_str(line.trim_end_matches('\\')); +impl<'a> Parser<'a> { + pub fn new(writer: &'a mut dyn Write, is_terminal: bool) -> Self { + let write_fn = if is_terminal { + serializer::write + } else { + serializer::write_raw + }; - if !line.ends_with('\\') { - let full_variable_cmd = variable_cmd.clone(); - let (variable, command, opts) = - parse_variable_line(&full_variable_cmd).with_context(|| { - format!( - "Failed to parse variable line. See line number {} in cheatsheet `{}`", - line_nr + 1, - id - ) - })?; - variable_cmd = String::from(""); - variables.insert_suggestion(&item.tags, variable, (String::from(command), opts)); - } - } - // snippet - else { - if !(&item.snippet).is_empty() { - item.snippet.push_str(writer::LINE_SEPARATOR); - } - item.snippet.push_str(&line); + let filter = match CONFIG.tag_rules() { + Some(tr) => gen_lists(&tr), + None => Default::default(), + }; + + Self { + variables: Default::default(), + visited_lines: Default::default(), + filter, + write_fn, + writer, } } - if !should_break { - let _ = write_cmd(&item, stdin, allowlist, denylist, visited_lines); + pub fn set_hash(&mut self, hash: u64) { + self.filter.hash = Some(hash) } - Ok(()) + fn write_cmd(&mut self, item: &Item) -> Result<()> { + if item.comment.is_empty() || item.snippet.trim().is_empty() { + return Ok(()); + } + + let hash = item.hash(); + if self.visited_lines.contains(&hash) { + return Ok(()); + } + self.visited_lines.insert(hash); + + if !self.filter.denylist.is_empty() { + for v in &self.filter.denylist { + if item.tags.contains(v) { + return Ok(()); + } + } + } + + if !self.filter.allowlist.is_empty() { + let mut should_allow = false; + for v in &self.filter.allowlist { + if item.tags.contains(v) { + should_allow = true; + break; + } + } + if !should_allow { + return Ok(()); + } + } + + if let Some(h) = self.filter.hash { + if h != hash { + return Ok(()); + } + } + + let write_fn = self.write_fn; + + return self + .writer + .write_all(write_fn(item).as_bytes()) + .context("Failed to write command to finder's stdin"); + } + + pub fn read_lines( + &mut self, + lines: impl Iterator>, + id: &str, + file_index: Option, + ) -> Result<()> { + let mut item = Item::new(file_index); + + let mut should_break = false; + + let mut variable_cmd = String::from(""); + + for (line_nr, line_result) in lines.enumerate() { + let line = line_result + .with_context(|| format!("Failed to read line number {} in cheatsheet `{}`", line_nr, id))?; + + if should_break { + break; + } + + // duplicate + if !item.tags.is_empty() && !item.comment.is_empty() {} + // blank + if line.is_empty() { + if !(&item.snippet).is_empty() { + item.snippet.push_str(serializer::LINE_SEPARATOR); + } + } + // tag + else if line.starts_with('%') { + should_break = self.write_cmd(&item).is_err(); + item.snippet = String::from(""); + item.tags = without_prefix(&line); + } + // dependency + else if line.starts_with('@') { + let tags_dependency = without_prefix(&line); + self.variables.insert_dependency(&item.tags, &tags_dependency); + } + // raycast icon + else if let Some(icon) = line.strip_prefix("; raycast.icon:") { + item.icon = Some(icon.trim().into()); + } + // metacomment + else if line.starts_with(';') { + } + // comment + else if line.starts_with('#') { + should_break = self.write_cmd(&item).is_err(); + item.snippet = String::from(""); + item.comment = without_prefix(&line); + } + // variable + else if !variable_cmd.is_empty() || (line.starts_with('$') && line.contains(':')) { + should_break = self.write_cmd(&item).is_err(); + + item.snippet = String::from(""); + + variable_cmd.push_str(line.trim_end_matches('\\')); + + if !line.ends_with('\\') { + let full_variable_cmd = variable_cmd.clone(); + let (variable, command, opts) = + parse_variable_line(&full_variable_cmd).with_context(|| { + format!( + "Failed to parse variable line. See line number {} in cheatsheet `{}`", + line_nr + 1, + id + ) + })?; + variable_cmd = String::from(""); + self.variables + .insert_suggestion(&item.tags, variable, (String::from(command), opts)); + } + } + // snippet + else { + if !(&item.snippet).is_empty() { + item.snippet.push_str(serializer::LINE_SEPARATOR); + } + item.snippet.push_str(&line); + } + } + + if !should_break { + let _ = self.write_cmd(&item); + } + + Ok(()) + } } #[cfg(test)] diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 0000000..bfe6124 --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,9 @@ +pub use crate::common::prelude::*; +pub use crate::config::CONFIG; // TODO +pub use regex::Regex; + +// pub use crate::common::fs::pathbuf_to_string; // TODO + +pub trait Runnable { + fn run(&self) -> Result<()>; +} diff --git a/src/serializer.rs b/src/serializer.rs new file mode 100644 index 0000000..7f0cbaf --- /dev/null +++ b/src/serializer.rs @@ -0,0 +1,121 @@ +use crate::common::terminal; +use crate::prelude::*; +use crate::structures::item::Item; +use crossterm::style::{style, Stylize}; +use std::cmp::max; + +pub fn get_widths() -> (usize, usize, usize) { + let width = terminal::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, + ); + let snippet_width_percentage = max( + CONFIG.snippet_min_width(), + width * CONFIG.snippet_width_percentage() / 100, + ); + ( + usize::from(tag_width_percentage), + usize::from(comment_width_percentage), + usize::from(snippet_width_percentage), + ) +} + +const NEWLINE_ESCAPE_CHAR: char = '\x15'; +const FIELD_SEP_ESCAPE_CHAR: char = '\x16'; +pub const LINE_SEPARATOR: &str = " \x15 "; +pub const DELIMITER: &str = r" ⠀"; + +lazy_static! { + pub static ref NEWLINE_REGEX: Regex = Regex::new(r"\\\s+").expect("Invalid regex"); + pub static ref VAR_REGEX: Regex = Regex::new(r"\\?<(\w[\w\d\-_]*)>").expect("Invalid regex"); + pub static ref COLUMN_WIDTHS: (usize, usize, usize) = get_widths(); +} + +pub fn with_new_lines(txt: String) -> String { + txt.replace(LINE_SEPARATOR, "\n") +} + +pub fn fix_newlines(txt: &str) -> String { + if txt.contains(NEWLINE_ESCAPE_CHAR) { + (*NEWLINE_REGEX) + .replace_all(txt.replace(LINE_SEPARATOR, " ").as_str(), "") + .to_string() + } else { + txt.to_string() + } +} + +fn limit_str(text: &str, length: usize) -> String { + if text.len() > length { + format!("{}…", text.chars().take(length - 1).collect::()) + } else { + format!("{:width$}", text, width = length) + } +} + +pub fn write(item: &Item) -> String { + let (tag_width_percentage, comment_width_percentage, snippet_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 = style(limit_str(&item.tags, tag_width_percentage)).with(CONFIG.tag_color()), + comment_short = style(limit_str(&item.comment, comment_width_percentage)).with(CONFIG.comment_color()), + snippet_short = style(limit_str(&fix_newlines(&item.snippet), snippet_width_percentage)).with(CONFIG.snippet_color()), + tags = item.tags, + comment = item.comment, + delimiter = DELIMITER, + snippet = &item.snippet.trim_end_matches(LINE_SEPARATOR), + file_index = item.file_index.unwrap_or(0), + ) +} + +pub fn write_raw(item: &Item) -> String { + format!( + "{hash}{delimiter}{tags}{delimiter}{comment}{delimiter}{icon}{delimiter}{snippet}\n", + hash = item.hash(), + tags = item.tags, + comment = item.comment, + delimiter = FIELD_SEP_ESCAPE_CHAR, + icon = item.icon.clone().unwrap_or_default(), + snippet = &item.snippet.trim_end_matches(LINE_SEPARATOR), + ) +} + +pub fn raycast_deser(line: &str) -> Result { + let mut parts = line.split(FIELD_SEP_ESCAPE_CHAR); + let hash: u64 = parts + .next() + .context("no hash")? + .parse() + .context("hash not a u64")?; + let tags = parts.next().context("no tags")?.into(); + let comment = parts.next().context("no comment")?.into(); + let icon_str = parts.next().context("no icon")?; + let snippet = parts.next().context("no snippet")?.into(); + + let icon = if icon_str.is_empty() { + None + } else { + Some(icon_str.into()) + }; + + let item = Item { + tags, + comment, + icon, + snippet, + ..Default::default() + }; + + if item.hash() != hash { + dbg!(&item.hash()); + dbg!(hash); + Err(anyhow!("Incorrect hash")) + } else { + Ok(item) + } +} diff --git a/src/shell.rs b/src/shell.rs deleted file mode 100644 index d65d228..0000000 --- a/src/shell.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::config::CONFIG; -use anyhow::Result; -use std::fmt::Debug; -use std::io::{self, Read}; -use std::process::Command; -use thiserror::Error; - -pub const EOF: &str = "NAVIEOF"; - -#[derive(Debug)] -pub enum Shell { - Bash, - Zsh, - Fish, - Elvish, -} - -#[derive(Error, Debug)] -#[error("Failed to spawn child process `bash` to execute `{command}`")] -pub struct ShellSpawnError { - command: String, - #[source] - source: anyhow::Error, -} - -impl ShellSpawnError { - pub fn new(command: impl Into, source: SourceError) -> Self - where - SourceError: std::error::Error + Sync + Send + 'static, - { - ShellSpawnError { - command: command.into(), - source: source.into(), - } - } -} - -pub fn out() -> Command { - let words_str = CONFIG.shell(); - let mut words_vec = shellwords::split(&words_str).expect("empty shell command"); - let mut words = words_vec.iter_mut(); - let first_cmd = words.next().expect("absent shell binary"); - let mut cmd = Command::new(&first_cmd); - cmd.args(words); - let dash_c = if words_str.contains("cmd.exe") { "/c" } else { "-c" }; - cmd.arg(dash_c); - cmd -} - -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 && p != format!("{}{}", pattern, 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(()) -} diff --git a/src/structures/cheat.rs b/src/structures/cheat.rs index d4ce57c..20a090e 100644 --- a/src/structures/cheat.rs +++ b/src/structures/cheat.rs @@ -1,23 +1,16 @@ +use crate::common::hash::fnv; use crate::finder::structures::Opts; -use crate::hash::fnv; -use std::collections::HashMap; +use crate::prelude::*; pub type Suggestion = (String, Option); -#[derive(Clone)] +#[derive(Clone, Default)] pub struct VariableMap { variables: HashMap>, dependencies: HashMap>, } impl VariableMap { - pub fn new() -> Self { - Self { - variables: HashMap::new(), - dependencies: HashMap::new(), - } - } - pub fn insert_dependency(&mut self, tags: &str, tags_dependency: &str) { let k = fnv(&tags); if let Some(v) = self.dependencies.get_mut(&k) { diff --git a/src/structures/fetcher.rs b/src/structures/fetcher.rs index 0eeda52..4488ef7 100644 --- a/src/structures/fetcher.rs +++ b/src/structures/fetcher.rs @@ -1,10 +1,10 @@ -use crate::structures::cheat::VariableMap; -use anyhow::Result; +use crate::parser::Parser; +use crate::prelude::*; pub trait Fetcher { - fn fetch( - &self, - stdin: &mut std::process::ChildStdin, - files: &mut Vec, - ) -> Result>; + fn fetch(&self, parser: &mut Parser) -> Result; + + fn files(&self) -> Vec { + vec![] + } } diff --git a/src/structures/item.rs b/src/structures/item.rs index f4fda5d..da4a181 100644 --- a/src/structures/item.rs +++ b/src/structures/item.rs @@ -1,17 +1,23 @@ +use crate::common::hash::fnv; + +#[derive(Default, Debug)] pub struct Item { pub tags: String, pub comment: String, pub snippet: String, - pub file_index: usize, + pub file_index: Option, + pub icon: Option, } impl Item { - pub fn new() -> Self { + pub fn new(file_index: Option) -> Self { Self { - tags: "".to_string(), - comment: "".to_string(), - snippet: "".to_string(), - file_index: 0, + file_index, + ..Default::default() } } + + pub fn hash(&self) -> u64 { + fnv(&format!("{}{}", &self.tags.trim(), &self.comment.trim())) + } } diff --git a/src/terminal.rs b/src/terminal.rs index 780ab9c..8b13789 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -1,67 +1 @@ -use anyhow::Result; -pub use crossterm::style; -use crossterm::terminal; -use std::str::FromStr; -const FALLBACK_WIDTH: u16 = 80; - -fn width_with_shell_out() -> Result { - use std::process::Command; - use std::process::Stdio; - - let output = if cfg!(target_os = "macos") { - Command::new("stty") - .arg("-f") - .arg("/dev/stderr") - .arg("size") - .stderr(Stdio::inherit()) - .output()? - } else { - Command::new("stty") - .arg("size") - .arg("-F") - .arg("/dev/stderr") - .stderr(Stdio::inherit()) - .output()? - }; - - 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::() - .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().unwrap_or(FALLBACK_WIDTH) - } -} - -pub fn parse_ansi(ansi: &str) -> Option { - 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 { - if let Some(c) = parse_ansi(ansi) { - Ok(Color(c)) - } else { - Err("Invalid color") - } - } -} diff --git a/src/ui.rs b/src/ui.rs deleted file mode 100644 index 2530dbb..0000000 --- a/src/ui.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::config::CONFIG; -use crate::terminal; -pub use crate::terminal::style::style; -use std::cmp::max; - -pub fn get_widths() -> (usize, usize, usize) { - let width = terminal::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, - ); - let snippet_width_percentage = max( - CONFIG.snippet_min_width(), - width * CONFIG.snippet_width_percentage() / 100, - ); - ( - usize::from(tag_width_percentage), - usize::from(comment_width_percentage), - usize::from(snippet_width_percentage), - ) -} diff --git a/src/welcome.rs b/src/welcome.rs index 2363014..62e41bc 100644 --- a/src/welcome.rs +++ b/src/welcome.rs @@ -1,48 +1,30 @@ -use crate::actor; -use crate::config::CONFIG; -use crate::extractor; -use crate::finder::structures::Opts as FinderOpts; -use crate::finder::Finder; -use crate::parser; -use crate::structures::cheat::VariableMap; -use anyhow::Context; -use anyhow::Result; +use crate::parser::Parser; +use crate::prelude::*; +use crate::structures::fetcher; -pub fn main() -> Result<()> { - let config = &CONFIG; - let opts = FinderOpts::snippet_default(); - - let (raw_selection, variables, files) = config - .finder() - .call(opts, |stdin, _| { - populate_cheatsheet(stdin)?; - Ok(Some(VariableMap::new())) - }) - .context("Failed getting selection and variables from finder")?; - - let extractions = extractor::extract_from_selections(&raw_selection, config.best_match()); - - if extractions.is_err() { - return main(); - } - - actor::act(extractions, files, variables)?; - Ok(()) -} - -pub fn populate_cheatsheet(stdin: &mut std::process::ChildStdin) -> Result<()> { +pub fn populate_cheatsheet(parser: &mut Parser) -> Result<()> { let cheatsheet = include_str!("../docs/navi.cheat"); - parser::read_lines( + parser.read_lines( cheatsheet.split('\n').into_iter().map(|s| Ok(s.to_string())), "welcome", - 0, - &mut VariableMap::new(), - &mut Default::default(), - stdin, - None, None, )?; Ok(()) } + +pub struct Fetcher {} + +impl Fetcher { + pub fn new() -> Self { + Self {} + } +} + +impl fetcher::Fetcher for Fetcher { + fn fetch(&self, parser: &mut Parser) -> Result { + populate_cheatsheet(parser)?; + Ok(true) + } +} diff --git a/src/writer.rs b/src/writer.rs deleted file mode 100644 index 13cc8e4..0000000 --- a/src/writer.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::config::CONFIG; -use crate::structures::item::Item; -use crate::ui; -use crossterm::style::Stylize; -use regex::Regex; - -const NEWLINE_ESCAPE_CHAR: char = '\x15'; -pub const LINE_SEPARATOR: &str = " \x15 "; -pub const DELIMITER: &str = r" ⠀"; - -lazy_static! { - pub static ref NEWLINE_REGEX: Regex = Regex::new(r"\\\s+").expect("Invalid regex"); - pub static ref VAR_REGEX: Regex = Regex::new(r"\\?<(\w[\w\d\-_]*)>").expect("Invalid regex"); - pub static ref COLUMN_WIDTHS: (usize, usize, usize) = ui::get_widths(); -} - -pub fn with_new_lines(txt: String) -> String { - txt.replace(LINE_SEPARATOR, "\n") -} - -pub fn fix_newlines(txt: &str) -> String { - if txt.contains(NEWLINE_ESCAPE_CHAR) { - (*NEWLINE_REGEX) - .replace_all(txt.replace(LINE_SEPARATOR, " ").as_str(), "") - .to_string() - } else { - txt.to_string() - } -} - -fn limit_str(text: &str, length: usize) -> String { - if text.len() > length { - format!("{}…", text.chars().take(length - 1).collect::()) - } else { - format!("{:width$}", text, width = length) - } -} - -pub fn write(item: &Item) -> String { - let (tag_width_percentage, comment_width_percentage, snippet_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_percentage)).with(CONFIG.tag_color()), - comment_short = ui::style(limit_str(&item.comment, comment_width_percentage)).with(CONFIG.comment_color()), - snippet_short = ui::style(limit_str(&fix_newlines(&item.snippet), snippet_width_percentage)).with(CONFIG.snippet_color()), - tags = item.tags, - comment = item.comment, - delimiter = DELIMITER, - snippet = &item.snippet.trim_end_matches(LINE_SEPARATOR), - file_index = item.file_index, - ) -}