From a0c5a6293bac092e2ac980c71e0d1ca131290aa4 Mon Sep 17 00:00:00 2001 From: Denis Isidoro Date: Mon, 28 Oct 2019 16:56:19 -0300 Subject: [PATCH] Refactor list visualization (#138) Fixes #137 ![Demo](https://user-images.githubusercontent.com/3226564/67512211-b2e99a00-f66e-11e9-8ffe-09f30cb54599.png) --- README.md | 8 +++- cheats/compression.cheat | 2 +- cheats/crontab.cheat | 2 +- cheats/docker.cheat | 4 +- docstring.txt | 52 +++++++++++++++++++++++++ navi | 33 ---------------- src/arg.sh | 2 +- src/cheat.sh | 76 +++++++++++++++++++++++++++++++++---- src/main.sh | 6 +-- src/opts.sh | 17 +++++++-- src/selection.sh | 82 +++++++++++++++++++++++++++------------- src/str.sh | 15 ++++++++ src/ui.sh | 36 ++++++++++++++++-- 13 files changed, 252 insertions(+), 83 deletions(-) create mode 100644 docstring.txt diff --git a/README.md b/README.md index c2357c7..337eb64 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ as a [shell widget](#shell-widget) with no additional setup. > to be able to run the command interactively, you will need to do one of the following: - Install it to /usr/bin/local (via `sudo make install`) -- Manually set your `PATH` so that navi can be found. +- Manually set `$PATH` so that navi can be found. You can manually update your path by adding a line like this in your `.zshrc`: @@ -250,6 +250,12 @@ List customization Lists can be stylized with the [$FZF_DEFAULT_OPTS](https://github.com/junegunn/fzf) environment variable. This way, you can change the [color scheme](https://github.com/junegunn/fzf/wiki/Color-schemes), for example. +In addition: +- the `--fzf-overrides` option allows you to hide columns, for example +- the `--col-widths` option allows you to limit column widths + +Please refer to `navi --help` for more details. + Related projects ---------------- diff --git a/cheats/compression.cheat b/cheats/compression.cheat index 248d01b..f71c750 100644 --- a/cheats/compression.cheat +++ b/cheats/compression.cheat @@ -1,4 +1,4 @@ -% tar, zip, gzip, compression +% compression # Create a tar containing files tar cf .tar diff --git a/cheats/crontab.cheat b/cheats/crontab.cheat index e7350b7..8f8fd0e 100644 --- a/cheats/crontab.cheat +++ b/cheats/crontab.cheat @@ -1,4 +1,4 @@ -% crontab, scheduling +% crontab, schedule # List cron jobs crontab -l diff --git a/cheats/docker.cheat b/cheats/docker.cheat index c0aa787..ff8b7a8 100644 --- a/cheats/docker.cheat +++ b/cheats/docker.cheat @@ -1,4 +1,4 @@ -% docker, container +% docker # Remove an image docker image rm @@ -47,7 +47,7 @@ $ container_id: docker ps --- --headers 1 --column 1 -% docker-compose, container +% docker-compose # Builds, (re)creates, starts, and attaches to containers for all services docker-compose up diff --git a/docstring.txt b/docstring.txt new file mode 100644 index 0000000..808c497 --- /dev/null +++ b/docstring.txt @@ -0,0 +1,52 @@ +An interactive cheatsheet tool for the command-line + +Usage: + navi [command] [...] [options] + +Commands: + search Search for cheatsheets on online repositories + query Pre-filter results + best ... Considers the best match + widget Prints the path for the widget file to be sourced + +Options: + --print Prevent script execution + --path List of :-separated paths to look for cheats + --no-interpolation Prevent argument interpolation + --no-preview Hide command preview window + --fzf-overrides Overrides for fzf commands [default: --with-nth 3,1,2 --exact] + --col-widths Set column widths [default: 15,50,0] + +Environment variables: + NAVI_PATH List of :-separated paths to look for cheats + FZF_DEFAULT_OPTS Default fzf options (e.g. '--layout=reverse') + +Examples: + navi # default behavior + navi --print # doesn't execute the snippet + navi --path '/some/dir:/other/dir' # uses custom cheats + navi search docker # uses online data + navi query git # filter results by "git" + navi best 'sql create db' root mydb # uses a snippet as a CLI + source "$(navi widget zsh)" # loads the zsh widget + navi --col-widths 10,50,0 # limits the first two columns's width + navi --fzf-overrides '--with-nth 1,2' # shows only the comment and tag columns + navi --fzf-overrides '--nth 1,2' # search will consider only the first two columns + navi --fzf-overrides '--no-exact' # looser search algorithm + +More info: + + search + http://cheat.sh is used for cheatsheet retrieval + please note that these cheatsheets may not work out of the box + always check the preview window before executing commands! + + --col-widths + the number is the percentage relative to the terminal width + 0 means that a column will be as wide as necessary + + fzf's --with-nth + 1 refers to the comment column; 2, snippet; 3, tag + + full docs + please refer to the README at https://github.com/denisidoro/navi diff --git a/navi b/navi index abae566..a9fc27e 100755 --- a/navi +++ b/navi @@ -2,41 +2,8 @@ set -euo pipefail export NAVI_HOME="$(cd "$(dirname "$0")" && pwd)" - source "${NAVI_HOME}/src/main.sh" -##? An interactive cheatsheet tool for the command-line -##? -##? Usage: -##? navi [command] [...] [options] -##? -##? Commands: -##? search Search for cheatsheets on online repositories -##? query Pre-filter results -##? best ... Considers the best match -##? -##? Options: -##? --print Prevent script execution -##? --path List of paths to look for cheats -##? --no-interpolation Prevent argument interpolation -##? --no-preview Hide command preview window -##? -##? Examples: -##? navi -##? navi --path '/some/folder:/another/folder' -##? navi search awk -##? navi search docker --print -##? navi query git -##? navi best 'sql create db' root mydb -##? -##? More info: -##? search -##? Queries cheatsheets from http://cheat.sh -##? Please note that these cheatsheets may not work out of the box -##? Always check the preview window before executing commands! -##? full docs -##? Please refer to the README at https://github.com/denisidoro/navi - VERSION="0.14.3" NAVI_ENV="${NAVI_ENV:-prod}" diff --git a/src/arg.sh b/src/arg.sh index cebc8f3..e17f376 100644 --- a/src/arg.sh +++ b/src/arg.sh @@ -38,7 +38,7 @@ arg::next() { } arg::deserialize() { - local arg="$1" + local arg="${1:-}" arg="${arg:1:${#arg}-2}" echo "$arg" \ diff --git a/src/cheat.sh b/src/cheat.sh index 0903557..09497e4 100755 --- a/src/cheat.sh +++ b/src/cheat.sh @@ -41,15 +41,68 @@ cheat::memoized_read_all() { | cheat::join_lines } +# TODO: move this elsewhere +cheat::get_index() { + local -r txt="$1" + local -r ref="$2" + + local -r i="$(echo "$txt" | grep "${ref}\$" | awk '{print $1}')" + echo $((i - 1)) +} + +# TODO: move this elsewhere +cheat::with_nth() { + grep -Eo 'with\-nth +([^ ]+)' | awk '{print $NF}' +} + cheat::prettify() { - awk 'function color(c,s) { - printf("\033[%dm%s\033[0m",30+c,s) + local -r print="$(dict::get "$OPTIONS" print)" + local -r widths="$(dict::get "$OPTIONS" col-widths | tr ',' $'\n')" + local -r numbered_with_nth="$(dict::get "$OPTIONS" fzf-overrides | cheat::with_nth | tr ',' $'\n' | str::with_line_numbers)" + + if [ -n "$numbered_with_nth" ]; then + local -r comment_index="$(cheat::get_index "$numbered_with_nth" 1 2>/dev/null)" + local -r snippet_index="$(cheat::get_index "$numbered_with_nth" 2 2>/dev/null)" + local -r tag_index="$(cheat::get_index "$numbered_with_nth" 3 2>/dev/null)" + local -r comment_width="$(echo "$widths" | coll::get $comment_index 2>/dev/null || echo 0)" + local -r snippet_width="$(echo "$widths" | coll::get $snippet_index 2>/dev/null || echo 0)" + local -r tag_width="$(echo "$widths" | coll::get $tag_index 2>/dev/null || echo 0)" + local -r columns="$(ui::width)" + else + local -r comment_width=0 + local -r snippet_width=0 + local -r tag_width=0 + local -r columns=0 + fi + + awk \ + -v COMMENT_MAX=$((columns * comment_width / 100)) \ + -v SNIPPET_MAX=$((columns * snippet_width / 100)) \ + -v TAG_MAX=$((columns * tag_width / 100)) \ + -v SEP="$ESCAPE_CHAR_3" \ + 'function color(c,s,max) { + if (max > 0 && length(s) > max) { + s=substr(s, 0, max) + s=s"…" + } + printf("\033[%dm%s", c, s) } - /^%/ { tags=" ["substr($0, 3)"]"; next } - /^#/ { print color(4, $0) color(60, tags); next } + /^%/ { tags=substr($0, 3); next } + /^#/ { comment=substr($0, 3); next } /^\$/ { next } - NF { print color(7, $0) color(60, tags); next }' + BEGIN { ORS="" } + NF { + print color(34, comment, COMMENT_MAX) + print color(0, SEP, 0) + print color(37, $0, SNIPPET_MAX) + print color(0, SEP, 0) + print color(90, tags, TAG_MAX); + print color(0, SEP, 0) + print color(90, "\033", 0); + print "\n" + next + }' } cheat::until_percentage() { @@ -60,13 +113,20 @@ cheat::until_percentage() { { print $0 }' } +cheat::from_tags() { + local -r cheats="$1" + local -r tags="$2" + + echo "$cheats" \ + | grep "% ${tags}" -A99999 \ + | cheat::until_percentage +} + cheat::from_selection() { local -r cheats="$1" local -r selection="$2" local -r tags="$(dict::get "$selection" tags)" - echo "$cheats" \ - | grep "% ${tags}" -A99999 \ - | cheat::until_percentage + cheat::from_tags "$cheats" "$tags" } diff --git a/src/main.sh b/src/main.sh index 0457ac1..79f70af 100644 --- a/src/main.sh +++ b/src/main.sh @@ -26,7 +26,7 @@ handler::main() { local -r interpolation="$(dict::get "$OPTIONS" interpolation)" - local cmd="$(selection::cmd "$selection" "$cheat")" + local cmd="$(selection::snippet "$selection")" local result arg value local i=0 @@ -50,10 +50,10 @@ handler::main() { handler::preview() { local -r query="$1" - local -r selection="$(echo "$query" | selection::dict)" local -r cheats="$(cheat::memoized_read_all)" + local -r selection="$(echo "$query" | selection::dict "$cheats")" local -r cheat="$(cheat::from_selection "$cheats" "$selection")" - [ -n "$cheat" ] && selection::cmd_or_comment "$selection" "$cheat" | cmd::unescape + [ -n "$cheat" ] && ui::print_preview "$selection" } handler::help() { diff --git a/src/opts.sh b/src/opts.sh index 2ae9034..54aaf1e 100644 --- a/src/opts.sh +++ b/src/opts.sh @@ -2,8 +2,8 @@ set -euo pipefail opts::extract_help() { - local -r file="$1" - grep "^##?" "$file" | cut -c 5- + local -r file="${NAVI_HOME}/docstring.txt" + cat "$file" } opts::eval() { @@ -17,6 +17,8 @@ opts::eval() { local best=false local query="" local values="" + local col_widths="15,50,0" + local fzf_overrides="--with-nth 3,1,2 --exact" case "${1:-}" in --version|version) entry_point="version"; shift ;; @@ -39,14 +41,21 @@ opts::eval() { search) query="$arg"; wait_for=""; path="${path}:$(search::full_path "$query")"; continue ;; query|best) query="$arg"; wait_for=""; continue ;; widget) SH="$arg"; wait_for=""; continue ;; + col-widths) col_widths="$(echo "$arg" | xargs | tr ' ' ',')"; wait_for=""; continue ;; + fzf-overrides) fzf_overrides="$arg" ; wait_for=""; continue ;; esac case $arg in --print) print=true ;; --no-interpolation) interpolation=false ;; + --interpolation) interpolation=true ;; --no-preview) preview=false ;; + --preview) preview=true ;; --path|--dir) wait_for="path" ;; --no-autoselect) autoselect=false ;; + --autoselect) autoselect=true ;; + --col-widths) wait_for="col-widths" ;; + --fzf-overrides) wait_for="fzf-overrides" ;; *) values="$(echo "$values" | coll::add "$arg")" ;; esac done @@ -59,7 +68,9 @@ opts::eval() { autoselect "$autoselect" \ query "$query" \ best "$best" \ - values "$values")" + values "$values" \ + fzf-overrides "$fzf_overrides" \ + col-widths "$col_widths")" export NAVI_PATH="$path" } diff --git a/src/selection.sh b/src/selection.sh index a88c132..105eb78 100644 --- a/src/selection.sh +++ b/src/selection.sh @@ -1,41 +1,69 @@ #!/usr/bin/env bash -selection::dict() { - local -r str="$(cat)" +SELECTION_ESCAPE_STR=" " - local -r tags="$(echo "$str" | awk -F'[' '{print $NF}' | tr -d ']')" - local -r core="$(echo "$str" | sed -e "s/ \[${tags}\]$//")" - - dict::new core "$core" tags "$tags" | sed "s/'''/'/g" +selection_str::cleanup() { + sed -E "s/ +/${SELECTION_ESCAPE_STR}/g" } -selection::core_is_comment() { - grep -qE '^#' +selection_str::without_ellipsis() { + tr -d "…" } -selection::cmd_or_comment() { - local -r selection="$1" - local -r cheat="$2" - local -r always_cmd="${3:-false}" +selection_str::comment() { + echo "$*" | awk -F "${SELECTION_ESCAPE_STR}" '{print $1}' | selection_str::without_ellipsis +} - local -r core="$(echo "$selection" | dict::get core)" +selection_str::snippet() { + echo "$*" | awk -F "${SELECTION_ESCAPE_STR}" '{print $2}' | selection_str::without_ellipsis +} - if echo "$core" | selection::core_is_comment; then - echo "$cheat" \ - | grep "$core" -A999 \ - | str::last_paragraph_line \ - | cmd::escape - elif $always_cmd; then - echo "$core" +selection_str::tags() { + echo "$*" | awk -F "${SELECTION_ESCAPE_STR}" '{print $3}' | selection_str::without_ellipsis +} + +selection::resolve_ellipsis() { + local -r str="$(selection_str::cleanup)" + local -r cheats="$*" + + if echo "$str" | grep -q "…"; then + local -r comment="$(selection_str::comment "$str")" + local -r snippet="$(selection_str::snippet "$str")" + local -r tags="$(selection_str::tags "$str")" + local -r cheat="$(cheat::from_tags "$cheats" "$tags")" + + local -r tags2="$(echo "$cheat" | head -n1 | str::sub 2)" + local -r comment2="$(echo "$cheat" | grep "$comment" | str::sub 2)" + local -r snippet2="$(echo "$cheat" | grep "$comment2" -A 999| str::last_paragraph_line)" + + echo "${comment2}${SELECTION_ESCAPE_STR}${snippet2}${SELECTION_ESCAPE_STR}${tags2}" else - echo "$cheat" \ - | grep "^${core}$" -B999 \ - | str::reverse_lines \ - | str::last_paragraph_line \ - | cmd::escape + echo "$str" fi } -selection::cmd() { - selection::cmd_or_comment "$@" true +selection::dict() { + local -r cheats="$1" + local -r str="$(selection::resolve_ellipsis "$cheats")" + + local -r comment="$(selection_str::comment "$str")" + local -r snippet="$(selection_str::snippet "$str")" + local -r tags="$(selection_str::tags "$str")" + + dict::new comment "$comment" snippet "$snippet" tags "$tags" | sed "s/'''/'/g" } + +selection::comment() { + local -r selection="$1" + dict::get "$selection" comment +} + +selection::snippet() { + local -r selection="$1" + dict::get "$selection" snippet +} + +selection::tags() { + local -r selection="$1" + dict::get "$selection" tags +} \ No newline at end of file diff --git a/src/str.sh b/src/str.sh index abd50d1..b817be1 100644 --- a/src/str.sh +++ b/src/str.sh @@ -62,4 +62,19 @@ str::not_empty() { str::remove_empty_lines() { sed '/^$/d' +} + +str::as_column() { + local -r txt="$(cat)" + local -r separator="$1" + + if command_exists column; then + echo "$txt" | column -t -s "$separator" + else + echo "$txt" | awk -F "$separator" -vOFS='\t' 'NF > 0 { $1 = $1 } 1' + fi +} + +str::with_line_numbers() { + awk '{printf("%d %s\n", NR,$0)}' } \ No newline at end of file diff --git a/src/ui.sh b/src/ui.sh index 0be16d6..497c56e 100644 --- a/src/ui.sh +++ b/src/ui.sh @@ -2,6 +2,9 @@ ui::fzf() { local -r autoselect="$(dict::get "$OPTIONS" autoselect)" + local -r with_nth="$(dict::get "$OPTIONS" with-nth)" + local -r nth="$(dict::get "$OPTIONS" nth)" + local -r fzf_overrides="$(dict::get "$OPTIONS" fzf-overrides)" local args args+=("--height") @@ -10,6 +13,9 @@ ui::fzf() { args+=("--select-1") fi + local fzf_opts="${FZF_DEFAULT_OPTS:---height 70% --reverse --border --inline-info --cycle}" + export FZF_DEFAULT_OPTS="${FZF_DEFAULT_OPTS} ${fzf_overrides}" + local -r fzf_cmd="$([ $NAVI_ENV == "test" ] && echo "fzf_mock" || echo "fzf")" "$fzf_cmd" ${args[@]:-} --inline-info "$@" } @@ -18,7 +24,7 @@ ui::select() { local -r cheats="$1" local -r script_path="${NAVI_HOME}/navi" - local -r preview_cmd="echo \'{}\' | $(arg::serialize_code) | xargs -I% \"${script_path}\" preview %" + local -r preview_cmd="\"${script_path}\" preview \$(echo \'{}\' | $(arg::serialize_code))" local -r query="$(dict::get "$OPTIONS" query)" local -r entry_point="$(dict::get "$OPTIONS" entry_point)" @@ -30,7 +36,7 @@ ui::select() { args+=("--ansi") if $preview; then args+=("--preview"); args+=("$preview_cmd") - args+=("--preview-window"); args+=("up:1") + args+=("--preview-window"); args+=("up:2") fi if [[ -n "$query" ]] && $best; then args+=("--filter"); args+=("${query} ") @@ -40,14 +46,38 @@ ui::select() { if [ "$entry_point" = "search" ]; then args+=("--header"); args+=("Displaying online results. Please refer to 'navi --help' for details") fi + args+=("--delimiter"); args+=('\s\s+'); echo "$cheats" \ | cheat::prettify \ + | str::as_column $(printf "$ESCAPE_CHAR_3") \ | ui::fzf "${args[@]}" \ | ($best && head -n1 || cat) \ - | selection::dict + | selection::dict "$cheats" } ui::clear_previous_line() { tput cuu1 2>/dev/null && tput el || true } + +ui::width() { + shopt -s checkwinsize; (:;:) 2> /dev/null || true + if command_exists tput; then + tput cols + else + echo 130 + fi +} + +ui::print_preview() { + local -r selection="$1" + + local -r comment="$(selection::comment "$selection" | cmd::unescape)" + local -r snippet="$(selection::snippet "$selection" | cmd::unescape)" + local -r tags="$(selection::tags "$selection" | cmd::unescape)" + + printf '\033[34m# '; echo -n "$comment" + printf " \033[90m["; echo -n "$tags"; echo "]" + printf '\033[0m' + echo "$snippet" +} \ No newline at end of file