Refactor code base (#760)

This commit is contained in:
Denis Isidoro 2022-07-28 20:11:42 -03:00 committed by GitHub
parent f5759f26aa
commit ebb02e28ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 1556 additions and 1354 deletions

View file

@ -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\//}

View file

@ -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

26
Cargo.lock generated
View file

@ -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",

View file

@ -1,6 +1,6 @@
[package]
name = "navi"
version = "2.20.1"
version = "2.21.0"
authors = ["Denis Isidoro <denis_isidoro@live.com>"]
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"

View file

@ -1,3 +1,3 @@
[toolchain]
channel = "1.56.0"
channel = "1.62.0"
components = [ "rustfmt", "clippy" ]

27
scripts/dot Executable file
View file

@ -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" "$@"

View file

@ -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

View file

@ -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 "$@"

View file

@ -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

View file

@ -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

View file

@ -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<Item = Result<String>> {
.into_iter()
}
fn read_all(query: &str, cheat: &str, stdin: &mut std::process::ChildStdin) -> Result<Option<VariableMap>> {
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<String> {
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<String>,
) -> Result<Option<VariableMap>> {
let cheat = fetch(&self.query)?;
read_all(&self.query, &cheat, stdin)
fn fetch(&self, parser: &mut Parser) -> Result<bool> {
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)
}
}

2
src/clients/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod cheatsh;
pub mod tldr;

View file

@ -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<Item = Result<St
.into_iter()
}
fn read_all(
query: &str,
markdown: &str,
stdin: &mut std::process::ChildStdin,
) -> Result<Option<VariableMap>> {
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<String> {
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<String>,
) -> Result<Option<VariableMap>> {
fn fetch(&self, parser: &mut Parser) -> Result<bool> {
let markdown = fetch(&self.query)?;
read_all(&self.query, &markdown, stdin)
parser.read_lines(markdown_lines(&self.query, &markdown), "markdown", None)?;
Ok(true)
}
}

View file

@ -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<String> {
let mut interpolated_snippet = String::from(snippet);
let variables_found: Vec<&str> = writer::VAR_REGEX.find_iter(snippet).map(|m| m.as_str()).collect();
let 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
};

View file

@ -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<usize>);
@ -18,7 +16,7 @@ pub fn extract_from_selections(raw_snippet: &str, is_single: bool) -> Result<Out
let mut parts = lines
.next()
.context("No more parts in `selections`")?
.split(writer::DELIMITER)
.split(serializer::DELIMITER)
.skip(3);
let tags = parts.next().unwrap_or("");

41
src/commands/core/mod.rs Normal file
View file

@ -0,0 +1,41 @@
mod actor;
mod extractor;
use crate::finder::structures::Opts as FinderOpts;
use crate::parser::Parser;
use crate::prelude::*;
use crate::welcome;
pub fn main() -> 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(())
}

View file

@ -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)

65
src/commands/func/mod.rs Normal file
View file

@ -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<Self, Self::Err> {
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<String>,
}
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(),
}
}
}

View file

@ -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(())
}

48
src/commands/info.rs Normal file
View file

@ -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<Self, Self::Err> {
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(())
}
}

39
src/commands/mod.rs Normal file
View file

@ -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(),
},
}
}

View file

@ -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)
}
}

106
src/commands/preview/var.rs Normal file
View file

@ -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)
}
}

View file

@ -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(())
}
}

View file

@ -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<bool> {
@ -17,20 +13,16 @@ fn ask_if_should_import_all(finder: &FinderChoice) -> Result<bool> {
..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(())

View file

@ -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<String> {
let finder = CONFIG.finder();
@ -18,13 +14,13 @@ pub fn main() -> Result<String> {
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<String> {
..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")?;

41
src/commands/repo/mod.rs Normal file
View file

@ -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()
}
}
}
}

43
src/commands/shell.rs Normal file
View file

@ -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<Self, Self::Err> {
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(())
}
}

64
src/commands/temp.rs Normal file
View file

@ -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(())
}

View file

@ -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#"

View file

@ -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<T> AsAny for T
where
T: Any,
{
fn as_any(&self) -> &dyn Any {
self
}
fn as_mut_any(&mut self) -> &mut dyn Any {
self
}
}

7
src/common/deps.rs.bkp Normal file
View file

@ -0,0 +1,7 @@
use super::prelude::*;
pub trait HasDeps {
fn deps(&self) -> HashSet<TypeId> {
HashSet::new()
}
}

50
src/common/deser.rs.bkp Normal file
View file

@ -0,0 +1,50 @@
use super::prelude::*;
use serde::de::DeserializeOwned;
#[cfg(feature = "yaml")]
pub fn yaml_from_path<T>(path: &Path) -> Result<T>
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<T>(path: &Path) -> Result<T>
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<T>(text: &str) -> Result<T>
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<T>(text: &str) -> Result<T>
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: &T) -> Result<String>
where
T: Serialize,
{
serde_yaml::to_string(t).with_context(|| "Failed to serialize into yaml") // TODO: debug struct?
}

View file

@ -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<PathBuf> {
// 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> {
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)
})
}

View file

@ -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<()> {

13
src/common/mod.rs Normal file
View file

@ -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;

29
src/common/prelude.rs Normal file
View file

@ -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;
*/

45
src/common/shell.rs Normal file
View file

@ -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<SourceError>(command: impl Into<String>, 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
}

57
src/common/system.rs.bkp Normal file
View file

@ -0,0 +1,57 @@
use super::prelude::*;
use std::collections::hash_map::Entry;
pub struct System<C> {
pub config: Arc<C>,
components: HashMap<TypeId, Arc<dyn Component>>,
type_ids: Option<HashSet<TypeId>>,
}
impl<C> System<C> {
pub fn new(config: C) -> Result<Self> {
Ok(System {
config: Arc::new(config),
components: HashMap::new(),
type_ids: None,
})
}
pub fn get<T>(&self) -> Result<&T>
where
T: Component,
{
let type_id = TypeId::of::<T>();
let c = self.components.get(&type_id).unwrap();
c.as_any().downcast_ref::<T>().context("invalid component")
}
pub fn set_type_ids(&mut self, type_ids: HashSet<TypeId>) {
self.type_ids = Some(type_ids);
}
pub fn maybe_add<T: Component, F: FnOnce(&Self) -> Result<T>>(
&mut self,
type_id: &TypeId,
f: F,
) -> Result<Option<Arc<T>>> {
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"))
}
}
}
}

65
src/common/terminal.rs Normal file
View file

@ -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<u16> {
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::<u16>()
.map_err(|_| anyhow!("Invalid width"));
}
Err(anyhow!("Invalid status code"))
}
pub fn width() -> u16 {
if let Ok((w, _)) = terminal::size() {
w
} else {
width_with_shell_out().unwrap_or(FALLBACK_WIDTH)
}
}
pub fn parse_ansi(ansi: &str) -> Option<style::Color> {
style::Color::parse_ansi(&format!("5;{}", ansi))
}
#[derive(Debug, Clone)]
pub struct Color(pub style::Color);
impl FromStr for Color {
type Err = &'static str;
fn from_str(ansi: &str) -> Result<Self, Self::Err> {
if let Some(c) = parse_ansi(ansi) {
Ok(Color(c))
} else {
Err("Invalid color")
}
}
}

36
src/common/tracing.rs.bkp Normal file
View file

@ -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);
}
}
}

View file

@ -1,4 +1,5 @@
use crate::shell::{self, ShellSpawnError};
use crate::common::shell::{self, ShellSpawnError};
use crate::prelude::*;
use anyhow::Result;
use shell::EOF;

View file

@ -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<Self, Self::Err> {
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<Self, Self::Err> {
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<Self, Self::Err> {
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<String>,
},
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<String>, Option<String>),
Filesystem(Option<String>),
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 {
}
}
}
*/

View file

@ -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<String>,

View file

@ -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<dyn Fetcher> {
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()),
}
}

View file

@ -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);

View file

@ -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";

View file

@ -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<String> {
@ -76,7 +76,7 @@ pub fn cheat_paths(path: Option<String>) -> Result<String> {
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<PathBuf> {
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<String>) -> (Option<Vec<String>>, Option<Vec<String>>) {
let mut allowlist = None;
let mut denylist: Option<Vec<String>> = 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<String>,
allowlist: Option<Vec<String>>,
denylist: Option<Vec<String>>,
files: RefCell<Vec<String>>,
}
impl Fetcher {
pub fn new(path: Option<String>, tag_rules: Option<String>) -> Self {
let (allowlist, denylist) = gen_lists(tag_rules);
pub fn new(path: Option<String>) -> Self {
Self {
path,
allowlist,
denylist,
files: Default::default(),
}
}
}
impl fetcher::Fetcher for Fetcher {
fn fetch(
&self,
stdin: &mut std::process::ChildStdin,
files: &mut Vec<String>,
) -> Result<Option<VariableMap>> {
let mut variables = VariableMap::new();
fn fetch(&self, parser: &mut Parser) -> Result<bool> {
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<String> {
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 = {

View file

@ -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<F>(&self, opts: Opts, stdin_fn: F) -> Result<(String, Option<VariableMap>, Vec<String>)>
where
F: Fn(&mut process::ChildStdin, &mut Vec<String>) -> Result<Option<VariableMap>>;
}
fn parse(out: Output, opts: Opts) -> Result<String> {
let text = match out.status.code() {
Some(0) | Some(1) | Some(2) => {
@ -54,10 +46,10 @@ fn parse(out: Output, opts: Opts) -> Result<String> {
post::process(output, opts.column, opts.delimiter.as_deref(), opts.map)
}
impl Finder for FinderChoice {
fn call<F>(&self, finder_opts: Opts, stdin_fn: F) -> Result<(String, Option<VariableMap>, Vec<String>)>
impl FinderChoice {
pub fn call<F, R>(&self, finder_opts: Opts, stdin_fn: F) -> Result<(String, R)>
where
F: Fn(&mut process::ChildStdin, &mut Vec<String>) -> Result<Option<VariableMap>>,
F: Fn(&mut dyn Write) -> Result<R>,
{
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))
}
}

View file

@ -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;

View file

@ -1,5 +1,5 @@
use crate::config::CONFIG;
use crate::filesystem;
use crate::prelude::*;
#[derive(Debug, PartialEq, Clone)]
pub struct Opts {

View file

@ -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<dyn Fetcher> = 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(())
}

View file

@ -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<String>) -> Result<()> {
match func {
Func::UrlOpen => url::open(args),
Func::Welcome => welcome::main(),
Func::WidgetLastCommand => shell::widget_last_command(),
Func::MapExpand => cheat_variable::map_expand(),
}
}

View file

@ -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(())
}

View file

@ -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()
}
},
},
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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;

View file

@ -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<FinderOpts>)> {
Ok((variable, command, command_options))
}
fn write_cmd(
item: &Item,
stdin: &mut std::process::ChildStdin,
allowlist: Option<&Vec<String>>,
denylist: Option<&Vec<String>>,
visited_lines: &mut HashSet<u64>,
) -> 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<Item = Result<String>>,
id: &str,
file_index: usize,
variables: &mut VariableMap,
visited_lines: &mut HashSet<u64>,
stdin: &mut std::process::ChildStdin,
allowlist: Option<&Vec<String>>,
denylist: Option<&Vec<String>>,
) -> Result<()> {
let mut item = Item::new();
item.file_index = file_index;
#[derive(Clone, Default)]
pub struct FilterOpts {
pub allowlist: Vec<String>,
pub denylist: Vec<String>,
pub hash: Option<u64>,
}
let mut should_break = false;
pub struct Parser<'a> {
pub variables: VariableMap,
visited_lines: HashSet<u64>,
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<Item = Result<String>>,
id: &str,
file_index: Option<usize>,
) -> 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)]

9
src/prelude.rs Normal file
View file

@ -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<()>;
}

121
src/serializer.rs Normal file
View file

@ -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::<String>())
} 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<Item> {
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)
}
}

View file

@ -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<SourceError>(command: impl Into<String>, 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(())
}

View file

@ -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<Opts>);
#[derive(Clone)]
#[derive(Clone, Default)]
pub struct VariableMap {
variables: HashMap<u64, HashMap<String, Suggestion>>,
dependencies: HashMap<u64, Vec<u64>>,
}
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) {

View file

@ -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<String>,
) -> Result<Option<VariableMap>>;
fn fetch(&self, parser: &mut Parser) -> Result<bool>;
fn files(&self) -> Vec<String> {
vec![]
}
}

View file

@ -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<usize>,
pub icon: Option<String>,
}
impl Item {
pub fn new() -> Self {
pub fn new(file_index: Option<usize>) -> 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()))
}
}

View file

@ -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<u16> {
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::<u16>()
.map_err(|_| anyhow!("Invalid width"));
}
Err(anyhow!("Invalid status code"))
}
pub fn width() -> u16 {
if let Ok((w, _)) = terminal::size() {
w
} else {
width_with_shell_out().unwrap_or(FALLBACK_WIDTH)
}
}
pub fn parse_ansi(ansi: &str) -> Option<style::Color> {
style::Color::parse_ansi(&format!("5;{}", ansi))
}
#[derive(Debug, Clone)]
pub struct Color(pub style::Color);
impl FromStr for Color {
type Err = &'static str;
fn from_str(ansi: &str) -> Result<Self, Self::Err> {
if let Some(c) = parse_ansi(ansi) {
Ok(Color(c))
} else {
Err("Invalid color")
}
}
}

View file

@ -1,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),
)
}

View file

@ -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<bool> {
populate_cheatsheet(parser)?;
Ok(true)
}
}

View file

@ -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::<String>())
} 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,
)
}