├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── config.yml └── workflows │ ├── cd.yml │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── assets ├── icon.png └── icon.svg ├── docs ├── README.md ├── cheatsheet │ ├── README.md │ ├── getting-started │ │ └── README.md │ ├── repositories │ │ └── README.md │ └── syntax │ │ └── README.md ├── configuration │ └── README.md ├── contributions │ ├── README.md │ ├── bugs │ │ └── README.md │ ├── code │ │ └── README.md │ └── documentation │ │ └── README.md ├── deprecated │ └── Alfred │ │ └── README.md ├── examples │ ├── cheatsheet │ │ ├── example.cheat │ │ └── navi.cheat │ └── configuration │ │ └── config-example.yaml ├── installation │ └── README.md ├── usage │ ├── README.md │ ├── commands │ │ ├── info │ │ │ └── README.md │ │ └── repo │ │ │ └── README.md │ ├── fzf-overrides │ │ └── README.md │ └── shell-scripting │ │ └── README.md └── widgets │ ├── README.md │ └── howto │ ├── TMUX.md │ └── VIM.md ├── rust-toolchain.toml ├── rustfmt.toml ├── scripts ├── docker ├── dot ├── install ├── make ├── release └── test ├── shell ├── navi.plugin.bash ├── navi.plugin.elv ├── navi.plugin.fish ├── navi.plugin.nu ├── navi.plugin.ps1 └── navi.plugin.zsh ├── src ├── bin │ └── main.rs ├── clients │ ├── cheatsh.rs │ ├── mod.rs │ └── tldr.rs ├── commands │ ├── core │ │ ├── actor.rs │ │ └── mod.rs │ ├── func │ │ ├── map.rs │ │ ├── mod.rs │ │ └── widget.rs │ ├── info.rs │ ├── mod.rs │ ├── preview │ │ ├── mod.rs │ │ ├── var.rs │ │ └── var_stdin.rs │ ├── repo │ │ ├── add.rs │ │ ├── browse.rs │ │ └── mod.rs │ ├── shell.rs │ └── temp.rs ├── common │ ├── clipboard.rs │ ├── deps.rs │ ├── fs.rs │ ├── git.rs │ ├── hash.rs │ ├── mod.rs │ ├── shell.rs │ ├── terminal.rs │ └── url.rs ├── config │ ├── cli.rs │ ├── env.rs │ ├── mod.rs │ └── yaml.rs ├── deser │ ├── mod.rs │ ├── raycast.rs │ └── terminal.rs ├── env_var.rs ├── filesystem.rs ├── finder │ ├── mod.rs │ ├── post.rs │ └── structures.rs ├── lib.rs ├── libs │ └── dns_common │ │ ├── component.rs │ │ ├── mod.rs │ │ └── tracing.rs ├── parser.rs ├── prelude.rs ├── structures │ ├── cheat.rs │ ├── fetcher.rs │ ├── item.rs │ └── mod.rs └── welcome.rs └── tests ├── cheats ├── more_cases.cheat └── ssh.cheat ├── config.yaml ├── core.bash ├── helpers.sh ├── no_prompt_cheats ├── cases.cheat └── one.cheat ├── run └── tests.rs /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 2 | 3 | * @denisidoro 4 | .github/* @denisidoro 5 | 6 | shell/navi.plugin.ps1 @alexis-opolka 7 | docs/* @alexis-opolka 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: denisidoro 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Versions:** 27 | 28 | - OS: [e.g. macOS, WSL ubuntu, ubuntu] 29 | - Shell Version [replace this text with the output of `sh --version`] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: new feature 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome 2 | 3 | # Comment to be posted to on first time issues 4 | newIssueWelcomeComment: > 5 | Thanks for opening your first issue here! In case you're facing a bug, please update navi to the latest version first. Maybe the bug is already solved! :) 6 | 7 | # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome 8 | 9 | # Comment to be posted to on PRs from first time contributors in your repository 10 | newPRWelcomeComment: > 11 | Thanks for opening this pull request! 12 | 13 | # Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge 14 | 15 | # Comment to be posted to on pull requests merged by a first time user 16 | firstPRMergeComment: > 17 | Congrats on merging your first pull request! 18 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | release: 8 | 9 | jobs: 10 | binary: 11 | name: Publish ${{ matrix.target }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - os: macos-latest 18 | target: x86_64-apple-darwin 19 | - os: ubuntu-latest 20 | target: x86_64-unknown-linux-musl 21 | - os: ubuntu-latest 22 | target: x86_64-pc-windows-gnu 23 | - os: ubuntu-latest 24 | target: armv7-unknown-linux-musleabihf 25 | - os: ubuntu-latest 26 | target: armv7-linux-androideabi 27 | - os: ubuntu-latest 28 | target: aarch64-linux-android 29 | - os: ubuntu-latest 30 | target: aarch64-unknown-linux-gnu 31 | - os: macos-latest 32 | target: aarch64-apple-darwin 33 | steps: 34 | ### We're checking out the repository at the triggered ref 35 | - uses: actions/checkout@v4 36 | 37 | - name: Get the version 38 | id: get_version 39 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 40 | 41 | - name: Check if release exists 42 | id: check_release 43 | run: | 44 | if gh release view ${{ steps.get_version.outputs.VERSION }} > /dev/null 2>&1; then 45 | echo "RELEASE_EXISTS=true" >> $GITHUB_OUTPUT 46 | else 47 | echo "RELEASE_EXISTS=false" >> $GITHUB_OUTPUT 48 | fi 49 | env: 50 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | 52 | - name: Create release 53 | continue-on-error: true 54 | if: steps.check_release.outputs.RELEASE_EXISTS == 'false' 55 | run: | 56 | gh release create ${{ steps.get_version.outputs.VERSION }} \ 57 | --title "Release ${{ steps.get_version.outputs.VERSION }}" \ 58 | --notes "Release notes for ${{ steps.get_version.outputs.VERSION }}" \ 59 | env: 60 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | 62 | - name: Build 63 | id: build 64 | run: scripts/release ${{ matrix.target }} 65 | 66 | - name: Upload binaries to release 67 | run: | 68 | cd ./target/${{ matrix.target }}/release/ 69 | cp navi.${{ steps.build.outputs.EXTENSION }} navi-${{ steps.get_version.outputs.VERSION }}-${{ matrix.target }}.${{ steps.build.outputs.EXTENSION }} 70 | gh release upload ${{ steps.get_version.outputs.VERSION }} navi-${{ steps.get_version.outputs.VERSION }}-${{ matrix.target }}.${{ steps.build.outputs.EXTENSION }} 71 | env: 72 | GH_TOKEN: ${{ github.token }} 73 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/actions-rs/meta/blob/master/recipes/quickstart.md 2 | # 3 | # While our "example" application has the platform-specific code, 4 | # for simplicity we are compiling and testing everything on the Ubuntu environment only. 5 | # For multi-OS testing see the `cross.yml` workflow. 6 | 7 | on: 8 | push: 9 | pull_request: 10 | branches: [master] 11 | 12 | name: CI 13 | 14 | jobs: 15 | # check: 16 | # name: Check 17 | # runs-on: ubuntu-latest 18 | # steps: 19 | # - name: Checkout sources 20 | # uses: actions/checkout@v2 21 | 22 | # - name: Install stable toolchain 23 | # uses: actions-rs/toolchain@v1 24 | # with: 25 | # profile: minimal 26 | # toolchain: stable 27 | # override: true 28 | 29 | # - name: Run cargo check 30 | # uses: actions-rs/cargo@v1 31 | # continue-on-error: false 32 | # with: 33 | # command: check 34 | 35 | test: 36 | name: Tests 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout sources 40 | uses: actions/checkout@v2 41 | 42 | - name: Install stable toolchain 43 | uses: actions-rs/toolchain@v1 44 | with: 45 | profile: minimal 46 | toolchain: stable 47 | override: true 48 | 49 | - name: Prep environment to test compiled-in paths 50 | run: | 51 | mkdir /tmp/cheats-dir 52 | touch /tmp/config-file 53 | 54 | - name: Run cargo test 55 | uses: actions-rs/cargo@v1 56 | continue-on-error: false 57 | env: 58 | NAVI_PATH: /tmp/cheats-dir 59 | NAVI_CONFIG: /tmp/config-file 60 | with: 61 | command: test 62 | 63 | - name: Run cargo test 64 | uses: actions-rs/cargo@v1 65 | continue-on-error: false 66 | with: 67 | command: test 68 | 69 | - name: Install deps 70 | run: ./scripts/dot pkg add git bash npm tmux 71 | 72 | - name: Install fzf 73 | run: git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf; yes | ~/.fzf/install; 74 | 75 | - name: Install tealdeer 76 | run: sudo npm install -g tldr 77 | 78 | - name: Run bash tests 79 | run: ./tests/run 80 | 81 | lints: 82 | name: Lints 83 | runs-on: ubuntu-latest 84 | steps: 85 | - name: Checkout sources 86 | uses: actions/checkout@v2 87 | 88 | # - name: Install stable toolchain 89 | # uses: actions-rs/toolchain@v1 90 | # with: 91 | # profile: minimal 92 | # toolchain: stable 93 | # override: true 94 | # components: rustfmt, clippy 95 | 96 | - name: Run cargo fmt 97 | uses: actions-rs/cargo@v1 98 | continue-on-error: false 99 | with: 100 | command: fmt 101 | args: --all -- --check 102 | 103 | - name: Run cargo clippy 104 | uses: actions-rs/cargo@v1 105 | continue-on-error: false 106 | with: 107 | command: clippy 108 | args: -- -D warnings 109 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | navi.log 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "navi" 3 | version = "2.25.0-beta1" 4 | authors = ["Denis Isidoro ", "Alexis Opolka "] 5 | edition = "2021" 6 | description = "An interactive cheatsheet tool for the command-line" 7 | homepage = "https://github.com/denisidoro/navi" 8 | documentation = "https://github.com/denisidoro/navi" 9 | repository = "https://github.com/denisidoro/navi" 10 | keywords = ["cheatsheets", "terminal", "cli", "tui", "shell"] 11 | categories = ["command-line-utilities"] 12 | license = "Apache-2.0" 13 | 14 | [features] 15 | disable-command-execution = [] 16 | disable-repo-management = [] 17 | 18 | [badges] 19 | travis-ci = { repository = "denisidoro/navi", branch = "master" } 20 | 21 | [dependencies] 22 | regex = { version = "1.7.3", default-features = false, features = [ 23 | "std", 24 | "unicode-perl", 25 | ] } 26 | clap = { version = "4.2.1", features = ["derive", "cargo"] } 27 | crossterm = "0.28.0" 28 | lazy_static = "1.4.0" 29 | etcetera = "0.10.0" 30 | walkdir = "2.3.3" 31 | shellwords = "1.1.0" 32 | anyhow = "1.0.70" 33 | thiserror = "2.0.0" 34 | strip-ansi-escapes = "0.2.0" 35 | edit = "0.1.4" 36 | remove_dir_all = "1.0.0" 37 | serde = { version = "1.0.219", features = ["derive"] } 38 | serde_yaml = "0.9.21" 39 | unicode-width = "0.2.0" 40 | tracing = "0.1.41" 41 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 42 | 43 | [target.'cfg(windows)'.dependencies] 44 | dunce = "1" 45 | 46 | [lib] 47 | name = "navi" 48 | path = "src/lib.rs" 49 | 50 | [[bin]] 51 | name = "navi" 52 | path = "src/bin/main.rs" 53 | bench = false 54 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export PATH := /usr/local/opt/bash/bin/:$(PATH) 2 | 3 | install: 4 | scripts/make install 5 | 6 | uninstall: 7 | scripts/make uninstall 8 | 9 | fix: 10 | scripts/make fix 11 | 12 | test: 13 | scripts/test 14 | 15 | build: 16 | cargo build -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # navi icon [![Actions Status](https://github.com/denisidoro/navi/workflows/CI/badge.svg)](https://github.com/denisidoro/navi/actions) ![GitHub release](https://img.shields.io/github/v/release/denisidoro/navi?include_prereleases) 2 | 3 | An interactive cheatsheet tool for the command-line. 4 | 5 | [![Demo](https://asciinema.org/a/406461.svg)](https://asciinema.org/a/406461) 6 | 7 | **navi** allows you to browse through cheatsheets (that you may write yourself or download from maintainers) and execute commands. Suggested values for arguments are dynamically displayed in a list. 8 | 9 | ## Pros 10 | 11 | - it will spare you from knowing CLIs by heart 12 | - it will spare you from copy-pasting output from intermediate commands 13 | - it will make you type less 14 | - it will teach you new one-liners 15 | 16 | It uses [fzf](https://github.com/junegunn/fzf) or [skim](https://github.com/lotabout/skim) under the hood and it can be either used as a command or as a shell widget (_à la_ Ctrl-R). 17 | 18 | ## Table of contents 19 | 20 | - [Installation](#installation) 21 | - [Usage](#usage) 22 | - [Cheatsheet repositories](#cheatsheet-repositories) 23 | - [Cheatsheet syntax](#cheatsheet-syntax) 24 | - [Customization](#customization) 25 | - [More info](#more-info) 26 | - [Trying out online](#trying-out-online) 27 | - [Similar tools](#similar-tools) 28 | - [Etymology](#etymology) 29 | 30 | ## Installation 31 | 32 | The recommended way to install **navi** is by running: 33 | 34 | ```sh 35 | brew install navi 36 | ``` 37 | 38 | > [!NOTE] 39 | > For more details on how to install Navi, see [docs/installation](docs/installation/README.md) 40 | 41 | **navi** can be installed with the following package managers: 42 | 43 | [![Packaging status](https://repology.org/badge/vertical-allrepos/navi.svg)](https://repology.org/project/navi/versions) 44 | 45 | ## Usage 46 | 47 | There are multiple ways to use **navi**: 48 | 49 | - by typing `navi` in the terminal 50 | - pros: you have access to all possible subcommands and flags 51 | - as a [shell widget](docs/installation/README.md#installing-the-shell-widget) for the terminal 52 | - pros: the shell history is correctly populated (i.e. with the actual command you ran instead of `navi`) and you can edit the command as you wish before executing it 53 | - as a [Tmux widget](docs/widgets/howto/TMUX.md) 54 | - pros: you can use your cheatsheets in any command-line app even in SSH sessions 55 | - as [aliases](docs/cheatsheet/syntax/README.md#aliases) 56 | - as a [shell scripting tool](docs/usage/shell-scripting/README.md) 57 | 58 | In particular, check [these instructions](https://github.com/denisidoro/navi/issues/491) if you want to replicate what's shown in the demo above. 59 | 60 | ## Cheatsheet repositories 61 | 62 | Running **navi** for the first time will help you download and manage cheatsheets. By default, they are stored at `~/.local/share/navi/cheats/`. 63 | 64 | You can also: 65 | 66 | - [browse through featured cheatsheets](docs/usage/commands/repo/README.md#browsing-through-cheatsheet-repositorieea) 67 | - [import cheatsheets from git repositories](docs/cheatsheet/repositories/README.md#importing-cheatsheet-repositories) 68 | - [write your own cheatsheets](#cheatsheet-syntax) (and [share them](docs/cheatsheet/repositories/README.md#submitting-cheatsheets), if you want) 69 | - [use cheatsheets from other tools](docs/cheatsheet/README.md#using-cheatsheets-from-other-tools), such as [tldr](https://github.com/tldr-pages/tldr) and [cheat.sh](https://github.com/chubin/cheat.sh) 70 | - [auto-update repositories](docs/cheatsheet/repositories/README.md#auto-updating-repositories) 71 | - auto-export cheatsheets from your [TiddlyWiki](https://tiddlywiki.com/) notes using a [TiddlyWiki plugin](https://bimlas.github.io/tw5-navi-cheatsheet/) 72 | 73 | ## Cheatsheet syntax 74 | 75 | Cheatsheets are described in `.cheat` files that look like this: 76 | 77 | ```sh 78 | % git, code 79 | 80 | # Change branch 81 | git checkout 82 | 83 | $ branch: git branch | awk '{print $NF}' 84 | ``` 85 | 86 | The full syntax and examples can be found [here](docs/cheatsheet/syntax/README.md). 87 | 88 | ## Customization 89 | 90 | You can: 91 | 92 | - [setup your own config file](docs/configuration/README.md) 93 | - [set custom paths for your config file and cheat sheets](docs/configuration/README.md#paths-and-environment-variables) 94 | - [change colors](docs/configuration/README.md#changing-colors) 95 | - [resize columns](docs/configuration/README.md#resizing-columns) 96 | - [change how search is performed](docs/configuration/README.md#overriding-fzf-options) 97 | 98 | ## More info 99 | 100 | Please run the following command to read more about all possible options: 101 | 102 | ```sh 103 | navi --help 104 | ``` 105 | 106 | In addition, please check the [/docs](docs) folder or the website. 107 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denisidoro/navi/e657f4b6b539f44489290d182c42035e54b8e6db/assets/icon.png -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Navi icon [![Actions Status](https://github.com/denisidoro/navi/workflows/CI/badge.svg)](https://github.com/denisidoro/navi/actions) ![GitHub release](https://img.shields.io/github/v/release/denisidoro/navi?include_prereleases) 2 | 3 | ## Table of Contents 4 | 5 | 6 | * [Navi icon ![Actions Status](https://github.com/denisidoro/navi/workflows/CI/badge.svg) ![GitHub release](https://img.shields.io/github/v/release/denisidoro/navi?include_prereleases)](#navi-img-srchttpsrawgithubusercontentcomdenisidoronavimasterassetsiconpng-alticon-height28px--) 7 | * [Table of Contents](#table-of-contents) 8 | * [About](#about) 9 | * [Navi Pros](#navi-pros) 10 | * [Similar tools](#similar-tools) 11 | * [Etymology](#etymology) 12 | 13 | 14 | ## About 15 | 16 | Navi is an interactive cheatsheet tool for the command-line.\ 17 | It allows you to browse through cheatsheets (that you may write yourself or download from maintainers) and execute commands. 18 | 19 | [![Demo](https://asciinema.org/a/406461.svg)](https://asciinema.org/a/406461) 20 | 21 | It uses [fzf](https://github.com/junegunn/fzf), [skim](https://github.com/lotabout/skim), or [Alfred](https://www.alfredapp.com/) under the hood and it can be either used as a command or as a shell widget (_à la_ Ctrl-R). 22 | 23 | ## Navi Pros 24 | 25 | - it will spare you from knowing CLIs by heart 26 | - it will spare you from copy-pasting output from intermediate commands 27 | - it will make you type less 28 | - it will teach you new one-liners 29 | 30 | ## Similar tools 31 | 32 | There are many similar projects out there ([beavr](https://github.com/denisidoro/beavr), [bro](https://github.com/hubsmoke/bro), [cheat](https://github.com/cheat/cheat), [cheat.sh](https://github.com/chubin/cheat.sh), [cmdmenu](https://github.com/amacfie/cmdmenu), [eg](https://github.com/srsudar/eg), [how2](https://github.com/santinic/how2), [howdoi](https://github.com/gleitz/howdoi), [Command Line Interface Pages](https://github.com/command-line-interface-pages) and [tldr](https://github.com/tldr-pages/tldr), to name a few). 33 | 34 | They are excellent projects, but **navi** remains unique in the following ways: 35 | 36 | - it's natural to write cheatsheets tailored to your needs 37 | - arguments are neither hardcoded nor a simple template 38 | 39 | ## Etymology 40 | 41 | [Navi](https://zelda.gamepedia.com/Navi) is a character from [The Legend of Zelda Ocarina of Time](https://zelda.gamepedia.com/Ocarina_of_Time) that provides [Link](https://zelda.gamepedia.com/Link) with a variety of clues to help him solve puzzles and make progress in his quest. 42 | -------------------------------------------------------------------------------- /docs/cheatsheet/README.md: -------------------------------------------------------------------------------- 1 | # Navi cheatsheets 2 | 3 | 4 | * [Navi cheatsheets](#navi-cheatsheets) 5 | * [Working with `cheatsheet repositories`](#working-with-cheatsheet-repositories) 6 | * [Manually adding cheatsheets to navi](#manually-adding-cheatsheets-to-navi) 7 | * [Choosing between queries and selection with variables](#choosing-between-queries-and-selection-with-variables) 8 | * [Using cheatsheets from other tools](#using-cheatsheets-from-other-tools) 9 | 10 | 11 | ## Working with `cheatsheet repositories` 12 | 13 | Navi works best with what we call `cheatsheet repositories`, for more details see [cheatsheet/repositories](repositories/README.md). 14 | 15 | ## Manually adding cheatsheets to navi 16 | 17 | If you don't want to work with `cheatsheet repositories`, you can manually add your 18 | cheatsheets to navi by putting them into the `cheats_path` of your platform. 19 | 20 | You can find out your path using the [info](/docs/usage/commands/info/README.md) subcommands 21 | but a quick working command to go there would be: 22 | 23 | - Before 2.25.0 24 | 25 | ```bash 26 | cd $(navi info cheats-path) 27 | ``` 28 | 29 | - After 2.25.0 30 | 31 | ```bash 32 | cd $(navi info default-cheats-path) 33 | ``` 34 | 35 | ## Choosing between queries and selection with variables 36 | 37 | Navi lets you use different methods to fill a variable value, when prompted. 38 | 39 | | Keyboard key | Preference | 40 | |:------------------:|:--------------------------:| 41 | | tab | The query is preferred | 42 | | enter | The selection is preferred | 43 | 44 | It means if you enter the tab key, navi will let you enter the value. 45 | 46 | ## Using cheatsheets from other tools 47 | 48 | > [!WARNING] 49 | > Navi **DOESN'T SUPPORT** as of now importing cheatsheets from other tools 50 | > but is able to **work with** TLDR and Cheat.sh. 51 | 52 | ![Demo](https://user-images.githubusercontent.com/3226564/91878474-bae27500-ec55-11ea-8b19-17876178e887.gif) 53 | 54 | You can use cheatsheets from [tldr](https://github.com/tldr-pages/tldr) by running: 55 | 56 | ```sh 57 | navi --tldr 58 | ``` 59 | 60 | You can use cheatsheets from [cheat.sh](https://github.com/chubin/cheat.sh) by running: 61 | 62 | ```sh 63 | navi --cheatsh 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/cheatsheet/getting-started/README.md: -------------------------------------------------------------------------------- 1 | # Cheatsheets - Getting started 2 | -------------------------------------------------------------------------------- /docs/cheatsheet/repositories/README.md: -------------------------------------------------------------------------------- 1 | # Cheatsheet repositories 2 | 3 | 4 | * [Cheatsheet repositories](#cheatsheet-repositories) 5 | * [About](#about) 6 | * [Importing cheatsheet repositories](#importing-cheatsheet-repositories) 7 | * [Submitting cheatsheets](#submitting-cheatsheets) 8 | * [Auto-updating repositories](#auto-updating-repositories) 9 | 10 | 11 | ## About 12 | 13 | Navi lets you work with what we call `cheatsheet repositories`, they are git repositories 14 | and mainly consists of `.cheat` files. 15 | 16 | This page is dedicated to the information you might need to work with `cheatsheet repositories`. 17 | 18 | ## Importing cheatsheet repositories 19 | 20 | You can import `cheatsheet repositories` with the `repo add` subcommand.\ 21 | See [/docs/usage/commands/repo](/docs/usage/commands/repo/README.md#importing-cheatsheet-repositories) for more details. 22 | 23 | ## Submitting cheatsheets 24 | 25 | The featured repository for cheatsheets is [denisidoro/cheats](https://github.com/denisidoro/cheats), 26 | feel free to open a PR[^1] there for me to include your contributions. 27 | 28 | In order to add your own repository as a featured cheatsheet repo, please [edit this file](https://github.com/denisidoro/cheats/edit/master/featured_repos.txt) and open a PR[^1]. 29 | 30 | ## Auto-updating repositories 31 | 32 | Right now, **navi** doesn't have support for auto-updating out of the box. 33 | However, you can achieve this by using `git` and `crontab`. 34 | 35 | - First make sure you cloned your repo using `git` to the correct folder: 36 | 37 | ```sh 38 | user="" 39 | repo="" 40 | git clone "https://github.com/${user}/${repo}" "$(navi info cheats-path)/${user}__${repo}" 41 | ``` 42 | 43 | - Then, add a cron job: 44 | 45 | ```sh 46 | crontab -e 47 | */0 11 * * * bash -c 'cd "$(/usr/local/bin/navi info cheats-path)/__" && /usr/local/bin/git pull -q origin master' 48 | ``` 49 | 50 | > [!NOTE] 51 | > Please note the cron job above is just an example **AND** you should edit it accordingly: 52 | > 53 | >- In this example, the cron job is triggered every day at 11am. 54 | > 55 | > You might want to check out [crontab guru](https://crontab.guru/) regarding crontab. 56 | > 57 | >- The full paths to `navi` and `git` may differ in your setup. 58 | > 59 | > Check their actual values using `which` as `which `. 60 | > 61 | >- Don't forget to replace `__` with the actual folder name 62 | 63 | [^1]: A *PR* is short for Pull Request 64 | -------------------------------------------------------------------------------- /docs/cheatsheet/syntax/README.md: -------------------------------------------------------------------------------- 1 | # The syntax of a Navi cheatsheet 2 | 3 | 4 | * [The syntax of a Navi cheatsheet](#the-syntax-of-a-navi-cheatsheet) 5 | * [Syntax overview](#syntax-overview) 6 | * [Variables](#variables) 7 | * [Advanced variable options](#advanced-variable-options) 8 | * [Variable dependency](#variable-dependency) 9 | * [Implicit dependencies](#implicit-dependencies) 10 | * [Explicit dependencies](#explicit-dependencies) 11 | * [Variable as multiple arguments](#variable-as-multiple-arguments) 12 | * [Extending cheats](#extending-cheats) 13 | * [Multiline commands/snippets](#multiline-commandssnippets) 14 | * [Aliases](#aliases) 15 | 16 | 17 | ## Syntax overview 18 | 19 | Cheats are described in cheatsheet files.\ 20 | A cheatsheet is a file that has a `.cheat` or `.cheat.md` extension and looks like this: 21 | 22 | ```sh 23 | % git, code 24 | 25 | # Change branch 26 | git checkout 27 | 28 | $ branch: git branch | awk '{print $NF}' 29 | ``` 30 | 31 | A cheatsheet can have the following elements: 32 | 33 | | Element | Syntax | Description | 34 | |:--------------------------------:|:------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| 35 | | Tags as cheat titles | `%` | Lines starting with this character are considered the start of a new cheat command and should contain tags. | 36 | | Cheat Description | `#` | Lines starting with this character should be the description of the cheat you're writing. | 37 | | Cheat Comments (or Metacomments) | `;` | Lines starting with this character will be ignored by navi but they can be great as editor's comments. | 38 | | Pre-defined variables | `$` | Lines starting with this character should contain commands that generate a list of possible values.

:information_source: See [#variables](#variables) for more details. | 39 | | Extended cheats | `@` | Lines starting with this character should contain tags associated to other defined cheats.

:information_source: See [#extending-cheats](#extending-cheats) for more details. | 40 | | Executable commands | N/A | All other non-empty lines are considered as executable commands. | 41 | 42 | > [!TIP] 43 | > If you are editing cheatsheets in Visual Studio Code, you could enable syntax highlighting 44 | > by installing this extension: [@yanivmo/navi-cheatsheet-language](https://marketplace.visualstudio.com/items?itemName=yanivmo.navi-cheatsheet-language). 45 | 46 | ## Variables 47 | 48 | Variables are defined with brackets inside executable commands (e.g. ``).\ 49 | Variable names should only include alphanumeric characters and `_`. 50 | 51 | You can show suggestions by using the Pre-defined variable lines (i.e. lines starting with`$`).\ 52 | Otherwise, the user will be able to type any value for it. 53 | 54 | ### Advanced variable options 55 | 56 | For Pre-Defined variable lines, you can use `---` to customize the behavior of `fzf` 57 | or how the value is going to be used. 58 | 59 | Below are examples of such customization: 60 | 61 | - We define what column to use, the number of header lines and a delimiter between values. 62 | 63 | ```sh 64 | # This will pick the 3rd column and use the first line as header 65 | docker rmi 66 | 67 | $ image_id: docker images --- --column 3 --header-lines 1 --delimiter '\s\s+' 68 | ``` 69 | 70 | - We modify the output values of a command 71 | 72 | ```shell 73 | # Even though "false/true" is displayed, this will print "0/1" 74 | echo 75 | 76 | $ mapped: echo 'false true' | tr ' ' '\n' --- --map "grep -q t && echo 1 || echo 0" 77 | ``` 78 | 79 | 80 | The supported parameters are: 81 | 82 | | Parameter | Description | 83 | |:------------------------|:------------------------------------------------------------------------------------------| 84 | | `--column ` | `` is the column number to extract from the result. | 85 | | `--map ` | **_[EXPERIMENTAL]_** `` is a map function to apply to the variable value. | 86 | | `--prevent-extra` | **_[EXPERIMENTAL]_** This parameter will limit the user to select one of the suggestions. | 87 | | `--fzf-overrides ` | **_[EXPERIMENTAL]_** `` is an arbitrary argument to override `fzf` behaviour. | 88 | | `--expand` | **_[EXPERIMENTAL]_** This parameter will convert each line into a separate argument. | 89 | 90 | 91 | In addition, it's possible to forward the following parameters to `fzf`: 92 | 93 | | Parameter forwarded to `fzf` | 94 | |:-----------------------------| 95 | | `--multi` | 96 | | `--header-lines ` | 97 | | `--delimiter ` | 98 | | `--query ` | 99 | | `--filter ` | 100 | | `--header ` | 101 | | `--preview ` | 102 | | `--preview-window ` | 103 | 104 | ### Variable dependency 105 | 106 | Pre-Defined variables can refer other pre-defined variables in two different ways, an implicit and explicit way. 107 | 108 | #### Implicit dependencies 109 | 110 | An implicit dependency is when you refer another variable with the same syntax used in 111 | executable commands (i.e. ``). 112 | 113 | Below is an example of using implicit dependencies to construct a path: 114 | 115 | ```sh 116 | # Should print /my/pictures/wallpapers 117 | echo "" 118 | 119 | $ pictures_folder: echo "/my/pictures" 120 | $ wallpaper_folder: echo "/wallpapers" 121 | ``` 122 | 123 | #### Explicit dependencies 124 | 125 | An explicit dependency is when you prepend a dollar sign (i.e. `$`) to the variable name. 126 | 127 | Below is an example of using explicit dependencies to give multiple choices: 128 | 129 | ```sh 130 | # If you select "hello" for , the possible values of will be "hello foo" and "hello bar" 131 | echo 132 | 133 | # If you want to ignore the contents of and only print 134 | : ; echo 135 | 136 | $ x: echo "hello hi" | tr ' ' '\n' 137 | $ y: echo "$x foo;$x bar" | tr ';' '\n' 138 | ``` 139 | 140 | ### Variable as multiple arguments 141 | 142 | Variables can have multiple arguments, 143 | below is an example of using multiple arguments to cat multiple files at the same time. 144 | 145 | ```sh 146 | # This will result into: cat "file1.json" "file2.json" 147 | cat 148 | 149 | $ jsons: find . -iname '*.json' -type f -print --- --multi --expand 150 | ``` 151 | 152 | ## Extending cheats 153 | 154 | Navi allows you to extend a cheat context with `Extended cheats` lines (i.e. starting with `@`).\ 155 | If you put the same tags from another cheat, you will be able to share the same context and will 156 | be able to use the same variables, for example. 157 | 158 | ```sh 159 | % dirs, common 160 | 161 | $ pictures_folder: echo "/my/pictures" 162 | 163 | % wallpapers 164 | @ dirs, common 165 | 166 | # Should print /my/pictures/wallpapers 167 | echo "/wallpapers" 168 | 169 | % screenshots 170 | @ dirs, common 171 | 172 | # Should print /my/pictures/screenshots 173 | echo "/screenshots" 174 | ``` 175 | 176 | ## Multiline commands/snippets 177 | 178 | Commands can be multiline, we call them snippets. 179 | 180 | - You can write them as follows: 181 | 182 | ```sh 183 | % bash, foo 184 | 185 | # This will output "foo\nyes" 186 | echo foo 187 | true \ 188 | && echo yes \ 189 | || echo no 190 | ``` 191 | 192 | - Or, you can place them inside Markdown code blocks, delimited by triple backticks (```` ``` ````): 193 | 194 | ````sh 195 | % git, code 196 | 197 | # Change branch 198 | ```sh 199 | git checkout 200 | ``` 201 | 202 | $ branch: git branch | awk '{print $NF}' 203 | ```` 204 | 205 | 206 | ## Aliases 207 | 208 | **navi** doesn't have support for aliases as first-class citizens at the moment.\ 209 | However, it is easy to create aliases using **navi** + a few conventions. 210 | 211 | > [!CAUTION] 212 | > The examples below will only work if you use **navi** as a shell scripting tool. 213 | > 214 | > See [/docs/usage/shell-scripting](/docs/usage/shell-scripting/README.md) for more details. 215 | 216 | For example, suppose you decide to end some of your commands with `:: `: 217 | 218 | ```bash 219 | % aliases 220 | 221 | # This is one command :: el 222 | echo lorem ipsum 223 | 224 | # This is another command :: ef 225 | echo foo bar 226 | ``` 227 | 228 | You could add something similar to this in your `.bashrc`-like file: 229 | 230 | ```bash 231 | navialias() { 232 | navi --query ":: $1" --best-match 233 | } 234 | 235 | alias el="navialias el" 236 | alias ef="navialias ef" 237 | ``` 238 | 239 | If you don't want to use these conventions, you can even add full comments in your aliases: 240 | 241 | ```bash 242 | navibestmatch() { 243 | navi --query "$1" --best-match 244 | } 245 | 246 | alias el="navibestmatch 'This is one command'" 247 | alias ef="navibestmatch 'This is another command'" 248 | ``` 249 | -------------------------------------------------------------------------------- /docs/configuration/README.md: -------------------------------------------------------------------------------- 1 | # Configuring Navi 2 | 3 | Navi allows you to configure it with a YAML configuration. 4 | 5 | 6 | * [Configuring Navi](#configuring-navi) 7 | * [Paths and Environment Variables](#paths-and-environment-variables) 8 | * [The default configuration file path](#the-default-configuration-file-path) 9 | * [Cheatsheets paths](#cheatsheets-paths) 10 | * [The default cheatsheets path](#the-default-cheatsheets-path) 11 | * [Defining the cheatsheets path with the environment variable](#defining-the-cheatsheets-path-with-the-environment-variable) 12 | * [Defining the cheatsheets path in the configuration file](#defining-the-cheatsheets-path-in-the-configuration-file) 13 | * [[DEPRECATED] - Using the `path` directive](#deprecated---using-the-path-directive) 14 | * [Customization](#customization) 15 | * [Changing colors](#changing-colors) 16 | * [fzf color scheme](#fzf-color-scheme) 17 | * [Navi colors](#navi-colors) 18 | * [Resizing columns](#resizing-columns) 19 | * [Overriding fzf options](#overriding-fzf-options) 20 | * [Overriding during cheats selection](#overriding-during-cheats-selection) 21 | * [Overriding during values selection](#overriding-during-values-selection) 22 | * [Overriding for all cases](#overriding-for-all-cases) 23 | * [Defining your own delimiter](#defining-your-own-delimiter) 24 | 25 | 26 | ## Paths and Environment Variables 27 | 28 | On the technical side, navi uses the `directories-next` crate for rust, 29 | which defines platform-specific locations to store the configuration files, 30 | the cache and other types of files an application might need. 31 | 32 | > [!TIP] 33 | > For example, this is why cheatsheets are being stored in `~/Library/Application Support/navi` on macOS. 34 | 35 | > [!NOTE] 36 | > Interested on how `directories-next` works?\ 37 | > Go see their `crates.io` page: [crates.io/crates/directories-next](https://crates.io/crates/directories-next) 38 | 39 | 40 | ### The default configuration file path 41 | 42 | During the compilation of navi, the default configuration file path is set by the `$NAVI_CONFIG` environment variable.\ 43 | If it is not set, it fallbacks to `~/.config/navi/config.yaml`. 44 | 45 | You can check your default configuration file path with the info subcommand, 46 | see [/docs/usage/commands/info/](/docs/usage/commands/info/README.md#default-configuration-path) for more details. 47 | 48 | ### Cheatsheets paths 49 | 50 | Navi checks the paths in the following order until it finds a value: 51 | 52 | 1. the `$NAVI_PATH` environment variable 53 | 2. the configuration file 54 | 3. The default value of navi 55 | 56 | #### The default cheatsheets path 57 | 58 | By default, navi stores the cheatsheets in the `~/.local/share/navi/cheats/` directory. 59 | 60 | You can check your default cheatsheets path with the info subcommand, 61 | see [/docs/usage/commands/info/](/docs/usage/commands/info/README.md#default-cheatsheets-path) for more details. 62 | 63 | #### Defining the cheatsheets path with the environment variable 64 | 65 | The cheatsheets path can be defined using the `$NAVI_PATH` environment variable in a colon-separated list, for example: 66 | 67 | ```sh 68 | export NAVI_PATH='/path/to/a/dir:/path/to/another/dir:/yet/another/dir' 69 | ``` 70 | 71 | #### Defining the cheatsheets path in the configuration file 72 | 73 | You can define the cheatsheets path in the configuration file with the following syntax: 74 | 75 | ```yaml 76 | cheats: 77 | paths: 78 | - /path/to/some/dir # on unix-like os 79 | - F:\\path\\to\\dir # on Windows 80 | ``` 81 | 82 | ##### [DEPRECATED] - Using the `path` directive 83 | 84 | Until `2.17.0`, you could define your cheatsheets path with the `path` directive with the following syntax: 85 | 86 | ```yaml 87 | cheats: 88 | path: /path/to/some/dir 89 | ``` 90 | 91 | The directive is now deprecated and will be removed in `2.27.0`. 92 | 93 | ## Customization 94 | 95 | ### Changing colors 96 | 97 | #### fzf color scheme 98 | 99 | You can change the color scheme of `fzf` by overriding fzf options. 100 | 101 | > [!NOTE] 102 | > See [@junegunn/fzf/wiki/Color-schemes](https://github.com/junegunn/fzf/wiki/Color-schemes) and 103 | > [#overriding-fzf-options](#overriding-fzf-options) for more details. 104 | 105 | #### Navi colors 106 | 107 | You can change the text color for each column of navi in the configuration file with the following syntax: 108 | 109 | ```yaml 110 | style: 111 | tag: 112 | color: 113 | comment: 114 | color: 115 | snippet: 116 | color: 117 | ``` 118 | 119 | Below is an example of what to do if you'd like navi to look like the French flag: 120 | 121 | - `config.yaml`: 122 | 123 | ```yaml 124 | style: 125 | tag: 126 | color: blue 127 | comment: 128 | color: white 129 | snippet: 130 | color: red 131 | ``` 132 | 133 | - The result: 134 | 135 | ![navi-custom-colors](https://github.com/user-attachments/assets/d80352c5-d888-43e6-927d-805a8de1a7e2) 136 | 137 | ### Resizing columns 138 | 139 | You can change the column width of each column of navi in the configuration file with the following syntax: 140 | 141 | ```yaml 142 | style: 143 | tag: 144 | width_percentage: 145 | min_width: 146 | comment: 147 | width_percentage: 148 | min_width: 149 | snippet: 150 | width_percentage: 151 | min_width: 152 | ``` 153 | 154 | ### Overriding fzf options 155 | 156 | You can override fzf options for two different cases: 157 | 158 | - During the cheats selection 159 | 160 | Navi exposes the `overrides` directive in the configuration file 161 | and the `NAVI_FZF_OVERRIDES` environment variable. 162 | 163 | - During the pre-defined variable values selection 164 | 165 | Navi exposes the `overrides_var` directive in the configuration file 166 | and the `NAVI_FZF_OVERRIDES_VAR` environment variable. 167 | 168 | For all cases, navi exposes the `FZF_DEFAULT_OPTS` environment variable. 169 | 170 | #### Overriding during cheats selection 171 | 172 | If you want to do the override with `--height 3`, 173 | you can do it with the following syntax in the configuration file: 174 | 175 | ```yaml 176 | finder: 177 | command: fzf 178 | overrides: --height 3 179 | ``` 180 | 181 | But you can also define the environment variable like this: 182 | 183 | ```bash 184 | export NAVI_FZF_OVERRIDES='--height 3' 185 | ``` 186 | 187 | #### Overriding during values selection 188 | 189 | If you want to do the override with `--height 3`, 190 | you can do it with the following syntax in the configuration file: 191 | 192 | ```yaml 193 | finder: 194 | command: fzf 195 | overrides_var: --height 3 196 | ``` 197 | 198 | But you can also define the environment variable like this: 199 | 200 | ```bash 201 | export NAVI_FZF_OVERRIDES_VAR='--height 3' 202 | ``` 203 | 204 | #### Overriding for all cases 205 | 206 | You can define the environment variable like this: 207 | 208 | ```bash 209 | export FZF_DEFAULT_OPTS="--height 3" 210 | ``` 211 | 212 | > [!NOTE] 213 | > See [@junegunn/fzf](https://github.com/junegunn/fzf#layout) for more details on `$FZF_DEFAULT_OPTS`. 214 | 215 | ## Defining your own delimiter 216 | 217 | Navi allows you to define your own delimiter to parse the selected result for a variable in your cheats.\ 218 | It is equivalent to defining `--delimiter` used with `--column`. 219 | 220 | You can define it as such: 221 | 222 | ```yaml 223 | finder: 224 | delimiter_var: ### By default the expression is \s\s+ 225 | ``` 226 | 227 | > [!CAUTION] 228 | > Defining the delimiter via the configuration file means that Navi will use this delimiter by default for 229 | > every variable using the `--column` instruction. 230 | 231 | You can override this configuration with the `--delimiter` instruction in the variable definition of your cheat.\ 232 | See [/docs/cheatsheet/syntax/](/docs/cheatsheet/syntax/README.md#advanced-variable-options) for more details. 233 | 234 | -------------------------------------------------------------------------------- /docs/contributions/README.md: -------------------------------------------------------------------------------- 1 | # Navi contributors 2 | 3 | This section is about the ways you can contribute to Navi and its ecosystem. 4 | 5 | 6 | * [Navi contributors](#navi-contributors) 7 | * [How to contribute to Navi](#how-to-contribute-to-navi) 8 | * [Versioning Scheme](#versioning-scheme) 9 | * [Deprecation of features](#deprecation-of-features) 10 | 11 | 12 | ## How to contribute to Navi 13 | 14 | You have multiple ways to contribute to navi, here are the documented ones: 15 | 16 | - [Write code for Navi](code/README.md) 17 | - [Write documentation for Navi](documentation/README.md) 18 | - [Open Bug tickets](bugs/README.md) 19 | 20 | Please see each section for more details. 21 | 22 | 23 | ## Versioning Scheme 24 | 25 | | Type | Description | 26 | |-------|--------------------------------------------------------------------------------------------------| 27 | | Major | Anything which introduces a major breaking change. The bash to rust rewrite was such an example. | 28 | | Minor | Almost everything. | 29 | | Fix | A fix, just like its name. It should be micro releases with minimal changes. | 30 | 31 | ## Deprecation of features 32 | 33 | Once you introduce a feature, you need to have a clear view of when we're 34 | going to remove its support within navi. 35 | 36 | In order to offer stability to the users, we prefer having 10 minor versions 37 | between the deprecation notice and the removal of its support. 38 | 39 | ````txt 40 | Version where the feature is being deprecated: 0.10.0 41 | Version where the support is dropped: 0.20.0 42 | ```` 43 | 44 | > [!NOTE] 45 | > This rule is not absolute and each feature deprecation needs to be handled 46 | > carefully given its own circumstances, but try to stick as close as possible 47 | > to this rule. 48 | -------------------------------------------------------------------------------- /docs/contributions/bugs/README.md: -------------------------------------------------------------------------------- 1 | # Contribute in opening bug tickets 2 | 3 | Like any other software, navi has bugs. 4 | 5 | If you encounter an issue with Navi, we encourage you to open a bug ticket.\ 6 | Please see [https://github.com/denisidoro/navi/issues/](https://github.com/denisidoro/navi/issues/) to open an issue. 7 | -------------------------------------------------------------------------------- /docs/contributions/code/README.md: -------------------------------------------------------------------------------- 1 | # Contribute code to Navi 2 | 3 | Navi is written in Rust, the widgets may be written in any language given it can be integrated with Navi. 4 | 5 | If you take the example of the most common widgets for Navi they are written in their shell scripting language 6 | because they intend to integrate Navi with the shell in question (Fish, Zsh, NuShell, PowerShell, etc.). 7 | 8 | We separate Navi into two categories: 9 | 10 | - `Navi Core` which refers to Navi's code in Rust 11 | - `Navi Widgets` which refers to code that intends to integrate Navi with a 3rd-party software 12 | 13 | ## Contribute to Navi Core 14 | 15 | If you want to contribute to Navi Core there are certain steps you need to follow for 16 | your changes to be accepted. 17 | 18 | 1. First, open an issue if no opened issues are related to the change you want to contribute. 19 | 2. [Optional] Wait to have an opinion from the maintainers, developers or contributors from Navi. 20 | 21 | > This step is marked as *Optional* as you can open a Merge Request (MR)/Pull Request (PR) 22 | > without having to open an issue beforehand, although it is recommended to not do so. 23 | 24 | We ask you to wait before working on a PR as the way you see a feature and its implementation 25 | might not be similar on how a maintainer of Navi sees it. 26 | 27 | This will save you and the maintainers time. 28 | 29 | 3. Fork the repository and iterate over your changes. 30 | 4. Update Navi documentation 31 | 32 | If you implement a new feature, you will need to create a new entry in the project's 33 | documentation for users to know what has changed. 34 | 35 | No significant modification in Navi's behaviour should be merged without being documented.\ 36 | For more details I recommend you to see [contributions/documentation/](../documentation/README.md). 37 | 38 | 5. Open a PR on [denisidoro/navi](https://github.com/denisidoro/navi/pulls) and request a review 39 | 6. [Optional] Your PR needs revisions and changes before it can be merged 40 | 41 | It's not rare that your PR will need changes before it can be accepted by the maintainers 42 | and then merged into the main branch. 43 | 44 | 7. Your PR has been merged 45 | 46 | Congratulations! Your PR has been reviewed and merged, you should be proud of it, 47 | and we thank you for your contribution. 48 | 49 | The next release cycle will package all contributions into a new release and users 50 | throughout the world will be able to use your new feature(s). 51 | -------------------------------------------------------------------------------- /docs/contributions/documentation/README.md: -------------------------------------------------------------------------------- 1 | # Contribute documentation to Navi 2 | 3 | If you don't want or can't code in Rust, we welcome all contributions, 4 | even more so if it's related to documentation. 5 | 6 | The documentation of Navi is currently made in Markdown. 7 | 8 | ## Markdown documentation 9 | 10 | The documentation source files are located in the `docs/` folder and are mainly grouped by features. 11 | The current documentation follows a structure where one folder equals one topic. 12 | 13 | Here is a quick representation of the folder structure this documentation currently follows: 14 | 15 | ```txt 16 | . 17 | +-- docs 18 | | +-- examples 19 | | | +-- 20 | | +-- src 21 | | | +-- 22 | | | | +-- 23 | | +-- 24 | | | +-- README.md 25 | ``` 26 | 27 | You can see that we have separated the `src` and `examples` folder from the topic with the intent to make it 28 | easier to find each type of documentation. 29 | 30 | > [!NOTE] 31 | > It is recommended to not go deeper than 3 levels in the documentation. 32 | 33 | -------------------------------------------------------------------------------- /docs/deprecated/Alfred/README.md: -------------------------------------------------------------------------------- 1 | # Alfred 2 | 3 | > [!CAUTION] 4 | > This feature has been deprecated and support has been dropped since 2.16.0. 5 | > 6 | > The latest version with support is [2.15.1](https://github.com/denisidoro/navi/releases/tag/v2.15.1). 7 | 8 | 9 | This is _experimental_. If you face any issues, please report [here](https://github.com/denisidoro/navi/issues/348). 10 | 11 | ![Alfred demo](https://user-images.githubusercontent.com/3226564/80294838-582b1b00-8743-11ea-9eb5-a335d8eed833.gif) 12 | 13 | ### Instructions 14 | 15 | - make sure you have [Alfred Powerpack](https://www.alfredapp.com/powerpack/) 16 | - make sure **navi** [2.15.1](https://github.com/denisidoro/navi/releases/tag/v2.15.1) is installed 17 | - make sure that the `navi` binary is in the `$PATH` determined by `~/.bashrc` 18 | - download and install the [latest .alfredworkflow available](https://github.com/denisidoro/navi/releases/tag/v2.15.1) 19 | -------------------------------------------------------------------------------- /docs/examples/cheatsheet/example.cheat: -------------------------------------------------------------------------------- 1 | % first cheat 2 | 3 | # print something 4 | echo "My name is !" 5 | 6 | $ name: whoami 7 | -------------------------------------------------------------------------------- /docs/examples/cheatsheet/navi.cheat: -------------------------------------------------------------------------------- 1 | % cheatsheets 2 | 3 | # Download default cheatsheets 4 | navi repo add denisidoro/cheats 5 | 6 | # Browse for cheatsheet repos 7 | navi repo browse 8 | 9 | # Edit main local cheatsheets 10 | f="$(navi info cheats-path)/main.cheat" 11 | [ -f "$f" ] || navi info cheats-example > "$f" 12 | ${EDITOR:-nano} "$f" 13 | 14 | 15 | % config 16 | 17 | # Edit config file 18 | f="$(navi info config-path)" 19 | [ -f "$f" ] || navi info config-example > "$f" 20 | ${EDITOR:-nano} "$f" 21 | 22 | 23 | % 3rd-party 24 | 25 | # Search using tldr 26 | navi --tldr "" 27 | 28 | # Search using cheatsh 29 | navi --cheatsh "" 30 | 31 | 32 | % widget 33 | 34 | # Load shell widget 35 | shell="$(basename $SHELL)"; eval "$(navi widget $shell)" 36 | 37 | 38 | % help 39 | 40 | # Read command-line help text 41 | navi --help 42 | 43 | # Read project README.md 44 | navi fn url::open "https://github.com/denisidoro/navi" 45 | -------------------------------------------------------------------------------- /docs/examples/configuration/config-example.yaml: -------------------------------------------------------------------------------- 1 | # THIS IS EXPERIMENTAL 2 | # the config file schema may change at any time 3 | 4 | style: 5 | tag: 6 | color: cyan # text color. possible values: https://bit.ly/3gloNNI 7 | width_percentage: 26 # column width relative to the terminal window 8 | min_width: 20 # minimum column width as number of characters 9 | comment: 10 | color: blue 11 | width_percentage: 42 12 | min_width: 45 13 | snippet: 14 | color: white 15 | 16 | finder: 17 | command: fzf # equivalent to the --finder option 18 | # overrides: --tac # equivalent to the --fzf-overrides option 19 | # overrides_var: --tac # equivalent to the --fzf-overrides-var option 20 | # delimiter_var: \s\s+ # equivalent to the --delimiter option that is used with --column option when you extract a column from the selected result for a variable 21 | 22 | # cheats: 23 | # paths: 24 | # - /path/to/some/dir # on unix-like os 25 | # - F:\\path\\to\\dir # on Windows 26 | 27 | # search: 28 | # tags: git,!checkout # equivalent to the --tag-rules option 29 | 30 | # client: 31 | # tealdeer: true # enables tealdeer support for navi --tldr 32 | 33 | shell: 34 | # Shell used for shell out. Possible values: bash, zsh, dash, ... 35 | # For Windows, use `cmd.exe` instead. 36 | command: bash 37 | 38 | # finder_command: bash # similar, but for fzf's internals 39 | -------------------------------------------------------------------------------- /docs/installation/README.md: -------------------------------------------------------------------------------- 1 | # Installation of navi 2 | 3 | This is a reference of all known methods to install navi. 4 | 5 | > [!CAUTION] 6 | > Navi, as of now, has only two official builds, the released binaries on GitHub 7 | > and the published package on brew. 8 | > 9 | > All the other packages are community-maintained. 10 | 11 | ## Using package managers 12 | 13 | ### Homebrew 14 | 15 | ```sh 16 | brew install navi 17 | ``` 18 | 19 | > [!NOTE] 20 | > See [brew.sh](https://brew.sh/) for more details. 21 | 22 | ### Using Gentoo 23 | 24 | > [!WARNING] 25 | > You need to enable the GURU overlay for the instructions below to work correctly. 26 | > 27 | > For more details see: 28 | > 29 | > - [wiki.gentoo.org/wiki/Ebuild_repository](https://wiki.gentoo.org/wiki/Ebuild_repository) 30 | > - [gpo.zugaina.org/Overlays/guru/app-misc/navi](https://gpo.zugaina.org/Overlays/guru/app-misc/navi). 31 | 32 | ```sh 33 | emerge -a app-misc/navi 34 | ``` 35 | 36 | > [!NOTE] 37 | > See [Gentoo.org](https://gentoo.org/) for more details. 38 | 39 | ### Using Pacman 40 | 41 | ```sh 42 | pacman -S navi 43 | ``` 44 | 45 | > [!NOTE] 46 | > See [wiki.archlinux.org/title/Pacman](https://wiki.archlinux.org/title/Pacman) for more details. 47 | 48 | ### Using nix 49 | 50 | ```sh 51 | nix-env -iA nixpkgs.navi 52 | ``` 53 | 54 | > [!NOTE] 55 | > See [nixos.org](https://nixos.org/) for more details 56 | 57 | ### Using Cargo 58 | 59 | ```bash 60 | cargo install --locked navi 61 | ``` 62 | 63 | > [!NOTE] 64 | > See [@rust-lang/cargo](https://github.com/rust-lang/cargo) for more details. 65 | 66 | ### Using Chocolatey 67 | 68 | ```bash 69 | choco install navi 70 | ``` 71 | 72 | > [!CAUTION] 73 | > You currently need to create the config file `$env:USERPROFILE\AppData\Roaming\navi\config.yaml` 74 | > and define the `shell.command` directive as `powershell` for navi to work correctly. 75 | > 76 | > ```yaml 77 | > shell: 78 | > command: powershell 79 | > ``` 80 | 81 | > [!NOTE] 82 | > See [community.chocolatey.org](https://community.chocolatey.org) for more details. 83 | 84 | ## Using the installation script 85 | 86 | Navi has an installation script ready for you to use, you can call it like this: 87 | 88 | ```bash 89 | bash <(curl -sL https://raw.githubusercontent.com/denisidoro/navi/master/scripts/install) 90 | ``` 91 | 92 | If you need to define the directory for the binary, you can call it like this: 93 | 94 | ```bash 95 | BIN_DIR=/usr/local/bin bash <(curl -sL https://raw.githubusercontent.com/denisidoro/navi/master/scripts/install) 96 | ``` 97 | 98 | ## Downloading pre-compiled binaries 99 | 100 | With each release, we try our best to build and publish a binary for each 101 | supported platform, you can find them here: 102 | [@denisidoro/navi/releases/latest](https://github.com/denisidoro/navi/releases/latest) 103 | 104 | What you need to do is: 105 | 106 | - to download the binary corresponding to the version you want to install 107 | - to extract the content of the archive to your `$PATH` 108 | 109 | ## Building from source 110 | 111 | You can also build navi from source, it's mainly used by contributors to 112 | test their modifications but can be used by end users who want to build their own version. 113 | 114 | - You need to clone the repository: 115 | 116 | ```bash 117 | git clone https://github.com/denisidoro/navi && cd navi 118 | ``` 119 | 120 | - Call `make` 121 | 122 | ```bash 123 | make install 124 | ``` 125 | 126 | You can specify the binary directory with: 127 | 128 | ```bash 129 | make BIN_DIR=/usr/local/bin install 130 | ``` 131 | 132 | ## Compile time environment variables 133 | 134 | **navi** supports environment variables at compile time that will modify the behavior of navi at runtime, they are: 135 | 136 | | Environment variable | Description | 137 | |----------------------|-------------------------------------------------------------| 138 | | `NAVI_PATH` | This defines the default path used by navi for cheatsheets. | 139 | | `NAVI_CONFIG` | This defines the default configuration file used by navi. | 140 | 141 | ## Other package managers 142 | 143 | You can find **navi** for more package managers by clicking on the image below: 144 | 145 | [![Packaging status](https://repology.org/badge/vertical-allrepos/navi.svg)](https://repology.org/project/navi/versions) 146 | 147 | Feel free to be the maintainer of **navi** for any package manager you'd like! 148 | -------------------------------------------------------------------------------- /docs/usage/README.md: -------------------------------------------------------------------------------- 1 | # The usage of Navi 2 | 3 | Navi can be used in multiple ways 4 | 5 | #### Defining the cheatsheets path at runtime 6 | 7 | You can define the paths to use for cheatsheets at runtime using the `--path` parameter and a colon-separated paths list 8 | 9 | For example, if we want to search for cheatsheets in `/some/dir` and in `/other/dir`: 10 | 11 | ```sh 12 | navi --path '/some/dir:/other/dir' 13 | ``` 14 | 15 | ## Logging 16 | 17 | The log file will be created under the same directory where the configuration file is located.\ 18 | You can use the `RUST_LOG` environment variable to set the log level. 19 | 20 | For example, to have the logging in debug mode when running navi: 21 | 22 | ```bash 23 | RUST_LOG=debug navi 24 | ``` 25 | 26 | > [!NOTE] 27 | > If the directory of the configuration file doesn't exist, no log file 28 | > is going to be created. 29 | -------------------------------------------------------------------------------- /docs/usage/commands/info/README.md: -------------------------------------------------------------------------------- 1 | # The info subcommands of navi 2 | 3 | Navi exposes information about its default values or examples for you to use. 4 | 5 | 6 | * [The info subcommands of navi](#the-info-subcommands-of-navi) 7 | * [Commands Reference](#commands-reference) 8 | * [Default configuration information](#default-configuration-information) 9 | * [Default configuration path](#default-configuration-path) 10 | * [Example configuration file](#example-configuration-file) 11 | * [Default cheatsheets path](#default-cheatsheets-path) 12 | 13 | 14 | ## Commands Reference 15 | 16 | | Command | Description | 17 | |---------------------|----------------------------------------------------| 18 | | config-path | [DEPRECATED] Lets you see the default config path | 19 | | cheats-path | [DEPRECATED] Lets you see the default cheats path | 20 | | default-config-path | Lets you see the default config path | 21 | | default-cheats-path | Lets you see the default cheats path | 22 | | config-example | Lets you see an example for the configuration file | 23 | | cheats-example | Lets you see an example for a cheat file | 24 | 25 | ## Default configuration information 26 | 27 | ### Default configuration path 28 | 29 | Navi exposes its default configuration path with: 30 | 31 | ```sh 32 | navi info config-path 33 | ``` 34 | 35 | > [!NOTE] 36 | > See [/docs/configuration/](/docs/configuration/README.md#the-default-configuration-file-path) for more details on how the default configuration path is defined. 37 | 38 | ### Example configuration file 39 | 40 | Navi lets you get an example configuration file with: 41 | 42 | ```sh 43 | navi info config-example 44 | ``` 45 | 46 | > [!NOTE] 47 | > You can retrieve this file at the following address: [/docs/examples/configuration/config-example.yaml](/docs/examples/configuration/config-example.yaml) 48 | 49 | For example, you can use this command to create the default configuration file, 50 | if not already present: 51 | 52 | ```sh 53 | navi info config-example > "$(navi info config-path)" 54 | ``` 55 | 56 | ## Default cheatsheets path 57 | 58 | Navi exposes its default cheatsheets path with: 59 | 60 | ```sh 61 | navi info cheats-path 62 | ``` 63 | 64 | > [!NOTE] 65 | > See [/docs/configuration/](/docs/configuration/README.md#the-default-cheatsheets-path) for more details on how the cheatsheets path is defined. 66 | 67 | -------------------------------------------------------------------------------- /docs/usage/commands/repo/README.md: -------------------------------------------------------------------------------- 1 | # The repo subcommands of navi 2 | 3 | 4 | * [The repo subcommands of navi](#the-repo-subcommands-of-navi) 5 | * [Commands Reference](#commands-reference) 6 | * [Browsing through cheatsheet repositories](#browsing-through-cheatsheet-repositories) 7 | * [Importing cheatsheet repositories](#importing-cheatsheet-repositories) 8 | 9 | 10 | ## Commands Reference 11 | 12 | | Command | Description | 13 | |---------|-------------------------------------------------------------------| 14 | | add | Lets you import a cheatsheet repository | 15 | | browser | Lets you browse through a curated list of cheatsheet repositories | 16 | 17 | ## Browsing through cheatsheet repositories 18 | 19 | Navi lets you browse featured [GitHub](https://github.com) repositories registered in [@denisidoro/cheats/featured_repos.txt](https://github.com/denisidoro/cheats/blob/master/featured_repos.txt). 20 | 21 | You can find them within navi with the following command: 22 | 23 | ```sh 24 | navi repo browse 25 | ``` 26 | 27 | ## Importing cheatsheet repositories 28 | 29 | You can import `cheatsheet repositories` using a working git-clone format.\ 30 | This includes using an HTTPS URL or an SSH URI. 31 | 32 | - Import using HTTPS 33 | 34 | ```sh 35 | navi repo add https://github.com/denisidoro/cheats 36 | ``` 37 | 38 | - Import using SSH 39 | 40 | ```shell 41 | navi repo add git@github.com:denisidoro/cheats 42 | ``` 43 | 44 | > [!CAUTION] 45 | > Despite `$NAVI_PATH` being set, it will not be used when installing cheat sheets directly via navi's own commands.\ 46 | > For example when running `navi add repo `, the default paths will still be used. 47 | > 48 | > To avoid this, you may simply clone repos via a regular `git clone` command, directly into `$NAVI_PATH`. 49 | -------------------------------------------------------------------------------- /docs/usage/fzf-overrides/README.md: -------------------------------------------------------------------------------- 1 | # The FZF Overrides of Navi 2 | 3 | Navi allows you to override certain parts of FZF in multiple ways. 4 | 5 | 6 | * [The FZF Overrides of Navi](#the-fzf-overrides-of-navi) 7 | * [Command line arguments](#command-line-arguments) 8 | * [Environment variables](#environment-variables) 9 | 10 | 11 | ## Command line arguments 12 | 13 | Navi allows you to use command line arguments in order to override fzf values: 14 | 15 | ```sh 16 | # if you want to override only when selecting snippets 17 | navi --fzf-overrides '--height 3' 18 | 19 | # if you want to override only when selecting argument values 20 | navi --fzf-overrides-var '--height 3' 21 | ``` 22 | 23 | ## Environment variables 24 | 25 | Navi allows you to use environment variables in order to override fzf values. 26 | 27 | ```bash 28 | # if you want to override for all cases 29 | FZF_DEFAULT_OPTS="--height 3" navi 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/usage/shell-scripting/README.md: -------------------------------------------------------------------------------- 1 | # Navi and shell scripting 2 | 3 | You can use Navi with shell scripting. 4 | 5 | 6 | * [Navi and shell scripting](#navi-and-shell-scripting) 7 | * [Simply calling a cheat](#simply-calling-a-cheat) 8 | * [Defining variables while calling](#defining-variables-while-calling) 9 | * [Filtering results for a variable](#filtering-results-for-a-variable) 10 | * [Selecting the best match for a variable](#selecting-the-best-match-for-a-variable) 11 | 12 | 13 | > [NOTE!] 14 | > The following blog post gives you an example of a real world scenario: [denisidoro.github.io/posts/cli-templates/](https://denisidoro.github.io/posts/cli-templates/) 15 | 16 | 17 | ## Simply calling a cheat 18 | 19 | Below is an example on how to call a cheat from within navi: 20 | 21 | ```sh 22 | navi --query "change branch" --best-match 23 | ``` 24 | 25 | > [!NOTE] 26 | > Navi will ask the user to fill all arguments/variables needed. 27 | 28 | ## Defining variables while calling 29 | 30 | If you want to set the `` beforehand in your script, you can do as follows: 31 | 32 | ```sh 33 | branch="master" navi --query "change branch" --best-match 34 | ``` 35 | 36 | Navi will not show any interactive input and `` will be exactly the one defined while calling. 37 | 38 | ## Filtering results for a variable 39 | 40 | If you want to filter some results for ``, you can do as follows: 41 | 42 | ```sh 43 | branch__query="master" navi --query "change branch" --best-match 44 | ``` 45 | 46 | Navi will show any interactive input, unless a single entry is automatically selected and 47 | the value for `` will be the one selected by the user. 48 | 49 | ## Selecting the best match for a variable 50 | 51 | If you want to select the best match for ``, you can do as follows: 52 | 53 | ```sh 54 | branch__best="master" navi --query "change branch" --best-match 55 | ``` 56 | 57 | Navi will not show any interactive input, and the value for `` will be the one that 58 | best matches the value passed as argument. 59 | -------------------------------------------------------------------------------- /docs/widgets/README.md: -------------------------------------------------------------------------------- 1 | # Navi widgets 2 | 3 | You want to launch Navi with a shortcut?\ 4 | Widgets are here for you! 5 | 6 | Widgets are 3rd-party contributions and integrates Navi with 3rd-party software such as shells. 7 | 8 | ## List of shell widgets 9 | 10 | | Shell | Navi support | 11 | |------------|--------------------| 12 | | Bash | :white_check_mark: | 13 | | Fish | | 14 | | Zsh | | 15 | | NuShell | :white_check_mark: | 16 | | PowerShell | :white_check_mark: | 17 | 18 | ## PowerShell Widget 19 | 20 | - Removal 21 | 22 | ```powershell 23 | Remove-Module navi.plugin 24 | ``` 25 | 26 | ## Other widgets 27 | 28 | - Tmux 29 | - Vim 30 | 31 | 32 | ### Installing the shell widget 33 | 34 | If you want to install it, add this line to your `.bashrc`-like file: 35 | 36 | ```sh 37 | # bash 38 | eval "$(navi widget bash)" 39 | 40 | # zsh 41 | eval "$(navi widget zsh)" 42 | 43 | # fish 44 | navi widget fish | source 45 | 46 | # elvish 47 | eval (navi widget elvish | slurp) 48 | 49 | # xonsh 50 | # xpip install xontrib-navi # ← run in your xonsh session to install xontrib 51 | xontrib load navi # ← add to your xonsh run control file 52 | ``` 53 | 54 | #### Nushell 55 | 56 | Due to Nushell's [unique design](https://www.nushell.sh/book/thinking_in_nu.html#think-of-nushell-as-a-compiled-language), it is not possible to `eval` a piece of code dynamically like in other shells therefore the integration process is a bit more involved. Here is an example: 57 | 1. run `^navi widget nushell | save ($nu.default-config-dir | path join "navi-integration.nu")` 58 | 2. add the following lines to `config.nu`: 59 | ```nushell 60 | source ($nu.default-config-dir | path join "navi-integration.nu") 61 | ``` 62 | 63 | 64 | By default, `Ctrl+G` is assigned to launching **navi** (in xonsh can be customized with `$X_NAVI_KEY`, see [xontrib-navi](https://github.com/eugenesvk/xontrib-navi) for details). 65 | 66 | There's currently no way to customize the widget behavior out-of-the-box. If you want to change the keybinding or the **navi** flags used by the widget, please: 67 | 68 | 1. run, e.g., `navi widget bash` in your terminal 69 | 2. copy the output 70 | 3. paste the output in your `.bashrc`-like file 71 | 4. edit the contents accordingly 72 | -------------------------------------------------------------------------------- /docs/widgets/howto/TMUX.md: -------------------------------------------------------------------------------- 1 | # Tmux widget 2 | 3 | You can use **navi** as a [Tmux](https://github.com/tmux/tmux/wiki) widget to reach your Vim commands, 4 | often used SQL queries, etc. in any command-line app even in SSH sessions. 5 | 6 | 7 | * [Tmux widget](#tmux-widget) 8 | * [Keybinding navi](#keybinding-navi) 9 | * [Example cheatsheet](#example-cheatsheet) 10 | 11 | 12 | ## Keybinding navi 13 | 14 | To be able to open navi via prefix + C-g , you need to add the following lines 15 | to your Tmux configuration file. 16 | 17 | ```sh 18 | bind-key -N "Open Navi (cheat sheets)" -T prefix C-g split-window \ 19 | "$SHELL --login -i -c 'navi --print | head -n 1 | tmux load-buffer -b tmp - ; tmux paste-buffer -p -t {last} -b tmp -d'" 20 | ``` 21 | 22 | ## Example cheatsheet 23 | 24 | Here is an example cheatsheet to use inside Tmux: 25 | 26 | ```sh 27 | % vim 28 | 29 | # Quit without save 30 | qa! 31 | 32 | # Delete a paragraph 33 | normal dap 34 | 35 | # Generate sequence of numbers 36 | put =range(, ) 37 | 38 | % postgresql 39 | 40 | # Describe table columns in `psql` or `pgcli` 41 | select 42 | table_name, 43 | column_name, 44 | data_type 45 | from 46 | information_schema.columns 47 | where 48 | table_name = ''; 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/widgets/howto/VIM.md: -------------------------------------------------------------------------------- 1 | # Vim widget 2 | 3 | 4 | * [Vim widget](#vim-widget) 5 | * [Syntax Highlighting](#syntax-highlighting) 6 | 7 | 8 | ## Syntax Highlighting 9 | 10 | If you want syntax highlighting support for Navi in Vim, you need to 11 | add those syntax rules to your syntax files such as at `$VIMRUNTIME/syntax/navi.vim`. 12 | 13 | The rules are defined based on the [Cheatsheet syntax](/docs/cheatsheet/syntax/README.md). 14 | 15 | Here is an example: 16 | 17 | ```vim 18 | syntax match Comment "\v^;.*$" 19 | syntax match Statement "\v^\%.*$" 20 | syntax match Operator "\v^\#.*$" 21 | syntax match String "\v\<.{-}\>" 22 | syntax match String "\v^\$.*$" 23 | ``` 24 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.81.0" 3 | components = [ "rustfmt", "clippy" ] 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 110 2 | -------------------------------------------------------------------------------- /scripts/docker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)" 5 | 6 | _start() { 7 | cd "$NAVI_HOME" 8 | 9 | ./scripts/release x86_64-unknown-linux-musl 10 | 11 | docker run \ 12 | -e HOMEBREW_NO_AUTO_UPDATE=1 \ 13 | -e HOMEBREW_NO_INSTALL_CLEANUP=1 \ 14 | -v "$(pwd):/navi" \ 15 | -it 'bashell/alpine-bash' \ 16 | bash -c '/navi/scripts docker setup; exec bash' 17 | } 18 | 19 | _setup() { 20 | apk add git 21 | apk add curl 22 | git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf 23 | ln -s /navi/target/debug/navi /usr/local/bin/navi 24 | } 25 | 26 | main() { 27 | local -r fn="$1" 28 | shift || true 29 | "_${fn}" "$@" 30 | } 31 | 32 | main "$@" -------------------------------------------------------------------------------- /scripts/dot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)" 5 | export PROJ_HOME="$NAVI_HOME" 6 | export PROJ_NAME="navi" 7 | export CARGO_PATH="${NAVI_HOME}/core/Cargo.toml" 8 | 9 | # TODO: bump dotfiles + remove this fn 10 | log::note() { log::info "$@"; } 11 | 12 | cargo() { 13 | if [ "${1:-}" = "install" ] && [ "${2:-}" = "cross" ]; then 14 | shift 2 || true 15 | command cargo install cross --git https://github.com/cross-rs/cross "$@" 16 | else 17 | command cargo "$@" 18 | fi 19 | } 20 | 21 | export -f log::note cargo 22 | 23 | dot::clone() { 24 | git clone 'https://github.com/denisidoro/dotfiles' "$DOTFILES" 25 | cd "$DOTFILES" 26 | git checkout 'v2022.07.16' 27 | } 28 | 29 | dot::clone_if_necessary() { 30 | [ -n "${DOTFILES:-}" ] && [ -x "${DOTFILES}/bin/dot" ] && return 31 | export DOTFILES="${NAVI_HOME}/target/dotfiles" 32 | dot::clone 33 | } 34 | 35 | dot::clone_if_necessary 36 | 37 | "${DOTFILES}/bin/dot" "$@" 38 | -------------------------------------------------------------------------------- /scripts/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | if ${X_MODE:-false}; then 5 | set -x 6 | fi 7 | 8 | # ===================== 9 | # paths 10 | # ===================== 11 | 12 | export CARGO_DEFAULT_BIN="${HOME}/.cargo/bin" 13 | export BIN_DIR="${BIN_DIR:-"$CARGO_DEFAULT_BIN"}" 14 | 15 | 16 | # ===================== 17 | # logging 18 | # ===================== 19 | 20 | echoerr() { 21 | echo "$@" 1>&2 22 | } 23 | 24 | tap() { 25 | local -r x="$(cat)" 26 | echoerr "$x" 27 | echo "$x" 28 | } 29 | 30 | log::ansi() { 31 | local bg=false 32 | case "$@" in 33 | *reset*) echo "\e[0m"; return 0 ;; 34 | *black*) color=30 ;; 35 | *red*) color=31 ;; 36 | *green*) color=32 ;; 37 | *yellow*) color=33 ;; 38 | *blue*) color=34 ;; 39 | *purple*) color=35 ;; 40 | *cyan*) color=36 ;; 41 | *white*) color=37 ;; 42 | esac 43 | case "$@" in 44 | *regular*) mod=0 ;; 45 | *bold*) mod=1 ;; 46 | *underline*) mod=4 ;; 47 | esac 48 | case "$@" in 49 | *background*) bg=true ;; 50 | *bg*) bg=true ;; 51 | esac 52 | 53 | if $bg; then 54 | echo "\e[${color}m" 55 | else 56 | echo "\e[${mod:-0};${color}m" 57 | fi 58 | } 59 | 60 | _log() { 61 | local template="$1" 62 | shift 63 | echoerr "$(printf "$template" "$@")" 64 | } 65 | 66 | _header() { 67 | local TOTAL_CHARS=60 68 | local total=$TOTAL_CHARS-2 69 | local size=${#1} 70 | local left=$((($total - $size) / 2)) 71 | local right=$(($total - $size - $left)) 72 | printf "%${left}s" '' | tr ' ' = 73 | printf " $1 " 74 | printf "%${right}s" '' | tr ' ' = 75 | } 76 | 77 | log::header() { _log "\n$(log::ansi bold)$(log::ansi purple)$(_header "$1")$(log::ansi reset)\n"; } 78 | log::success() { _log "$(log::ansi green)✔ %s$(log::ansi reset)\n" "$@"; } 79 | log::error() { _log "$(log::ansi red)✖ %s$(log::ansi reset)\n" "$@"; } 80 | log::warning() { _log "$(log::ansi yellow)➜ %s$(log::ansi reset)\n" "$@"; } 81 | log::note() { _log "$(log::ansi blue)%s$(log::ansi reset)\n" "$@"; } 82 | 83 | # TODO: remove 84 | header() { 85 | echoerr "$*" 86 | echoerr 87 | } 88 | 89 | die() { 90 | log::error "$@" 91 | exit 42 92 | } 93 | 94 | no_binary_warning() { 95 | log::note "There's no precompiled binary for your platform: $(uname -a)" 96 | } 97 | 98 | installation_finish_instructions() { 99 | local -r shell="$(get_shell)" 100 | log::note -e "Finished. To call navi, restart your shell or reload the config file:\n source ~/.${shell}rc" 101 | local code 102 | if [[ "$shell" == "zsh" ]]; then 103 | code="navi widget ${shell} | source" 104 | else 105 | code='source <(navi widget '"$shell"')' 106 | fi 107 | log::note -e "\nTo add the Ctrl-G keybinding, add the following to ~/.${shell}rc:\n ${code}" 108 | } 109 | 110 | 111 | # ===================== 112 | # security 113 | # ===================== 114 | 115 | sha256() { 116 | if command_exists sha256sum; then 117 | sha256sum 118 | elif command_exists shasum; then 119 | shasum -a 256 120 | elif command_exists openssl; then 121 | openssl dgst -sha256 122 | else 123 | log::note "Unable to calculate sha256!" 124 | exit 43 125 | fi 126 | } 127 | 128 | 129 | # ===================== 130 | # github 131 | # ===================== 132 | 133 | latest_version_released() { 134 | curl -s 'https://api.github.com/repos/denisidoro/navi/releases/latest' \ 135 | | grep -Eo '"html_url": "https://github.com/denisidoro/navi/releases/tag/v([0-9\.]+)' \ 136 | | sed 's|"html_url": "https://github.com/denisidoro/navi/releases/tag/v||' 137 | } 138 | 139 | asset_url() { 140 | local -r version="$1" 141 | local -r variant="${2:-}" 142 | 143 | if [[ -n "$variant" ]]; then 144 | echo "https://github.com/denisidoro/navi/releases/download/v${version}/navi-v${version}-${variant}.tar.gz" 145 | else 146 | echo "https://github.com/denisidoro/navi/archive/v${version}.tar.gz" 147 | fi 148 | } 149 | 150 | download_asset() { 151 | local -r tmp_dir="$(mktemp -d -t navi-install-XXXX)" 152 | local -r url="$(asset_url "$@")" 153 | log::note "Downloading ${url}..." 154 | cd "$tmp_dir" 155 | curl -L "$url" -o navi.tar.gz 156 | tar xvzf navi.tar.gz 157 | mkdir -p "${BIN_DIR}" &>/dev/null || true 158 | mv "./navi" "${BIN_DIR}/navi" 159 | } 160 | 161 | sha_for_asset_on_github() { 162 | local -r url="$(asset_url "$@")" 163 | curl -sL "$url" | sha256 | awk '{print $1}' 164 | } 165 | 166 | error_installing() { 167 | log::error "Unable to install navi. Please check https://github.com/denisidoro/navi for alternative installation instructions" 168 | exit 33 169 | } 170 | 171 | 172 | # ===================== 173 | # code 174 | # ===================== 175 | 176 | version_from_toml() { 177 | cat "${NAVI_HOME}/Cargo.toml" \ 178 | | grep version \ 179 | | head -n1 \ 180 | | awk '{print $NF}' \ 181 | | tr -d '"' \ 182 | | tr -d "'" 183 | } 184 | 185 | 186 | # ===================== 187 | # platform 188 | # ===================== 189 | 190 | command_exists() { 191 | type "$1" &>/dev/null 192 | } 193 | 194 | get_target() { 195 | local -r unamea="$(uname -a)" 196 | local -r archi="$(uname -sm)" 197 | 198 | local target 199 | case "$unamea $archi" in 200 | *arwin*) target="x86_64-apple-darwin" ;; 201 | *inux*x86*) target="x86_64-unknown-linux-musl" ;; 202 | *ndroid*aarch*|*ndroid*arm*) target="aarch64-linux-android" ;; 203 | *inux*aarch*|*inux*arm*) target="armv7-unknown-linux-musleabihf" ;; 204 | *) target="" ;; 205 | esac 206 | 207 | echo "$target" 208 | } 209 | 210 | get_shell() { 211 | echo $SHELL | xargs basename 212 | } 213 | 214 | 215 | # ===================== 216 | # main 217 | # ===================== 218 | 219 | export_path_cmd() { 220 | echo 221 | echo ' export PATH="${PATH}:'"$1"'"' 222 | } 223 | 224 | append_to_file() { 225 | local -r path="$1" 226 | local -r text="$2" 227 | if [ -f "$path" ]; then 228 | echo "$text" >> "$path" 229 | fi 230 | } 231 | 232 | get_navi_bin_path() { 233 | local file="${BIN_DIR}/navi" 234 | if [ -f "$file" ]; then 235 | echo "$file" 236 | return 0 237 | fi 238 | file="${CARGO_DEFAULT_BIN}/navi" 239 | if [ -f "$file" ]; then 240 | echo "$file" 241 | return 0 242 | fi 243 | } 244 | 245 | install_navi() { 246 | local -r target="$(get_target)" 247 | 248 | if command_exists navi; then 249 | log::success "navi is already installed" 250 | exit 0 251 | 252 | elif command_exists brew; then 253 | brew install navi 254 | 255 | elif [[ -n "$target" ]]; then 256 | local -r version="$(latest_version_released)" 257 | download_asset "$version" "$target" || error_installing 258 | 259 | elif command_exists cargo; then 260 | cargo install navi 261 | 262 | else 263 | error_installing 264 | 265 | fi 266 | 267 | hash -r 2>/dev/null || true 268 | 269 | local navi_bin_path="$(which navi || get_navi_bin_path)" 270 | ln -s "$navi_bin_path" "${BIN_DIR}/navi" &>/dev/null || true 271 | if [ -f "${BIN_DIR}/navi" ]; then 272 | navi_bin_path="${BIN_DIR}/navi" 273 | fi 274 | 275 | local -r navi_bin_dir="$(dirname "$navi_bin_path")" 276 | 277 | echoerr 278 | log::success "Finished" 279 | log::success "navi is now available at ${navi_bin_path}" 280 | echoerr 281 | 282 | if echo "$PATH" | grep -q "$navi_bin_dir"; then 283 | : 284 | else 285 | local -r cmd="$(export_path_cmd "$navi_bin_dir")" 286 | append_to_file "${HOME}/.bashrc" "$cmd" 287 | append_to_file "${ZDOTDIR:-"$HOME"}/.zshrc" "$cmd" 288 | append_to_file "${HOME}/.fishrc" "$cmd" 289 | fi 290 | 291 | log::note "To call navi, restart your shell or reload your .bashrc-like config file" 292 | echo 293 | log::note "Check https://github.com/denisidoro/navi for more info" 294 | 295 | export PATH="${PATH}:${navi_bin_dir}" 296 | 297 | return 0 298 | } 299 | 300 | (return 0 2>/dev/null) || install_navi "$@" 301 | -------------------------------------------------------------------------------- /scripts/make: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | ##? make install 5 | ##? make uninstall 6 | 7 | export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)" 8 | source "${NAVI_HOME}/scripts/install" 9 | 10 | install() { 11 | cargo install --path . 12 | } 13 | 14 | uninstall() { 15 | cargo uninstall 16 | } 17 | 18 | fix() { 19 | "${NAVI_HOME}/scripts/fix" 20 | } 21 | 22 | cmd="$1" 23 | shift 24 | 25 | export X_MODE=true 26 | set -x 27 | 28 | case "$cmd" in 29 | "install") install "$@" ;; 30 | "uninstall") uninstall "$@" ;; 31 | "fix") fix "$@" ;; 32 | esac 33 | -------------------------------------------------------------------------------- /scripts/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | ### -------------------------------------------------------------------------------------------------------------------- 5 | ### Logging functions 6 | ### -------------------------------------------------------------------------------------------------------------------- 7 | 8 | log::info() { 9 | ### Will print `[INFO]` in black foreground colour and magenta background colour 10 | ### then will print the given text in a magenta foreground colour and default background colour. 11 | printf "\033[35m\033[7m[INFO]\033[27;39m \033[35m$*\033[39m\n" 12 | } 13 | 14 | log::error() { 15 | ### Will print `[ERROR]` in black foreground colour and red background colour 16 | ### then will print the given text in a red foreground colour and default background colour. 17 | printf "\033[31m\033[7m[ERROR]\033[27;39m \033[31m$*\033[39m\n" 18 | } 19 | 20 | log::warn() { 21 | ### Will print `[WARNING]` in black foreground colour and yellow background colour 22 | ### then will print the given text in a yellow foreground colour and default background colour. 23 | printf "\033[33m\033[7m[WARNING]\033[27;39m \033[33m$*\033[39m\n" 24 | } 25 | 26 | ### -------------------------------------------------------------------------------------------------------------------- 27 | ### Utils functions 28 | ### -------------------------------------------------------------------------------------------------------------------- 29 | 30 | ### Permits us to know if the current target environment 31 | ### is a windows platform or not. 32 | is_windows() { 33 | local -r target="$1" 34 | echo "$target" | grep -q "windows" 35 | } 36 | 37 | ### NOTE: This function is currently not in use but kept as 38 | ### a backup function in case something breaks 39 | ### 40 | ### Returns the target environment, with a fix for the x86_64 target. 41 | get_env_target() { 42 | eval "$(rustc --print cfg | grep target)" 43 | local -rr raw="${target_arch:-}-${target_vendor:-}-${target_os:-}-${target_env:-}" 44 | 45 | if echo "$raw" | grep -q "x86_64-apple-macos"; then 46 | echo "x86_64-apple-darwin" 47 | else 48 | echo "$raw" 49 | fi 50 | } 51 | 52 | ### NOTE: This function is currently not in use but kept as 53 | ### a backup function in case something breaks 54 | ### 55 | ### Logs the given arguments then execute it 56 | _tap() { 57 | log::info "$@" 58 | "$@" 59 | } 60 | 61 | ### NOTE: This function is currently not in use but kept as 62 | ### a backup function in case something breaks 63 | ### 64 | ### Lists the content of a path, given as parameter. 65 | _ls() { 66 | log::info "contents from $*:" 67 | ls -la "$@" || true 68 | } 69 | 70 | ### -------------------------------------------------------------------------------------------------------------------- 71 | ### Release-Related functions 72 | ### -------------------------------------------------------------------------------------------------------------------- 73 | 74 | release() { 75 | local -r env_target="$1" 76 | log::info "env target: $env_target" 77 | 78 | local -r cross_target="${1:-"$env_target"}" 79 | log::info "desired target: $cross_target" 80 | 81 | TAR_DIR="$(pwd)/target/tar" 82 | 83 | ### We clean up the target folder, just in case 84 | rm -rf "$(pwd)/target" 2> /dev/null || true 85 | 86 | ### We add the target for rustup in case cross doesn't find it. 87 | ### Since the default behaviour of cross is to compile from 88 | ### a rustup target if it doesn't find one for itself. 89 | rustup target add $env_target 90 | cargo install cross 91 | 92 | ### We're building the release via cross for the target environment 93 | cross build --release --target "$env_target" 94 | 95 | cd target/"$env_target"/release/ 96 | 97 | if is_windows "$env_target"; then 98 | ### If our target is windows, we can simply zip our executable 99 | ### since having tar is not the norm and neither the default 100 | zip -r "navi.zip" "navi.exe" 101 | 102 | ### We export a CI/CD variable to be used later in the pipeline 103 | echo "EXTENSION=zip" >> $GITHUB_OUTPUT 104 | else 105 | 106 | ### @alexis-opolka - I'm currently disabling the usage of UPX since I cannot find how 107 | ### it was used before the merge of the code from the @denisidoro/dotfiles repository. 108 | ### 109 | #if upx --best --lzma "navi"; then 110 | # log::info "upx succeeded" 111 | #else 112 | # log::info "upx failed" 113 | #fi 114 | 115 | 116 | ### For all other targets, they have tar as the norm 117 | ### or have it installed by default. 118 | tar -czf "navi.tar.gz" "navi" 119 | 120 | ### We export a CI/CD variable to be used later in the pipeline 121 | echo "EXTENSION=tar.gz" >> $GITHUB_OUTPUT 122 | fi 123 | } 124 | 125 | ### -------------------------------------------------------------------------------------------------------------------- 126 | ### Main script 127 | ### -------------------------------------------------------------------------------------------------------------------- 128 | 129 | release "$@" 130 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)" 5 | source "${NAVI_HOME}/scripts/install" 6 | 7 | "${NAVI_HOME}/tests/run" -------------------------------------------------------------------------------- /shell/navi.plugin.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | _navi_call() { 4 | local result="$(navi "$@" /dev/tty 2>&1 } 30 | -------------------------------------------------------------------------------- /shell/navi.plugin.fish: -------------------------------------------------------------------------------- 1 | function _navi_smart_replace 2 | set --local query (commandline --current-process | string trim) 3 | 4 | if test -n "$query" 5 | set --local best_match (navi --print --query "$query" --best-match) 6 | if test -n "$best_match" 7 | commandline --current-process $best_match 8 | return 9 | end 10 | end 11 | 12 | set --local candidate (navi --print --query "$query") 13 | if test -n "$candidate" 14 | commandline --current-process $candidate 15 | end 16 | end 17 | 18 | bind \cg _navi_smart_replace 19 | bind --mode insert \cg _navi_smart_replace 20 | -------------------------------------------------------------------------------- /shell/navi.plugin.nu: -------------------------------------------------------------------------------- 1 | export def navi_widget [] { 2 | let current_input = (commandline) 3 | let last_command = ($current_input | navi fn widget::last_command | str trim) 4 | 5 | match ($last_command | is-empty) { 6 | true => {^navi --print | complete | get "stdout"} 7 | false => { 8 | let find = $"($last_command)_NAVIEND" 9 | let replacement = (^navi --print --query $'($last_command)' | complete | get "stdout") 10 | 11 | match ($replacement | str trim | is-empty) { 12 | false => {$"($current_input)_NAVIEND" | str replace $find $replacement} 13 | true => $current_input 14 | } 15 | } 16 | } 17 | | str trim 18 | | commandline edit --replace $in 19 | 20 | commandline set-cursor --end 21 | } 22 | 23 | let nav_keybinding = { 24 | name: "navi", 25 | modifier: control, 26 | keycode: char_g, 27 | mode: [emacs, vi_normal, vi_insert], 28 | event: { 29 | send: executehostcommand, 30 | cmd: navi_widget, 31 | } 32 | } 33 | 34 | $env.config.keybindings = ($env.config.keybindings | append $nav_keybinding) 35 | -------------------------------------------------------------------------------- /shell/navi.plugin.ps1: -------------------------------------------------------------------------------- 1 | 2 | 3 | $null = New-Module { 4 | 5 | function Invoke-Navi { 6 | $startArgs = @{ 7 | FileName = "navi"; 8 | Arguments = $args; 9 | RedirectStandardOutput = $true; 10 | WorkingDirectory = $PWD; 11 | UseShellExecute = $false; 12 | } 13 | $p = [System.Diagnostics.Process]@{StartInfo = $startArgs} 14 | 15 | [void]$p.Start() 16 | $result = $p.StandardOutput.ReadToEnd() 17 | $p.WaitForExit() 18 | 19 | $result 20 | } 21 | 22 | 23 | ### Initial code from @lurebat (https://github.com/lurebat/) 24 | ### See #570 (https://github.com/denisidoro/navi/issues/570) for its original contribution 25 | function Invoke-NaviWidget { 26 | $ast = $tokens = $errors = $cursor = $null 27 | [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref] $ast, [ref] $tokens, [ref] $errors, [ref] $cursor) 28 | 29 | $line = $ast.ToString().Trim() 30 | $output = $null 31 | 32 | if ([String]::IsNullOrEmpty($line)) { 33 | $output = (Invoke-Navi "--print" | Out-String).Trim() 34 | } 35 | else { 36 | $best_match = (Invoke-Navi "--print --best-match --query `"$line`"" | Out-String).Trim() 37 | if ([String]::IsNullOrEmpty($best_match)) { 38 | $output = (Invoke-Navi "--print --query `"$line`"" | Out-String).Trim() 39 | } 40 | else { 41 | $output = $best_match 42 | } 43 | } 44 | 45 | [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine() 46 | [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt() 47 | 48 | ### Handling the case when the user escapes without selecting any entry 49 | if (-Not([String]::IsNullOrEmpty($output))) { 50 | [Microsoft.PowerShell.PSConsoleReadLine]::Insert([String]$output) 51 | } 52 | } 53 | 54 | Set-PSReadlineKeyHandler -BriefDescription "A keybinding to open Navi Widget" -Chord Ctrl+g -ScriptBlock { Invoke-NaviWidget } 55 | Export-ModuleMember -Function @() 56 | } 57 | -------------------------------------------------------------------------------- /shell/navi.plugin.zsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | _navi_call() { 4 | local result="$(navi "$@" (source: SourceError) -> Self 18 | where 19 | SourceError: Into, 20 | { 21 | FileAnIssue { 22 | source: source.into(), 23 | } 24 | } 25 | } 26 | 27 | fn main() -> anyhow::Result<()> { 28 | if let Err(err) = init_logger() { 29 | // may need redir stderr to a file to show this log initialization error 30 | eprintln!("failed to initialize logging: {err:?}"); 31 | } 32 | navi::handle().map_err(|e| { 33 | error!("{e:?}"); 34 | FileAnIssue::new(e).into() 35 | }) 36 | } 37 | 38 | fn init_logger() -> anyhow::Result<()> { 39 | const FILE_NAME: &str = "navi.log"; 40 | let mut file = navi::default_config_pathbuf()?; 41 | file.set_file_name(FILE_NAME); 42 | 43 | // If config path doesn't exist, navi won't log. 44 | if file.parent().map(|p| !p.exists()).unwrap_or(true) { 45 | return Ok(()); 46 | } 47 | 48 | let writer = std::fs::File::create(&file).with_context(|| format!("{file:?} is not created"))?; 49 | tracing::subscriber::set_global_default( 50 | tracing_subscriber::fmt() 51 | .with_ansi(false) 52 | .with_writer(writer) 53 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 54 | .finish(), 55 | )?; 56 | debug!("tracing initialized"); 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /src/clients/cheatsh.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use std::process::Command; 3 | 4 | fn map_line(line: &str) -> String { 5 | line.trim().trim_end_matches(':').to_string() 6 | } 7 | 8 | fn as_lines(query: &str, markdown: &str) -> Vec { 9 | format!( 10 | "% {query}, cheat.sh 11 | {markdown}" 12 | ) 13 | .lines() 14 | .map(map_line) 15 | .collect() 16 | } 17 | 18 | pub fn call(query: &str) -> Result> { 19 | let args = ["-qO-", &format!("cheat.sh/{query}")]; 20 | 21 | let child = Command::new("wget") 22 | .args(args) 23 | .stdin(Stdio::piped()) 24 | .stdout(Stdio::piped()) 25 | .spawn(); 26 | 27 | let child = match child { 28 | Ok(x) => x, 29 | Err(_) => { 30 | let msg = "navi was unable to call wget. 31 | Make sure wget is correctly installed."; 32 | return Err(anyhow!(msg)); 33 | } 34 | }; 35 | 36 | let out = child.wait_with_output().context("Failed to wait for wget")?; 37 | 38 | if let Some(0) = out.status.code() { 39 | let stdout = out.stdout; 40 | let plain_bytes = strip_ansi_escapes::strip(stdout); 41 | 42 | let markdown = String::from_utf8(plain_bytes).context("Output is invalid utf8")?; 43 | if markdown.starts_with("Unknown topic.") { 44 | let msg = format!( 45 | "`{}` not found in cheatsh. 46 | Output: 47 | {} 48 | ", 49 | &query, markdown, 50 | ); 51 | return Err(anyhow!(msg)); 52 | } 53 | 54 | let lines = as_lines(query, &markdown); 55 | Ok(lines) 56 | } else { 57 | let msg = format!( 58 | "Failed to call: 59 | wget {} 60 | 61 | Output: 62 | {} 63 | 64 | Error: 65 | {} 66 | ", 67 | args.join(" "), 68 | String::from_utf8(out.stdout).unwrap_or_else(|_e| "Unable to get output message".to_string()), 69 | String::from_utf8(out.stderr).unwrap_or_else(|_e| "Unable to get error message".to_string()) 70 | ); 71 | Err(anyhow!(msg)) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/clients/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cheatsh; 2 | pub mod tldr; 3 | -------------------------------------------------------------------------------- /src/clients/tldr.rs: -------------------------------------------------------------------------------- 1 | use crate::config::CONFIG; 2 | use crate::prelude::*; 3 | use std::process::{Command, Stdio}; 4 | 5 | lazy_static! { 6 | pub static ref VAR_TLDR_REGEX: Regex = Regex::new(r"\{\{(.*?)\}\}").expect("Invalid regex"); 7 | pub static ref NON_VAR_CHARS_REGEX: Regex = Regex::new(r"[^\da-zA-Z_]").expect("Invalid regex"); 8 | } 9 | 10 | static VERSION_DISCLAIMER: &str = 11 | "tldr-c-client (the default one in Homebrew) doesn't support markdown files, so navi can't use it. 12 | The recommended client is tealdeer(https://github.com/dbrgn/tealdeer)."; 13 | 14 | fn convert_tldr_vars(line: &str) -> String { 15 | let caps = VAR_TLDR_REGEX.find_iter(line); 16 | let mut new_line: String = line.to_string(); 17 | for cap in caps { 18 | let braced_var = cap.as_str(); 19 | let var = &braced_var[2..braced_var.len() - 2]; 20 | let mut new_var = NON_VAR_CHARS_REGEX.replace_all(var, "_").to_string(); 21 | if let Some(c) = new_var.chars().next() { 22 | if c.to_string().parse::().is_ok() { 23 | new_var = format!("example_{new_var}"); 24 | } 25 | } 26 | let bracketed_var = format!("<{new_var}>"); 27 | new_line = new_line.replace(braced_var, &bracketed_var); 28 | } 29 | new_line 30 | } 31 | 32 | fn convert_tldr(line: &str) -> String { 33 | let line = line.trim(); 34 | if line.starts_with('-') { 35 | format!("{}{}", "# ", &line[2..line.len() - 1]) 36 | } else if line.starts_with('`') { 37 | convert_tldr_vars(&line[1..line.len() - 1]) 38 | } else if line.starts_with('%') { 39 | line.to_string() 40 | } else { 41 | "".to_string() 42 | } 43 | } 44 | 45 | fn markdown_lines(query: &str, markdown: &str) -> Vec { 46 | format!( 47 | "% {query}, tldr 48 | {markdown}" 49 | ) 50 | .lines() 51 | .map(convert_tldr) 52 | .collect() 53 | } 54 | 55 | pub fn call(query: &str) -> Result> { 56 | let tealdeer = CONFIG.tealdeer(); 57 | let output_flag = if tealdeer { "--raw" } else { "--markdown" }; 58 | let args = [query, output_flag]; 59 | 60 | let child = Command::new("tldr") 61 | .args(args) 62 | .stdin(Stdio::piped()) 63 | .stdout(Stdio::piped()) 64 | .stderr(Stdio::piped()) 65 | .spawn(); 66 | 67 | let child = match child { 68 | Ok(x) => x, 69 | Err(_) => { 70 | let msg = format!( 71 | "navi was unable to call tldr. 72 | Make sure tldr is correctly installed. 73 | 74 | Note: 75 | {VERSION_DISCLAIMER} 76 | " 77 | ); 78 | return Err(anyhow!(msg)); 79 | } 80 | }; 81 | 82 | let out = child.wait_with_output().context("Failed to wait for tldr")?; 83 | 84 | if let Some(0) = out.status.code() { 85 | let stdout = out.stdout; 86 | 87 | let markdown = String::from_utf8(stdout).context("Output is invalid utf8")?; 88 | let lines = markdown_lines(query, &markdown); 89 | Ok(lines) 90 | } else { 91 | let msg = format!( 92 | "Failed to call: 93 | tldr {} 94 | 95 | Output: 96 | {} 97 | 98 | Error: 99 | {} 100 | 101 | Note: 102 | The client.tealdeer config option can be set to enable tealdeer support. 103 | If you want to use another client, please make sure it supports the --markdown flag. 104 | If you are already using a supported version you can ignore this message. 105 | {} 106 | ", 107 | args.join(" "), 108 | String::from_utf8(out.stdout).unwrap_or_else(|_e| "Unable to get output message".to_string()), 109 | String::from_utf8(out.stderr).unwrap_or_else(|_e| "Unable to get error message".to_string()), 110 | VERSION_DISCLAIMER, 111 | ); 112 | Err(anyhow!(msg)) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/commands/core/actor.rs: -------------------------------------------------------------------------------- 1 | use crate::common::clipboard; 2 | use crate::common::fs; 3 | use crate::common::shell; 4 | use crate::common::shell::ShellSpawnError; 5 | use crate::config::Action; 6 | use crate::deser; 7 | use crate::env_var; 8 | use crate::finder::structures::{Opts as FinderOpts, SuggestionType}; 9 | use crate::prelude::*; 10 | use crate::structures::cheat::{Suggestion, VariableMap}; 11 | use crate::structures::item::Item; 12 | use shell::EOF; 13 | use std::process::Stdio; 14 | 15 | fn prompt_finder( 16 | variable_name: &str, 17 | suggestion: Option<&Suggestion>, 18 | variable_count: usize, 19 | ) -> Result { 20 | env_var::remove(env_var::PREVIEW_COLUMN); 21 | env_var::remove(env_var::PREVIEW_DELIMITER); 22 | env_var::remove(env_var::PREVIEW_MAP); 23 | 24 | let mut extra_preview: Option = None; 25 | 26 | let (suggestions, initial_opts) = if let Some(s) = suggestion { 27 | let (suggestion_command, suggestion_opts) = s; 28 | 29 | if let Some(sopts) = suggestion_opts { 30 | if let Some(c) = &sopts.column { 31 | env_var::set(env_var::PREVIEW_COLUMN, c.to_string()); 32 | } 33 | if let Some(d) = &sopts.delimiter { 34 | env_var::set(env_var::PREVIEW_DELIMITER, d); 35 | } 36 | if let Some(m) = &sopts.map { 37 | env_var::set(env_var::PREVIEW_MAP, m); 38 | } 39 | if let Some(p) = &sopts.preview { 40 | extra_preview = Some(p.into()); 41 | } 42 | } 43 | 44 | let mut cmd = shell::out(); 45 | cmd.stdout(Stdio::piped()).arg(suggestion_command); 46 | debug!(cmd = ?cmd); 47 | let child = cmd 48 | .spawn() 49 | .map_err(|e| ShellSpawnError::new(suggestion_command, e))?; 50 | 51 | let text = String::from_utf8( 52 | child 53 | .wait_with_output() 54 | .context("Failed to wait and collect output from bash")? 55 | .stdout, 56 | ) 57 | .context("Suggestions are invalid utf8")?; 58 | 59 | (text, suggestion_opts) 60 | } else { 61 | ('\n'.to_string(), &None) 62 | }; 63 | 64 | let exe = fs::exe_string(); 65 | 66 | let preview = if CONFIG.shell().contains("powershell") { 67 | format!( 68 | r#"{exe} preview-var {{+}} "{{q}}" "{name}"; {extra}"#, 69 | exe = exe, 70 | name = variable_name, 71 | extra = extra_preview 72 | .clone() 73 | .map(|e| format!(" echo; {e}")) 74 | .unwrap_or_default(), 75 | ) 76 | } else if CONFIG.shell().contains("cmd.exe") { 77 | format!( 78 | r#"(@echo.{{+}}{eof}{{q}}{eof}{name}{eof}{extra}) | {exe} preview-var-stdin"#, 79 | exe = exe, 80 | name = variable_name, 81 | extra = extra_preview.clone().unwrap_or_default(), 82 | eof = EOF, 83 | ) 84 | } else if CONFIG.shell().contains("fish") { 85 | format!( 86 | r#"{exe} preview-var "{{+}}" "{{q}}" "{name}"; {extra}"#, 87 | exe = exe, 88 | name = variable_name, 89 | extra = extra_preview 90 | .clone() 91 | .map(|e| format!(" echo; {e}")) 92 | .unwrap_or_default(), 93 | ) 94 | } else { 95 | format!( 96 | r#"{exe} preview-var "$(cat <<{eof} 97 | {{+}} 98 | {eof} 99 | )" "$(cat <<{eof} 100 | {{q}} 101 | {eof} 102 | )" "{name}"; {extra}"#, 103 | exe = exe, 104 | name = variable_name, 105 | extra = extra_preview 106 | .clone() 107 | .map(|e| format!(" echo; {e}")) 108 | .unwrap_or_default(), 109 | eof = EOF, 110 | ) 111 | }; 112 | 113 | let mut opts = FinderOpts { 114 | preview: Some(preview), 115 | show_all_columns: true, 116 | ..initial_opts.clone().unwrap_or_else(FinderOpts::var_default) 117 | }; 118 | 119 | opts.query = env_var::get(format!("{variable_name}__query")).ok(); 120 | 121 | if let Ok(f) = env_var::get(format!("{variable_name}__best")) { 122 | opts.filter = Some(f); 123 | opts.suggestion_type = SuggestionType::SingleSelection; 124 | } 125 | 126 | if opts.preview_window.is_none() { 127 | opts.preview_window = Some(if extra_preview.is_none() { 128 | format!("up:{}", variable_count + 3) 129 | } else { 130 | "right:50%".to_string() 131 | }); 132 | } 133 | 134 | if suggestion.is_none() { 135 | opts.suggestion_type = SuggestionType::Disabled; 136 | }; 137 | 138 | let (output, _) = CONFIG 139 | .finder() 140 | .call(opts, |stdin| { 141 | stdin 142 | .write_all(suggestions.as_bytes()) 143 | .context("Could not write to finder's stdin")?; 144 | Ok(()) 145 | }) 146 | .context("finder was unable to prompt with suggestions")?; 147 | 148 | Ok(output) 149 | } 150 | 151 | fn unique_result_count(results: &[&str]) -> usize { 152 | let mut vars = results.to_owned(); 153 | vars.sort_unstable(); 154 | vars.dedup(); 155 | vars.len() 156 | } 157 | 158 | fn replace_variables_from_snippet(snippet: &str, tags: &str, variables: VariableMap) -> Result { 159 | let mut interpolated_snippet = String::from(snippet); 160 | 161 | if CONFIG.prevent_interpolation() { 162 | return Ok(interpolated_snippet); 163 | } 164 | 165 | let variables_found: Vec<&str> = deser::VAR_REGEX.find_iter(snippet).map(|m| m.as_str()).collect(); 166 | let variable_count = unique_result_count(&variables_found); 167 | 168 | for bracketed_variable_name in variables_found { 169 | let variable_name = &bracketed_variable_name[1..bracketed_variable_name.len() - 1]; 170 | 171 | let env_variable_name = env_var::escape(variable_name); 172 | let env_value = env_var::get(&env_variable_name); 173 | 174 | let value = if let Ok(e) = env_value { 175 | e 176 | } else if let Some(suggestion) = variables.get_suggestion(tags, variable_name) { 177 | let mut new_suggestion = suggestion.clone(); 178 | new_suggestion.0 = replace_variables_from_snippet(&new_suggestion.0, tags, variables.clone())?; 179 | prompt_finder(variable_name, Some(&new_suggestion), variable_count)? 180 | } else { 181 | prompt_finder(variable_name, None, variable_count)? 182 | }; 183 | 184 | env_var::set(env_variable_name, &value); 185 | 186 | interpolated_snippet = if value.as_str() == "\n" { 187 | interpolated_snippet.replacen(bracketed_variable_name, "", 1) 188 | } else { 189 | interpolated_snippet.replacen(bracketed_variable_name, value.as_str(), 1) 190 | }; 191 | } 192 | 193 | Ok(interpolated_snippet) 194 | } 195 | 196 | pub fn with_absolute_path(snippet: String) -> String { 197 | if let Some(s) = snippet.strip_prefix("navi ") { 198 | return format!("{} {}", fs::exe_string(), s); 199 | } 200 | snippet 201 | } 202 | 203 | pub fn act( 204 | extractions: Result<(&str, Item)>, 205 | files: Vec, 206 | variables: Option, 207 | ) -> Result<()> { 208 | let ( 209 | key, 210 | Item { 211 | tags, 212 | comment, 213 | snippet, 214 | file_index, 215 | .. 216 | }, 217 | ) = extractions.unwrap(); 218 | 219 | if key == "ctrl-o" { 220 | edit::edit_file(Path::new(&files[file_index.expect("No files found")])) 221 | .expect("Could not open file in external editor"); 222 | return Ok(()); 223 | } 224 | 225 | env_var::set(env_var::PREVIEW_INITIAL_SNIPPET, &snippet); 226 | env_var::set(env_var::PREVIEW_TAGS, &tags); 227 | env_var::set(env_var::PREVIEW_COMMENT, comment); 228 | 229 | let interpolated_snippet = { 230 | let mut s = replace_variables_from_snippet( 231 | &snippet, 232 | &tags, 233 | variables.expect("No variables received from finder"), 234 | ) 235 | .context("Failed to replace variables from snippet")?; 236 | s = with_absolute_path(s); 237 | s = deser::with_new_lines(s); 238 | s 239 | }; 240 | 241 | match CONFIG.action() { 242 | Action::Print => { 243 | println!("{interpolated_snippet}"); 244 | } 245 | Action::Execute => match key { 246 | "ctrl-y" => { 247 | clipboard::copy(interpolated_snippet)?; 248 | } 249 | _ => { 250 | let mut cmd = shell::out(); 251 | cmd.arg(&interpolated_snippet[..]); 252 | debug!(cmd = ?cmd); 253 | cmd.spawn() 254 | .map_err(|e| ShellSpawnError::new(&interpolated_snippet[..], e))? 255 | .wait() 256 | .context("bash was not running")?; 257 | } 258 | }, 259 | }; 260 | 261 | Ok(()) 262 | } 263 | -------------------------------------------------------------------------------- /src/commands/core/mod.rs: -------------------------------------------------------------------------------- 1 | mod actor; 2 | 3 | use crate::clients::{cheatsh, tldr}; 4 | use crate::config::Source; 5 | use crate::deser; 6 | use crate::filesystem; 7 | use crate::finder::structures::Opts as FinderOpts; 8 | use crate::parser::Parser; 9 | use crate::prelude::*; 10 | use crate::structures::fetcher::{Fetcher, StaticFetcher}; 11 | use crate::welcome; 12 | 13 | pub fn init(fetcher: Box) -> Result<()> { 14 | let config = &CONFIG; 15 | let opts = FinderOpts::snippet_default(); 16 | debug!("opts = {opts:#?}"); 17 | // let fetcher = config.fetcher(); 18 | 19 | let (raw_selection, (variables, files)) = config 20 | .finder() 21 | .call(opts, |writer| { 22 | let mut parser = Parser::new(writer, true); 23 | 24 | let found_something = fetcher 25 | .fetch(&mut parser) 26 | .context("Failed to parse variables intended for finder")?; 27 | 28 | if !found_something { 29 | welcome::populate_cheatsheet(&mut parser)?; 30 | } 31 | 32 | Ok((Some(parser.variables), fetcher.files())) 33 | }) 34 | .context("Failed getting selection and variables from finder")?; 35 | 36 | debug!(raw_selection = ?raw_selection); 37 | let extractions = deser::terminal::read(&raw_selection, config.best_match()); 38 | 39 | if extractions.is_err() { 40 | return init(fetcher); 41 | } 42 | 43 | actor::act(extractions, files, variables)?; 44 | 45 | Ok(()) 46 | } 47 | 48 | pub fn get_fetcher() -> Result> { 49 | let source = CONFIG.source(); 50 | debug!(source = ?source); 51 | match source { 52 | Source::Cheats(query) => { 53 | let lines = cheatsh::call(&query)?; 54 | let fetcher = Box::new(StaticFetcher::new(lines)); 55 | Ok(fetcher) 56 | } 57 | Source::Tldr(query) => { 58 | let lines = tldr::call(&query)?; 59 | let fetcher = Box::new(StaticFetcher::new(lines)); 60 | Ok(fetcher) 61 | } 62 | Source::Filesystem(path) => { 63 | let fetcher = Box::new(filesystem::Fetcher::new(path)); 64 | Ok(fetcher) 65 | } 66 | Source::Welcome => { 67 | let fetcher = Box::new(welcome::Fetcher::new()); 68 | Ok(fetcher) 69 | } 70 | } 71 | } 72 | 73 | pub fn main() -> Result<()> { 74 | let fetcher = get_fetcher()?; 75 | init(fetcher) 76 | } 77 | -------------------------------------------------------------------------------- /src/commands/func/map.rs: -------------------------------------------------------------------------------- 1 | use crate::common::shell::{self, ShellSpawnError}; 2 | use crate::prelude::*; 3 | 4 | pub fn expand() -> Result<()> { 5 | let cmd = r#"sed -e 's/^.*$/"&"/' | tr '\n' ' '"#; 6 | shell::out() 7 | .arg(cmd) 8 | .spawn() 9 | .map_err(|e| ShellSpawnError::new(cmd, e))? 10 | .wait()?; 11 | Ok(()) 12 | } 13 | -------------------------------------------------------------------------------- /src/commands/func/mod.rs: -------------------------------------------------------------------------------- 1 | mod map; 2 | mod widget; 3 | 4 | use super::core; 5 | use super::temp; 6 | use crate::common::url; 7 | use crate::prelude::*; 8 | use clap::Args; 9 | use clap::ValueEnum; 10 | 11 | #[derive(Debug, Clone, ValueEnum)] 12 | pub enum Func { 13 | #[value(name = "url::open")] 14 | UrlOpen, 15 | #[value(name = "welcome")] 16 | Welcome, 17 | #[value(name = "widget::last_command")] 18 | WidgetLastCommand, 19 | #[value(name = "map::expand")] 20 | MapExpand, 21 | #[value(name = "temp")] 22 | Temp, 23 | } 24 | 25 | #[derive(Debug, Clone, Args)] 26 | pub struct Input { 27 | /// Function name (example: "url::open") 28 | #[arg(ignore_case = true)] 29 | pub func: Func, 30 | /// List of arguments (example: "https://google.com") 31 | pub args: Vec, 32 | } 33 | 34 | impl Runnable for Input { 35 | fn run(&self) -> Result<()> { 36 | let func = &self.func; 37 | let args = self.args.clone(); // TODO 38 | 39 | match func { 40 | Func::UrlOpen => url::open(args), 41 | Func::Welcome => core::main(), 42 | Func::WidgetLastCommand => widget::last_command(), 43 | Func::MapExpand => map::expand(), 44 | Func::Temp => temp::main(), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/commands/func/widget.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use std::io::{self, Read}; 3 | 4 | pub fn last_command() -> Result<()> { 5 | let mut text = String::new(); 6 | io::stdin().read_to_string(&mut text)?; 7 | 8 | let replacements = vec![("||", "ග"), ("|", "ඛ"), ("&&", "ඝ")]; 9 | 10 | let parts = shellwords::split(&text).unwrap_or_else(|_| text.split('|').map(|s| s.to_string()).collect()); 11 | 12 | for p in parts { 13 | for (pattern, escaped) in replacements.clone() { 14 | if p.contains(pattern) && p != pattern && p != format!("{pattern}{pattern}") { 15 | let replacement = p.replace(pattern, escaped); 16 | text = text.replace(&p, &replacement); 17 | } 18 | } 19 | } 20 | 21 | let mut extracted = text.clone(); 22 | 23 | for (pattern, _) in replacements.clone() { 24 | let mut new_parts = text.rsplit(pattern); 25 | if let Some(extracted_attempt) = new_parts.next() { 26 | if extracted_attempt.len() <= extracted.len() { 27 | extracted = extracted_attempt.to_string(); 28 | } 29 | } 30 | } 31 | 32 | for (pattern, escaped) in replacements.clone() { 33 | text = text.replace(escaped, pattern); 34 | extracted = extracted.replace(escaped, pattern); 35 | } 36 | 37 | println!("{}", extracted.trim_start()); 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/info.rs: -------------------------------------------------------------------------------- 1 | use crate::filesystem; 2 | use crate::prelude::*; 3 | use clap::{Args, Subcommand}; 4 | 5 | #[derive(Debug, Clone, Args)] 6 | pub struct Input { 7 | #[clap(subcommand)] 8 | pub info: Info, 9 | } 10 | 11 | #[derive(Debug, Clone, Subcommand)] 12 | pub enum Info { 13 | /// Prints a cheatsheet example. 14 | CheatsExample, 15 | /// Prints a configuration file example. 16 | ConfigExample, 17 | 18 | /// [DEPRECATED] Prints the default cheatsheets path. 19 | /// Please use `info default-cheats-path` instead. 20 | CheatsPath, 21 | /// [DEPRECATED] Prints the default configuration path. 22 | /// Please use `info default-config-path` instead. 23 | ConfigPath, 24 | 25 | /// Prints the default cheatsheets path. 26 | DefaultCheatsPath, 27 | /// Prints the default configuration path. 28 | DefaultConfigPath, 29 | } 30 | 31 | impl Runnable for Input { 32 | fn run(&self) -> Result<()> { 33 | let info = &self.info; 34 | 35 | match info { 36 | // Here should be the example commands 37 | Info::CheatsExample => { 38 | println!("{}", include_str!("../../docs/examples/cheatsheet/example.cheat")) 39 | } 40 | Info::ConfigExample => println!( 41 | "{}", 42 | include_str!("../../docs/examples/configuration/config-example.yaml") 43 | ), 44 | 45 | // Here should be the old deprecated default value commands 46 | Info::CheatsPath => println!("{}", &filesystem::default_cheat_pathbuf()?.to_string()), 47 | Info::ConfigPath => println!("{}", &filesystem::default_config_pathbuf()?.to_string()), 48 | 49 | // Here should be the default values (computed at compile time) 50 | Info::DefaultCheatsPath => println!("{}", &filesystem::default_cheat_pathbuf()?.to_string()), 51 | Info::DefaultConfigPath => println!("{}", &filesystem::default_config_pathbuf()?.to_string()), 52 | } 53 | Ok(()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod core; 2 | pub mod func; 3 | pub mod info; 4 | pub mod preview; 5 | pub mod repo; 6 | pub mod shell; 7 | pub mod temp; 8 | 9 | use crate::commands; 10 | use crate::prelude::*; 11 | 12 | pub fn handle() -> Result<()> { 13 | use crate::config::Command::*; 14 | 15 | debug!("CONFIG = {:#?}", &*CONFIG); 16 | match CONFIG.cmd() { 17 | None => commands::core::main(), 18 | 19 | Some(c) => match c { 20 | Preview(input) => input.run(), 21 | 22 | PreviewVarStdin(input) => input.run(), 23 | 24 | PreviewVar(input) => input.run(), 25 | 26 | Widget(input) => input.run().context("Failed to print shell widget code"), 27 | 28 | Fn(input) => input 29 | .run() 30 | .with_context(|| format!("Failed to execute function `{:#?}`", input.func)), 31 | 32 | Info(input) => input 33 | .run() 34 | .with_context(|| format!("Failed to fetch info `{:#?}`", input.info)), 35 | 36 | #[cfg(not(feature = "disable-repo-management"))] 37 | Repo(input) => input.run(), 38 | }, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/preview/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::deser; 2 | use crate::prelude::*; 3 | use clap::Args; 4 | use crossterm::style::{style, Stylize}; 5 | use std::process; 6 | 7 | pub mod var; 8 | pub mod var_stdin; 9 | 10 | #[derive(Debug, Clone, Args)] 11 | pub struct Input { 12 | /// Selection line 13 | pub line: String, 14 | } 15 | 16 | fn extract_elements(argstr: &str) -> Result<(&str, &str, &str)> { 17 | let mut parts = argstr.split(deser::terminal::DELIMITER).skip(3); 18 | let tags = parts.next().context("No `tags` element provided.")?; 19 | let comment = parts.next().context("No `comment` element provided.")?; 20 | let snippet = parts.next().context("No `snippet` element provided.")?; 21 | Ok((tags, comment, snippet)) 22 | } 23 | 24 | impl Runnable for Input { 25 | fn run(&self) -> Result<()> { 26 | let line = &self.line; 27 | 28 | let (tags, comment, snippet) = extract_elements(line)?; 29 | 30 | println!( 31 | "{comment} {tags} \n{snippet}", 32 | comment = style(comment).with(CONFIG.comment_color()), 33 | tags = style(format!("[{tags}]")).with(CONFIG.tag_color()), 34 | snippet = style(deser::fix_newlines(snippet)).with(CONFIG.snippet_color()), 35 | ); 36 | 37 | process::exit(0) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/preview/var.rs: -------------------------------------------------------------------------------- 1 | use crate::deser; 2 | use crate::env_var; 3 | use crate::finder; 4 | use crate::prelude::*; 5 | use clap::Args; 6 | use crossterm::style::style; 7 | use crossterm::style::Stylize; 8 | use std::iter; 9 | use std::process; 10 | 11 | #[derive(Debug, Clone, Args)] 12 | pub struct Input { 13 | /// Selection line 14 | pub selection: String, 15 | /// Query match 16 | pub query: String, 17 | /// Typed text 18 | pub variable: String, 19 | } 20 | 21 | impl Runnable for Input { 22 | fn run(&self) -> Result<()> { 23 | let selection = &self.selection; 24 | let query = &self.query; 25 | let variable = &self.variable; 26 | 27 | let snippet = env_var::must_get(env_var::PREVIEW_INITIAL_SNIPPET); 28 | let tags = env_var::must_get(env_var::PREVIEW_TAGS); 29 | let comment = env_var::must_get(env_var::PREVIEW_COMMENT); 30 | let column = env_var::parse(env_var::PREVIEW_COLUMN); 31 | let delimiter = env_var::get(env_var::PREVIEW_DELIMITER).ok(); 32 | let map = env_var::get(env_var::PREVIEW_MAP).ok(); 33 | 34 | let active_color = CONFIG.tag_color(); 35 | let inactive_color = CONFIG.comment_color(); 36 | 37 | let mut colored_snippet = String::from(&snippet); 38 | let mut visited_vars: HashSet<&str> = HashSet::new(); 39 | 40 | let mut variables = String::from(""); 41 | 42 | println!( 43 | "{comment} {tags}", 44 | comment = style(comment).with(CONFIG.comment_color()), 45 | tags = style(format!("[{tags}]")).with(CONFIG.tag_color()), 46 | ); 47 | 48 | let bracketed_current_variable = format!("<{variable}>"); 49 | 50 | let bracketed_variables: Vec<&str> = { 51 | if snippet.contains(&bracketed_current_variable) { 52 | deser::VAR_REGEX.find_iter(&snippet).map(|m| m.as_str()).collect() 53 | } else { 54 | iter::once(&bracketed_current_variable) 55 | .map(|s| s.as_str()) 56 | .collect() 57 | } 58 | }; 59 | 60 | for bracketed_variable_name in bracketed_variables { 61 | let variable_name = &bracketed_variable_name[1..bracketed_variable_name.len() - 1]; 62 | 63 | if visited_vars.contains(variable_name) { 64 | continue; 65 | } else { 66 | visited_vars.insert(variable_name); 67 | } 68 | 69 | let is_current = variable_name == variable; 70 | let variable_color = if is_current { active_color } else { inactive_color }; 71 | let env_variable_name = env_var::escape(variable_name); 72 | 73 | let value = if is_current { 74 | let v = selection.trim_matches('\''); 75 | if v.is_empty() { query.trim_matches('\'') } else { v }.to_string() 76 | } else if let Ok(v) = env_var::get(&env_variable_name) { 77 | v 78 | } else { 79 | "".to_string() 80 | }; 81 | 82 | let replacement = format!( 83 | "{variable}", 84 | variable = style(bracketed_variable_name).with(variable_color), 85 | ); 86 | 87 | colored_snippet = colored_snippet.replace(bracketed_variable_name, &replacement); 88 | 89 | variables = format!( 90 | "{variables}\n{variable} = {value}", 91 | variables = variables, 92 | variable = style(variable_name).with(variable_color), 93 | value = if env_var::get(&env_variable_name).is_ok() { 94 | value 95 | } else if is_current { 96 | finder::process(value, column, delimiter.as_deref(), map.clone()) 97 | .expect("Unable to process value") 98 | } else { 99 | "".to_string() 100 | } 101 | ); 102 | } 103 | 104 | println!("{snippet}", snippet = deser::fix_newlines(&colored_snippet)); 105 | println!("{variables}"); 106 | 107 | process::exit(0) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/commands/preview/var_stdin.rs: -------------------------------------------------------------------------------- 1 | use clap::Args; 2 | 3 | use super::var; 4 | use crate::common::shell::{self, ShellSpawnError, EOF}; 5 | use crate::prelude::*; 6 | use std::io::{self, Read}; 7 | 8 | #[derive(Debug, Clone, Args)] 9 | pub struct Input {} 10 | 11 | impl Runnable for Input { 12 | fn run(&self) -> Result<()> { 13 | let mut text = String::new(); 14 | io::stdin().read_to_string(&mut text)?; 15 | 16 | let mut parts = text.split(EOF); 17 | let selection = parts.next().expect("Unable to get selection").to_owned(); 18 | let query = parts.next().expect("Unable to get query").to_owned(); 19 | let variable = parts.next().expect("Unable to get variable").trim().to_owned(); 20 | 21 | let input = var::Input { 22 | selection, 23 | query, 24 | variable, 25 | }; 26 | 27 | input.run()?; 28 | 29 | if let Some(extra) = parts.next() { 30 | if !extra.is_empty() { 31 | print!(""); 32 | 33 | let mut cmd = shell::out(); 34 | cmd.arg(extra); 35 | debug!(?cmd); 36 | cmd.spawn().map_err(|e| ShellSpawnError::new(extra, e))?.wait()?; 37 | } 38 | } 39 | 40 | Ok(()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/repo/add.rs: -------------------------------------------------------------------------------- 1 | use crate::common::git; 2 | use crate::filesystem; 3 | use crate::finder::structures::{Opts as FinderOpts, SuggestionType}; 4 | use crate::finder::FinderChoice; 5 | use crate::prelude::*; 6 | use std::fs; 7 | use std::path; 8 | 9 | fn ask_if_should_import_all(finder: &FinderChoice) -> Result { 10 | let opts = FinderOpts { 11 | column: Some(1), 12 | header: Some("Do you want to import all files from this repo?".to_string()), 13 | ..Default::default() 14 | }; 15 | 16 | let (response, _) = finder 17 | .call(opts, |stdin| { 18 | stdin 19 | .write_all(b"Yes\nNo") 20 | .context("Unable to writer alternatives")?; 21 | Ok(()) 22 | }) 23 | .context("Unable to get response")?; 24 | 25 | Ok(response.to_lowercase().starts_with('y')) 26 | } 27 | 28 | pub fn main(uri: String) -> Result<()> { 29 | let finder = CONFIG.finder(); 30 | 31 | let should_import_all = ask_if_should_import_all(&finder).unwrap_or(false); 32 | let (actual_uri, user, repo) = git::meta(uri.as_str()); 33 | 34 | let cheat_pathbuf = filesystem::default_cheat_pathbuf()?; 35 | let tmp_pathbuf = filesystem::tmp_pathbuf()?; 36 | let tmp_path_str = &tmp_pathbuf.to_string(); 37 | 38 | let _ = filesystem::remove_dir(&tmp_pathbuf); 39 | filesystem::create_dir(&tmp_pathbuf)?; 40 | 41 | eprintln!("Cloning {} into {}...\n", &actual_uri, &tmp_path_str); 42 | 43 | git::shallow_clone(actual_uri.as_str(), tmp_path_str) 44 | .with_context(|| format!("Failed to clone `{actual_uri}`"))?; 45 | 46 | let all_files = filesystem::all_cheat_files(&tmp_pathbuf).join("\n"); 47 | 48 | let opts = FinderOpts { 49 | suggestion_type: SuggestionType::MultipleSelections, 50 | preview: Some(format!("cat '{tmp_path_str}/{{}}'")), 51 | header: Some("Select the cheatsheets you want to import with then hit \nUse Ctrl-R for (de)selecting all".to_string()), 52 | preview_window: Some("right:30%".to_string()), 53 | ..Default::default() 54 | }; 55 | 56 | let files = if should_import_all { 57 | all_files 58 | } else { 59 | let (files, _) = finder 60 | .call(opts, |stdin| { 61 | stdin 62 | .write_all(all_files.as_bytes()) 63 | .context("Unable to prompt cheats to import")?; 64 | Ok(()) 65 | }) 66 | .context("Failed to get cheatsheet files from finder")?; 67 | files 68 | }; 69 | 70 | let to_folder = { 71 | let mut p = cheat_pathbuf; 72 | p.push(format!("{user}__{repo}")); 73 | p 74 | }; 75 | 76 | for file in files.split('\n') { 77 | let from = { 78 | let mut p = tmp_pathbuf.clone(); 79 | p.push(file); 80 | p 81 | }; 82 | let filename = file 83 | .replace(&format!("{}{}", &tmp_path_str, path::MAIN_SEPARATOR), "") 84 | .replace(path::MAIN_SEPARATOR, "__"); 85 | let to = { 86 | let mut p = to_folder.clone(); 87 | p.push(filename); 88 | p 89 | }; 90 | fs::create_dir_all(&to_folder).unwrap_or(()); 91 | fs::copy(&from, &to) 92 | .with_context(|| format!("Failed to copy `{}` to `{}`", &from.to_string(), &to.to_string()))?; 93 | } 94 | 95 | filesystem::remove_dir(&tmp_pathbuf)?; 96 | 97 | eprintln!( 98 | "The following .cheat files were imported successfully:\n{}\n\nThey are now located at {}", 99 | files, 100 | to_folder.to_string() 101 | ); 102 | 103 | Ok(()) 104 | } 105 | -------------------------------------------------------------------------------- /src/commands/repo/browse.rs: -------------------------------------------------------------------------------- 1 | use crate::filesystem; 2 | use crate::finder::structures::{Opts as FinderOpts, SuggestionType}; 3 | 4 | use crate::common::git; 5 | use crate::prelude::*; 6 | use std::fs; 7 | 8 | pub fn main() -> Result { 9 | let finder = CONFIG.finder(); 10 | 11 | let repo_pathbuf = { 12 | let mut p = filesystem::tmp_pathbuf()?; 13 | p.push("featured"); 14 | p 15 | }; 16 | 17 | let repo_path_str = &repo_pathbuf.to_string(); 18 | 19 | let _ = filesystem::remove_dir(&repo_pathbuf); 20 | filesystem::create_dir(&repo_pathbuf)?; 21 | 22 | let (repo_url, _, _) = git::meta("denisidoro/cheats"); 23 | git::shallow_clone(repo_url.as_str(), repo_path_str) 24 | .with_context(|| format!("Failed to clone `{repo_url}`"))?; 25 | 26 | let feature_repos_file = { 27 | let mut p = repo_pathbuf.clone(); 28 | p.push("featured_repos.txt"); 29 | p 30 | }; 31 | 32 | let repos = fs::read_to_string(feature_repos_file).context("Unable to fetch featured repositories")?; 33 | 34 | let opts = FinderOpts { 35 | column: Some(1), 36 | suggestion_type: SuggestionType::SingleSelection, 37 | ..Default::default() 38 | }; 39 | 40 | let (repo, _) = finder 41 | .call(opts, |stdin| { 42 | stdin 43 | .write_all(repos.as_bytes()) 44 | .context("Unable to prompt featured repositories")?; 45 | Ok(()) 46 | }) 47 | .context("Failed to get repo URL from finder")?; 48 | 49 | filesystem::remove_dir(&repo_pathbuf)?; 50 | 51 | Ok(repo) 52 | } 53 | -------------------------------------------------------------------------------- /src/commands/repo/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::commands; 2 | use crate::prelude::*; 3 | use clap::{Args, Subcommand}; 4 | 5 | pub mod add; 6 | pub mod browse; 7 | 8 | #[derive(Debug, Clone, Subcommand)] 9 | pub enum RepoCommand { 10 | /// Imports cheatsheets from a repo 11 | Add { 12 | /// A URI to a git repository containing .cheat files ("user/repo" will download cheats from github.com/user/repo) 13 | uri: String, 14 | }, 15 | /// Browses for featured cheatsheet repos 16 | Browse, 17 | } 18 | 19 | #[derive(Debug, Clone, Args)] 20 | pub struct Input { 21 | #[clap(subcommand)] 22 | pub cmd: RepoCommand, 23 | } 24 | 25 | impl Runnable for Input { 26 | fn run(&self) -> Result<()> { 27 | match &self.cmd { 28 | RepoCommand::Add { uri } => { 29 | add::main(uri.clone()) 30 | .with_context(|| format!("Failed to import cheatsheets from `{uri}`"))?; 31 | commands::core::main() 32 | } 33 | RepoCommand::Browse => { 34 | let repo = browse::main().context("Failed to browse featured cheatsheets")?; 35 | add::main(repo.clone()) 36 | .with_context(|| format!("Failed to import cheatsheets from `{repo}`"))?; 37 | commands::core::main() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/shell.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::Display; 3 | 4 | use clap::Args; 5 | 6 | use crate::common::shell::Shell; 7 | use crate::prelude::*; 8 | 9 | impl Display for Shell { 10 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 11 | let s = match self { 12 | Self::Bash => "bash", 13 | Self::Zsh => "zsh", 14 | Self::Fish => "fish", 15 | Self::Elvish => "elvish", 16 | Self::Nushell => "nushell", 17 | Self::Powershell => "powershell", 18 | }; 19 | 20 | write!(f, "{s}") 21 | } 22 | } 23 | 24 | #[derive(Debug, Clone, Args)] 25 | pub struct Input { 26 | #[clap(ignore_case = true, default_value_t = Shell::Bash)] 27 | pub shell: Shell, 28 | } 29 | 30 | impl Runnable for Input { 31 | fn run(&self) -> Result<()> { 32 | let shell = &self.shell; 33 | 34 | let content = match shell { 35 | Shell::Bash => include_str!("../../shell/navi.plugin.bash"), 36 | Shell::Zsh => include_str!("../../shell/navi.plugin.zsh"), 37 | Shell::Fish => include_str!("../../shell/navi.plugin.fish"), 38 | Shell::Elvish => include_str!("../../shell/navi.plugin.elv"), 39 | Shell::Nushell => include_str!("../../shell/navi.plugin.nu"), 40 | Shell::Powershell => include_str!("../../shell/navi.plugin.ps1"), 41 | }; 42 | 43 | println!("{content}"); 44 | 45 | Ok(()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/commands/temp.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::core::get_fetcher; 2 | use crate::common::shell::{self, ShellSpawnError}; 3 | use crate::finder::structures::Opts as FinderOpts; 4 | use crate::parser::Parser; 5 | use crate::{deser, prelude::*}; 6 | use std::io::{self, Write}; 7 | 8 | pub fn main() -> Result<()> { 9 | let _config = &CONFIG; 10 | let _opts = FinderOpts::snippet_default(); 11 | 12 | let fetcher = get_fetcher()?; 13 | let hash: u64 = 2087294461664323320; 14 | 15 | let mut buf = vec![]; 16 | let mut parser = Parser::new(&mut buf, false); 17 | parser.set_hash(hash); 18 | 19 | let _res = fetcher 20 | .fetch(&mut parser) 21 | .context("Failed to parse variables intended for finder")?; 22 | 23 | let variables = parser.variables; 24 | let item_str = String::from_utf8(buf)?; 25 | let item = deser::raycast::read(&item_str)?; 26 | dbg!(&item); 27 | 28 | let x = variables.get_suggestion(&item.tags, "local_branch").expect("foo"); 29 | dbg!(&x); 30 | 31 | let suggestion_command = x.0.clone(); 32 | let child = shell::out() 33 | .stdout(Stdio::piped()) 34 | .arg(&suggestion_command) 35 | .spawn() 36 | .map_err(|e| ShellSpawnError::new(suggestion_command, e))?; 37 | 38 | let text = String::from_utf8( 39 | child 40 | .wait_with_output() 41 | .context("Failed to wait and collect output from bash")? 42 | .stdout, 43 | ) 44 | .context("Suggestions are invalid utf8")?; 45 | 46 | dbg!(&text); 47 | 48 | Ok(()) 49 | } 50 | 51 | pub fn _main0() -> Result<()> { 52 | let _config = &CONFIG; 53 | 54 | let fetcher = get_fetcher()?; 55 | 56 | let mut stdout = io::stdout(); 57 | let mut writer: Box<&mut dyn Write> = Box::new(&mut stdout); 58 | let mut parser = Parser::new(&mut writer, false); 59 | 60 | let _res = fetcher 61 | .fetch(&mut parser) 62 | .context("Failed to parse variables intended for finder")?; 63 | 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /src/common/clipboard.rs: -------------------------------------------------------------------------------- 1 | use crate::common::shell::{self, ShellSpawnError, EOF}; 2 | use crate::prelude::*; 3 | 4 | pub fn copy(text: String) -> Result<()> { 5 | let cmd = r#" 6 | exst() { 7 | type "$1" &>/dev/null 8 | } 9 | 10 | _copy() { 11 | if exst pbcopy; then 12 | pbcopy 13 | elif exst xclip; then 14 | xclip -selection clipboard 15 | elif exst clip.exe; then 16 | clip.exe 17 | else 18 | exit 55 19 | fi 20 | }"#; 21 | 22 | shell::out() 23 | .arg( 24 | format!( 25 | r#"{cmd} 26 | read -r -d '' x <<'{EOF}' 27 | {text} 28 | {EOF} 29 | 30 | echo -n "$x" | _copy"#, 31 | ) 32 | .as_str(), 33 | ) 34 | .spawn() 35 | .map_err(|e| ShellSpawnError::new(cmd, e))? 36 | .wait()?; 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /src/common/deps.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | pub trait HasDeps { 4 | fn deps(&self) -> HashSet { 5 | HashSet::new() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/common/fs.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use remove_dir_all::remove_dir_all; 3 | use std::ffi::OsStr; 4 | use std::fs::{self, create_dir_all, File}; 5 | use std::io; 6 | use thiserror::Error; 7 | 8 | pub trait ToStringExt { 9 | fn to_string(&self) -> String; 10 | } 11 | 12 | impl ToStringExt for Path { 13 | fn to_string(&self) -> String { 14 | self.to_string_lossy().to_string() 15 | } 16 | } 17 | 18 | impl ToStringExt for OsStr { 19 | fn to_string(&self) -> String { 20 | self.to_string_lossy().to_string() 21 | } 22 | } 23 | 24 | #[derive(Error, Debug)] 25 | #[error("Invalid path `{0}`")] 26 | pub struct InvalidPath(pub PathBuf); 27 | 28 | #[derive(Error, Debug)] 29 | #[error("Unable to read directory `{dir}`")] 30 | pub struct UnreadableDir { 31 | dir: PathBuf, 32 | #[source] 33 | source: anyhow::Error, 34 | } 35 | 36 | pub fn open(filename: &Path) -> Result { 37 | File::open(filename).with_context(|| { 38 | let x = filename.to_string(); 39 | format!("Failed to open file {}", &x) 40 | }) 41 | } 42 | 43 | pub fn read_lines(filename: &Path) -> Result>> { 44 | let file = open(filename)?; 45 | Ok(io::BufReader::new(file) 46 | .lines() 47 | .map(|line| line.map_err(Error::from))) 48 | } 49 | 50 | pub fn pathbuf_to_string(pathbuf: &Path) -> Result { 51 | Ok(pathbuf 52 | .as_os_str() 53 | .to_str() 54 | .ok_or_else(|| InvalidPath(pathbuf.to_path_buf())) 55 | .map(str::to_string)?) 56 | } 57 | 58 | fn follow_symlink(pathbuf: PathBuf) -> Result { 59 | fs::read_link(pathbuf.clone()) 60 | .map(|o| { 61 | let o_str = o 62 | .as_os_str() 63 | .to_str() 64 | .ok_or_else(|| InvalidPath(o.to_path_buf()))?; 65 | if o_str.starts_with('.') { 66 | let p = pathbuf 67 | .parent() 68 | .ok_or_else(|| anyhow!("`{}` has no parent", pathbuf.display()))?; 69 | let mut p = PathBuf::from(p); 70 | p.push(o_str); 71 | follow_symlink(p) 72 | } else { 73 | follow_symlink(o) 74 | } 75 | }) 76 | .unwrap_or(Ok(pathbuf)) 77 | } 78 | 79 | fn exe_pathbuf() -> Result { 80 | let pathbuf = std::env::current_exe().context("Unable to acquire executable's path")?; 81 | 82 | #[cfg(target_family = "windows")] 83 | let pathbuf = dunce::canonicalize(pathbuf)?; 84 | 85 | debug!(current_exe = ?pathbuf); 86 | follow_symlink(pathbuf) 87 | } 88 | 89 | fn exe_abs_string() -> Result { 90 | pathbuf_to_string(&exe_pathbuf()?) 91 | } 92 | 93 | pub fn exe_string() -> String { 94 | exe_abs_string().unwrap_or_else(|_| "navi".to_string()) 95 | } 96 | 97 | pub fn create_dir(path: &Path) -> Result<()> { 98 | create_dir_all(path).with_context(|| { 99 | format!( 100 | "Failed to create directory `{}`", 101 | pathbuf_to_string(path).expect("Unable to parse {path}") 102 | ) 103 | }) 104 | } 105 | 106 | pub fn remove_dir(path: &Path) -> Result<()> { 107 | remove_dir_all(path).with_context(|| { 108 | format!( 109 | "Failed to remove directory `{}`", 110 | pathbuf_to_string(path).expect("Unable to parse {path}") 111 | ) 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /src/common/git.rs: -------------------------------------------------------------------------------- 1 | use crate::common::shell::ShellSpawnError; 2 | use crate::prelude::*; 3 | use std::process::Command; 4 | 5 | pub fn shallow_clone(uri: &str, target: &str) -> Result<()> { 6 | Command::new("git") 7 | .args(["clone", uri, target, "--depth", "1"]) 8 | .spawn() 9 | .map_err(|e| ShellSpawnError::new("git clone", e))? 10 | .wait() 11 | .context("Unable to git clone")?; 12 | Ok(()) 13 | } 14 | 15 | pub fn meta(uri: &str) -> (String, String, String) { 16 | let actual_uri = if uri.contains("://") || uri.contains('@') { 17 | uri.to_string() 18 | } else { 19 | format!("https://github.com/{uri}") 20 | }; 21 | 22 | let uri_to_split = actual_uri.replace(':', "/"); 23 | let parts: Vec<&str> = uri_to_split.split('/').collect(); 24 | let user = parts[parts.len() - 2]; 25 | let repo = parts[parts.len() - 1].replace(".git", ""); 26 | 27 | (actual_uri, user.to_string(), repo) 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use super::*; 33 | 34 | #[test] 35 | fn test_meta_github_https() { 36 | let (actual_uri, user, repo) = meta("https://github.com/denisidoro/navi"); 37 | assert_eq!(actual_uri, "https://github.com/denisidoro/navi".to_string()); 38 | assert_eq!(user, "denisidoro".to_string()); 39 | assert_eq!(repo, "navi".to_string()); 40 | } 41 | 42 | #[test] 43 | fn test_meta_github_ssh() { 44 | let (actual_uri, user, repo) = meta("git@github.com:denisidoro/navi.git"); 45 | assert_eq!(actual_uri, "git@github.com:denisidoro/navi.git".to_string()); 46 | assert_eq!(user, "denisidoro".to_string()); 47 | assert_eq!(repo, "navi".to_string()); 48 | } 49 | 50 | #[test] 51 | fn test_meta_gitlab_https() { 52 | let (actual_uri, user, repo) = meta("https://gitlab.com/user/repo.git"); 53 | assert_eq!(actual_uri, "https://gitlab.com/user/repo.git".to_string()); 54 | assert_eq!(user, "user".to_string()); 55 | assert_eq!(repo, "repo".to_string()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/common/hash.rs: -------------------------------------------------------------------------------- 1 | use std::hash::{Hash, Hasher}; 2 | 3 | const MAGIC_INIT: u64 = 0x811C_9DC5; 4 | 5 | pub fn fnv(x: &T) -> u64 { 6 | let mut hasher = FnvHasher::new(); 7 | x.hash(&mut hasher); 8 | hasher.finish() 9 | } 10 | 11 | struct FnvHasher(u64); 12 | 13 | impl FnvHasher { 14 | fn new() -> Self { 15 | FnvHasher(MAGIC_INIT) 16 | } 17 | } 18 | 19 | impl Hasher for FnvHasher { 20 | fn finish(&self) -> u64 { 21 | self.0 22 | } 23 | 24 | fn write(&mut self, bytes: &[u8]) { 25 | for byte in bytes.iter() { 26 | self.0 ^= u64::from(*byte); 27 | self.0 = self.0.wrapping_mul(0x0100_0000_01b3); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod clipboard; 2 | pub mod deps; 3 | pub mod fs; 4 | pub mod git; 5 | pub mod hash; 6 | pub mod shell; 7 | pub mod terminal; 8 | pub mod url; 9 | -------------------------------------------------------------------------------- /src/common/shell.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use clap::ValueEnum; 3 | use std::process::Command; 4 | use thiserror::Error; 5 | 6 | pub const EOF: &str = "NAVIEOF"; 7 | 8 | #[derive(Debug, Clone, ValueEnum)] 9 | pub enum Shell { 10 | Bash, 11 | Zsh, 12 | Fish, 13 | Elvish, 14 | Nushell, 15 | Powershell, 16 | } 17 | 18 | #[derive(Error, Debug)] 19 | #[error("Failed to spawn child process `bash` to execute `{command}`")] 20 | pub struct ShellSpawnError { 21 | command: String, 22 | #[source] 23 | source: anyhow::Error, 24 | } 25 | 26 | impl ShellSpawnError { 27 | pub fn new(command: impl Into, source: SourceError) -> Self 28 | where 29 | SourceError: std::error::Error + Sync + Send + 'static, 30 | { 31 | ShellSpawnError { 32 | command: command.into(), 33 | source: source.into(), 34 | } 35 | } 36 | } 37 | 38 | pub fn out() -> Command { 39 | let words_str = CONFIG.shell(); 40 | let mut words_vec = shellwords::split(&words_str).expect("empty shell command"); 41 | let mut words = words_vec.iter_mut(); 42 | let first_cmd = words.next().expect("absent shell binary"); 43 | let mut cmd = Command::new(first_cmd); 44 | cmd.args(words); 45 | let dash_c = if words_str.contains("cmd.exe") { "/c" } else { "-c" }; 46 | cmd.arg(dash_c); 47 | cmd 48 | } 49 | -------------------------------------------------------------------------------- /src/common/terminal.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crossterm::style; 3 | use crossterm::terminal; 4 | 5 | use std::process::Command; 6 | 7 | const FALLBACK_WIDTH: u16 = 80; 8 | 9 | fn width_with_shell_out() -> Result { 10 | let output = if cfg!(target_os = "macos") { 11 | Command::new("stty") 12 | .arg("-f") 13 | .arg("/dev/stderr") 14 | .arg("size") 15 | .stderr(Stdio::inherit()) 16 | .output()? 17 | } else { 18 | Command::new("stty") 19 | .arg("size") 20 | .arg("-F") 21 | .arg("/dev/stderr") 22 | .stderr(Stdio::inherit()) 23 | .output()? 24 | }; 25 | 26 | if let Some(0) = output.status.code() { 27 | let stdout = String::from_utf8(output.stdout).expect("Invalid utf8 output from stty"); 28 | let mut data = stdout.split_whitespace(); 29 | data.next(); 30 | return data 31 | .next() 32 | .expect("Not enough data") 33 | .parse::() 34 | .map_err(|_| anyhow!("Invalid width")); 35 | } 36 | 37 | Err(anyhow!("Invalid status code")) 38 | } 39 | 40 | pub fn width() -> u16 { 41 | if let Ok((w, _)) = terminal::size() { 42 | w 43 | } else { 44 | width_with_shell_out().unwrap_or(FALLBACK_WIDTH) 45 | } 46 | } 47 | 48 | pub fn parse_ansi(ansi: &str) -> Option { 49 | style::Color::parse_ansi(&format!("5;{ansi}")) 50 | } 51 | 52 | #[derive(Debug, Clone)] 53 | pub struct Color(#[allow(unused)] pub style::Color); // suppress warning: field `0` is never read. 54 | 55 | impl FromStr for Color { 56 | type Err = &'static str; 57 | 58 | fn from_str(ansi: &str) -> Result { 59 | if let Some(c) = parse_ansi(ansi) { 60 | Ok(Color(c)) 61 | } else { 62 | Err("Invalid color") 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/common/url.rs: -------------------------------------------------------------------------------- 1 | use crate::common::shell::{self, ShellSpawnError}; 2 | use crate::prelude::*; 3 | use anyhow::Result; 4 | use shell::EOF; 5 | 6 | pub fn open(args: Vec) -> Result<()> { 7 | let url = args 8 | .into_iter() 9 | .next() 10 | .ok_or_else(|| anyhow!("No URL specified"))?; 11 | let code = r#" 12 | exst() { 13 | type "$1" &>/dev/null 14 | } 15 | 16 | _open_url() { 17 | local -r url="$1" 18 | if exst xdg-open; then 19 | xdg-open "$url" &disown 20 | elif exst open; then 21 | echo "$url" | xargs -I% open "%" 22 | else 23 | exit 55 24 | fi 25 | }"#; 26 | let cmd = format!( 27 | r#"{code} 28 | 29 | read -r -d '' url <<'{EOF}' 30 | {url} 31 | {EOF} 32 | 33 | _open_url "$url""#, 34 | ); 35 | shell::out() 36 | .arg(cmd.as_str()) 37 | .spawn() 38 | .map_err(|e| ShellSpawnError::new(cmd, e))? 39 | .wait()?; 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /src/config/cli.rs: -------------------------------------------------------------------------------- 1 | use crate::commands; 2 | use crate::finder::FinderChoice; 3 | 4 | use clap::{crate_version, Parser, Subcommand}; 5 | 6 | #[derive(Debug, Parser)] 7 | #[command(after_help = "\x1b[0;33mMORE INFO:\x1b[0;0m 8 | Please refer to \x1b[0;32mhttps://github.com/denisidoro/navi\x1b[0;0m 9 | 10 | \x1b[0;33mENVIRONMENT VARIABLES:\x1b[0m 11 | \x1b[0;32mNAVI_CONFIG\x1b[0;0m # path to config file 12 | \x1b[0;32mNAVI_CONFIG_YAML\x1b[0;0m # config file content 13 | 14 | \x1b[0;33mFEATURE STABILITY:\x1b[0m 15 | \x1b[0;32mexperimental\x1b[0;0m # may be removed or changed at any time 16 | \x1b[0;32mdeprecated\x1b[0;0m # may be removed in 3 months after first being deprecated 17 | 18 | \x1b[0;33mCOMMON NAVI COMMANDS:\x1b[0m 19 | Run \x1b[0;32mnavi fn welcome\x1b[0;0m to browse the cheatsheet for navi itself 20 | 21 | \x1b[0;33mEXAMPLES:\x1b[0m 22 | navi # default behavior 23 | navi fn welcome # show cheatsheets for navi itself 24 | navi --print # doesn't execute the snippet 25 | navi --tldr docker # search for docker cheatsheets using tldr 26 | navi --cheatsh docker # search for docker cheatsheets using cheatsh 27 | navi --path '/some/dir:/other/dir' # use .cheat files from custom paths 28 | navi --query git # filter results by \"git\" 29 | navi --query 'create db' --best-match # autoselect the snippet that best matches a query 30 | db=my navi --query 'create db' --best-match # same, but set the value for the variable 31 | navi repo add denisidoro/cheats # import cheats from a git repository 32 | eval \"$(navi widget zsh)\" # load the zsh widget 33 | navi --finder 'skim' # set skim as finder, instead of fzf 34 | navi --fzf-overrides '--with-nth 1,2' # show only the comment and tag columns 35 | navi --fzf-overrides '--no-select-1' # prevent autoselection in case of single line 36 | navi --fzf-overrides-var '--no-select-1' # same, but for variable selection 37 | navi --fzf-overrides '--nth 1,2' # only consider the first two columns for search 38 | navi --fzf-overrides '--no-exact' # use looser search algorithm 39 | navi --tag-rules='git,!checkout' # show non-checkout git snippets only")] 40 | #[clap(version = crate_version!())] 41 | pub(super) struct ClapConfig { 42 | /// Colon-separated list of paths containing .cheat files 43 | #[arg(short, long)] 44 | pub path: Option, 45 | 46 | /// Instead of executing a snippet, prints it to stdout 47 | #[arg(long)] 48 | #[cfg(not(feature = "disable-command-execution"))] 49 | pub print: bool, 50 | 51 | /// Returns the best match 52 | #[arg(long)] 53 | pub best_match: bool, 54 | 55 | /// Prevents variable interpolation 56 | #[arg(long)] 57 | pub prevent_interpolation: bool, 58 | 59 | /// Searches for cheatsheets using the tldr-pages repository 60 | #[arg(long)] 61 | pub tldr: Option, 62 | 63 | /// [Experimental] Comma-separated list that acts as filter for tags. Parts starting with ! represent negation 64 | #[arg(long)] 65 | pub tag_rules: Option, 66 | 67 | /// Searches for cheatsheets using the cheat.sh repository 68 | #[arg(long)] 69 | pub cheatsh: Option, 70 | 71 | /// Prepopulates the search field 72 | #[arg(short, long, allow_hyphen_values = true)] 73 | pub query: Option, 74 | 75 | /// Finder overrides for snippet selection 76 | #[arg(long, allow_hyphen_values = true)] 77 | pub fzf_overrides: Option, 78 | 79 | /// Finder overrides for variable selection 80 | #[arg(long, allow_hyphen_values = true)] 81 | pub fzf_overrides_var: Option, 82 | 83 | /// Finder application to use 84 | #[arg(long, ignore_case = true)] 85 | pub finder: Option, 86 | 87 | #[command(subcommand)] 88 | pub cmd: Option, 89 | } 90 | 91 | impl ClapConfig { 92 | pub fn new() -> Self { 93 | Self::parse() 94 | } 95 | } 96 | 97 | // #[derive(Subcommand, Debug, Clone, Runnable, HasDeps)] 98 | #[derive(Subcommand, Debug, Clone)] 99 | pub enum Command { 100 | /// [Experimental] Calls internal functions 101 | Fn(commands::func::Input), 102 | /// Manages cheatsheet repositories 103 | #[cfg(not(feature = "disable-repo-management"))] 104 | Repo(commands::repo::Input), 105 | /// Used for fzf's preview window when selecting snippets 106 | #[command(hide = true)] 107 | Preview(commands::preview::Input), 108 | /// Used for fzf's preview window when selecting variable suggestions 109 | #[command(hide = true)] 110 | PreviewVar(commands::preview::var::Input), 111 | /// Used for fzf's preview window when selecting variable suggestions 112 | #[command(hide = true)] 113 | PreviewVarStdin(commands::preview::var_stdin::Input), 114 | /// Outputs shell widget source code 115 | Widget(commands::shell::Input), 116 | /// Shows info 117 | Info(commands::info::Input), 118 | } 119 | 120 | #[derive(Debug)] 121 | pub enum Source { 122 | Filesystem(Option), 123 | Tldr(String), 124 | Cheats(String), 125 | Welcome, 126 | } 127 | 128 | pub enum Action { 129 | Print, 130 | Execute, 131 | } 132 | -------------------------------------------------------------------------------- /src/config/env.rs: -------------------------------------------------------------------------------- 1 | use crate::env_var; 2 | use crate::finder::FinderChoice; 3 | use crate::prelude::*; 4 | 5 | #[derive(Debug)] 6 | pub struct EnvConfig { 7 | pub config_yaml: Option, 8 | pub config_path: Option, 9 | pub path: Option, 10 | pub finder: Option, 11 | pub fzf_overrides: Option, 12 | pub fzf_overrides_var: Option, 13 | } 14 | 15 | impl EnvConfig { 16 | pub fn new() -> Self { 17 | Self { 18 | config_yaml: env_var::get(env_var::CONFIG_YAML).ok(), 19 | config_path: env_var::get(env_var::CONFIG).ok(), 20 | path: env_var::get(env_var::PATH).ok(), 21 | finder: env_var::get(env_var::FINDER) 22 | .ok() 23 | .and_then(|x| FinderChoice::from_str(&x).ok()), 24 | fzf_overrides: env_var::get(env_var::FZF_OVERRIDES).ok(), 25 | fzf_overrides_var: env_var::get(env_var::FZF_OVERRIDES_VAR).ok(), 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | mod cli; 2 | mod env; 3 | mod yaml; 4 | 5 | use crate::commands::func::Func; 6 | use crate::finder::FinderChoice; 7 | use crate::prelude::debug; 8 | pub use cli::*; 9 | use crossterm::style::Color; 10 | use env::EnvConfig; 11 | use yaml::YamlConfig; 12 | 13 | lazy_static! { 14 | pub static ref CONFIG: Config = Config::new(); 15 | } 16 | #[derive(Debug)] 17 | pub struct Config { 18 | yaml: YamlConfig, 19 | clap: ClapConfig, 20 | env: EnvConfig, 21 | } 22 | 23 | impl Config { 24 | pub fn new() -> Self { 25 | let env = EnvConfig::new(); 26 | let yaml = YamlConfig::get(&env).unwrap_or_else(|e| { 27 | eprintln!("Error parsing config file: {e}"); 28 | eprintln!("Fallbacking to default one..."); 29 | eprintln!(); 30 | YamlConfig::default() 31 | }); 32 | let clap = ClapConfig::new(); 33 | Self { yaml, clap, env } 34 | } 35 | 36 | pub fn best_match(&self) -> bool { 37 | self.clap.best_match 38 | } 39 | 40 | pub fn prevent_interpolation(&self) -> bool { 41 | self.clap.prevent_interpolation 42 | } 43 | 44 | pub fn cmd(&self) -> Option<&Command> { 45 | self.clap.cmd.as_ref() 46 | } 47 | 48 | pub fn source(&self) -> Source { 49 | if let Some(query) = self.clap.tldr.clone() { 50 | Source::Tldr(query) 51 | } else if let Some(query) = self.clap.cheatsh.clone() { 52 | Source::Cheats(query) 53 | } else if let Some(Command::Fn(input)) = self.cmd() { 54 | if let Func::Welcome = input.func { 55 | Source::Welcome 56 | } else { 57 | Source::Filesystem(self.path()) 58 | } 59 | } else { 60 | Source::Filesystem(self.path()) 61 | } 62 | } 63 | 64 | pub fn path(&self) -> Option { 65 | if self.clap.path.is_some() { 66 | debug!("CLAP PATH: {}", self.clap.path.as_ref().unwrap()); 67 | } 68 | 69 | self.clap 70 | .path 71 | .clone() 72 | .or_else(|| { 73 | if self.env.path.is_some() { 74 | debug!("ENV PATH: {}", self.env.path.as_ref().unwrap()); 75 | } 76 | 77 | self.env.path.clone() 78 | }) 79 | .or_else(|| { 80 | let p = self.yaml.cheats.paths.clone(); 81 | 82 | if p.is_empty() { 83 | None 84 | } else { 85 | debug!("MULTIPLE YAML PATH: {}", p.as_slice().join(",")); 86 | Some(p.join(crate::filesystem::JOIN_SEPARATOR)) 87 | } 88 | }) 89 | .or_else(|| { 90 | if self.yaml.cheats.path.is_some() { 91 | debug!( 92 | "DEPRECATED UNIQUE YAML PATH: {}", 93 | self.yaml.cheats.path.as_ref().unwrap() 94 | ); 95 | } 96 | 97 | self.yaml.cheats.path.clone() 98 | }) 99 | .or_else(|| { 100 | debug!("No specific path given!"); 101 | 102 | None 103 | }) 104 | } 105 | 106 | pub fn finder(&self) -> FinderChoice { 107 | self.clap 108 | .finder 109 | .or(self.env.finder) 110 | .unwrap_or(self.yaml.finder.command) 111 | } 112 | 113 | pub fn fzf_overrides(&self) -> Option { 114 | self.clap 115 | .fzf_overrides 116 | .clone() 117 | .or_else(|| self.env.fzf_overrides.clone()) 118 | .or_else(|| self.yaml.finder.overrides.clone()) 119 | } 120 | 121 | pub fn fzf_overrides_var(&self) -> Option { 122 | self.clap 123 | .fzf_overrides_var 124 | .clone() 125 | .or_else(|| self.env.fzf_overrides_var.clone()) 126 | .or_else(|| self.yaml.finder.overrides_var.clone()) 127 | } 128 | 129 | pub fn delimiter_var(&self) -> Option { 130 | self.yaml.finder.delimiter_var.clone() 131 | } 132 | 133 | pub fn tealdeer(&self) -> bool { 134 | self.yaml.client.tealdeer 135 | } 136 | 137 | pub fn shell(&self) -> String { 138 | self.yaml.shell.command.clone() 139 | } 140 | 141 | pub fn finder_shell(&self) -> String { 142 | self.yaml 143 | .shell 144 | .finder_command 145 | .clone() 146 | .unwrap_or_else(|| self.yaml.shell.command.clone()) 147 | } 148 | 149 | pub fn tag_rules(&self) -> Option { 150 | self.clap 151 | .tag_rules 152 | .clone() 153 | .or_else(|| self.yaml.search.tags.clone()) 154 | } 155 | 156 | pub fn tag_color(&self) -> Color { 157 | self.yaml.style.tag.color.get() 158 | } 159 | 160 | pub fn comment_color(&self) -> Color { 161 | self.yaml.style.comment.color.get() 162 | } 163 | 164 | pub fn snippet_color(&self) -> Color { 165 | self.yaml.style.snippet.color.get() 166 | } 167 | 168 | pub fn tag_width_percentage(&self) -> u16 { 169 | self.yaml.style.tag.width_percentage 170 | } 171 | 172 | pub fn comment_width_percentage(&self) -> u16 { 173 | self.yaml.style.comment.width_percentage 174 | } 175 | 176 | pub fn snippet_width_percentage(&self) -> u16 { 177 | self.yaml.style.snippet.width_percentage 178 | } 179 | 180 | pub fn tag_min_width(&self) -> u16 { 181 | self.yaml.style.tag.min_width 182 | } 183 | 184 | pub fn comment_min_width(&self) -> u16 { 185 | self.yaml.style.comment.min_width 186 | } 187 | 188 | pub fn snippet_min_width(&self) -> u16 { 189 | self.yaml.style.snippet.min_width 190 | } 191 | 192 | #[cfg(feature = "disable-command-execution")] 193 | fn print(&self) -> bool { 194 | true 195 | } 196 | 197 | #[cfg(not(feature = "disable-command-execution"))] 198 | fn print(&self) -> bool { 199 | self.clap.print 200 | } 201 | 202 | pub fn action(&self) -> Action { 203 | if self.print() { 204 | Action::Print 205 | } else { 206 | Action::Execute 207 | } 208 | } 209 | 210 | pub fn get_query(&self) -> Option { 211 | let q = self.clap.query.clone(); 212 | if q.is_some() { 213 | return q; 214 | } 215 | if self.best_match() { 216 | match self.source() { 217 | Source::Tldr(q) => Some(q), 218 | Source::Cheats(q) => Some(q), 219 | _ => Some(String::from("")), 220 | } 221 | } else { 222 | None 223 | } 224 | } 225 | } 226 | 227 | impl Default for Config { 228 | fn default() -> Self { 229 | Self::new() 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/config/yaml.rs: -------------------------------------------------------------------------------- 1 | use super::env::EnvConfig; 2 | use crate::common::fs; 3 | use crate::filesystem::default_config_pathbuf; 4 | use crate::finder::FinderChoice; 5 | use crate::prelude::*; 6 | use crossterm::style::Color as TerminalColor; 7 | use serde::de; 8 | 9 | #[derive(Deserialize, Debug)] 10 | pub struct Color(#[serde(deserialize_with = "color_deserialize")] TerminalColor); 11 | 12 | impl Color { 13 | pub fn get(&self) -> TerminalColor { 14 | self.0 15 | } 16 | } 17 | 18 | fn color_deserialize<'de, D>(deserializer: D) -> Result 19 | where 20 | D: de::Deserializer<'de>, 21 | { 22 | let s: String = Deserialize::deserialize(deserializer)?; 23 | TerminalColor::try_from(s.as_str()) 24 | .map_err(|_| de::Error::custom(format!("Failed to deserialize color: {s}"))) 25 | } 26 | 27 | #[derive(Deserialize, Debug)] 28 | #[serde(default)] 29 | pub struct ColorWidth { 30 | pub color: Color, 31 | pub width_percentage: u16, 32 | pub min_width: u16, 33 | } 34 | 35 | #[derive(Deserialize, Debug)] 36 | #[serde(default)] 37 | pub struct Style { 38 | pub tag: ColorWidth, 39 | pub comment: ColorWidth, 40 | pub snippet: ColorWidth, 41 | } 42 | 43 | #[derive(Deserialize, Debug)] 44 | #[serde(default)] 45 | pub struct Finder { 46 | #[serde(deserialize_with = "finder_deserialize")] 47 | pub command: FinderChoice, 48 | pub overrides: Option, 49 | pub overrides_var: Option, 50 | pub delimiter_var: Option, 51 | } 52 | 53 | fn finder_deserialize<'de, D>(deserializer: D) -> Result 54 | where 55 | D: de::Deserializer<'de>, 56 | { 57 | let s: String = Deserialize::deserialize(deserializer)?; 58 | FinderChoice::from_str(s.to_lowercase().as_str()) 59 | .map_err(|_| de::Error::custom(format!("Failed to deserialize finder: {s}"))) 60 | } 61 | 62 | #[derive(Deserialize, Default, Debug)] 63 | #[serde(default)] 64 | pub struct Cheats { 65 | pub path: Option, 66 | pub paths: Vec, 67 | } 68 | 69 | #[derive(Deserialize, Default, Debug)] 70 | #[serde(default)] 71 | pub struct Search { 72 | pub tags: Option, 73 | } 74 | 75 | #[derive(Deserialize, Debug)] 76 | #[serde(default)] 77 | pub struct Shell { 78 | pub command: String, 79 | pub finder_command: Option, 80 | } 81 | 82 | #[derive(Deserialize, Debug)] 83 | #[serde(default)] 84 | #[derive(Default)] 85 | pub struct Client { 86 | pub tealdeer: bool, 87 | } 88 | 89 | #[derive(Deserialize, Debug)] 90 | #[serde(default)] 91 | pub struct YamlConfig { 92 | pub style: Style, 93 | pub finder: Finder, 94 | pub cheats: Cheats, 95 | pub search: Search, 96 | pub shell: Shell, 97 | pub client: Client, 98 | pub source: String, // <= The source of the current configuration 99 | } 100 | 101 | impl YamlConfig { 102 | fn from_str(text: &str) -> Result { 103 | serde_yaml::from_str(text).map_err(|e| e.into()) 104 | } 105 | 106 | fn from_path(path: &Path) -> Result { 107 | let file = fs::open(path)?; 108 | let reader = BufReader::new(file); 109 | serde_yaml::from_reader(reader).map_err(|e| e.into()) 110 | } 111 | 112 | pub fn get(env: &EnvConfig) -> Result { 113 | if let Some(yaml) = env.config_yaml.as_ref() { 114 | // We're getting the configuration from the environment variable `NAVI_CONFIG_YAML` 115 | let mut cfg = Self::from_str(yaml)?; 116 | cfg.source = "ENV_NAVI_CONFIG_YAML".to_string(); 117 | 118 | return Ok(cfg); 119 | } 120 | if let Some(path_str) = env.config_path.as_ref() { 121 | // We're getting the configuration from a file given in the environment variable 'NAVI_CONFIG' 122 | 123 | let p = PathBuf::from(path_str); 124 | let mut cfg = YamlConfig::from_path(&p)?; 125 | cfg.source = "ENV_NAVI_CONFIG".to_string(); 126 | 127 | return Ok(cfg); 128 | } 129 | if let Ok(p) = default_config_pathbuf() { 130 | // We're getting the configuration from the default path 131 | 132 | if p.exists() { 133 | let mut cfg = YamlConfig::from_path(&p)?; 134 | cfg.source = "DEFAULT_CONFIG_FILE".to_string(); 135 | 136 | return Ok(cfg); 137 | } 138 | } 139 | 140 | // As no configuration has been found, we set the YAML configuration 141 | // to be its default (built-in) value. 142 | Ok(YamlConfig::default()) 143 | } 144 | } 145 | 146 | impl Default for ColorWidth { 147 | fn default() -> Self { 148 | Self { 149 | color: Color(TerminalColor::Blue), 150 | width_percentage: 26, 151 | min_width: 20, 152 | } 153 | } 154 | } 155 | 156 | impl Default for Style { 157 | fn default() -> Self { 158 | Self { 159 | tag: ColorWidth { 160 | color: Color(TerminalColor::Cyan), 161 | width_percentage: 26, 162 | min_width: 20, 163 | }, 164 | comment: ColorWidth { 165 | color: Color(TerminalColor::Blue), 166 | width_percentage: 42, 167 | min_width: 45, 168 | }, 169 | snippet: Default::default(), 170 | } 171 | } 172 | } 173 | 174 | impl Default for Finder { 175 | fn default() -> Self { 176 | Self { 177 | command: FinderChoice::Fzf, 178 | overrides: None, 179 | overrides_var: None, 180 | delimiter_var: None, 181 | } 182 | } 183 | } 184 | 185 | impl Default for Shell { 186 | fn default() -> Self { 187 | Self { 188 | command: "bash".to_string(), 189 | finder_command: None, 190 | } 191 | } 192 | } 193 | 194 | impl Default for YamlConfig { 195 | fn default() -> Self { 196 | Self { 197 | style: Default::default(), 198 | finder: Default::default(), 199 | cheats: Default::default(), 200 | search: Default::default(), 201 | shell: Default::default(), 202 | client: Default::default(), 203 | source: "BUILT-IN".to_string(), 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/deser/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use unicode_width::UnicodeWidthStr; 3 | 4 | pub mod raycast; 5 | pub mod terminal; 6 | 7 | const NEWLINE_ESCAPE_CHAR: char = '\x15'; 8 | pub const LINE_SEPARATOR: &str = " \x15 "; 9 | 10 | lazy_static! { 11 | pub static ref NEWLINE_REGEX: Regex = Regex::new(r"\\\s+").expect("Invalid regex"); 12 | pub static ref VAR_REGEX: Regex = Regex::new(r"\\?<(\w[\w\d\-_]*)>").expect("Invalid regex"); 13 | } 14 | 15 | pub fn with_new_lines(txt: String) -> String { 16 | txt.replace(LINE_SEPARATOR, "\n") 17 | } 18 | 19 | pub fn fix_newlines(txt: &str) -> String { 20 | if txt.contains(NEWLINE_ESCAPE_CHAR) { 21 | (*NEWLINE_REGEX) 22 | .replace_all(txt.replace(LINE_SEPARATOR, " ").as_str(), "") 23 | .to_string() 24 | } else { 25 | txt.to_string() 26 | } 27 | } 28 | 29 | fn limit_str(text: &str, length: usize) -> String { 30 | let len = UnicodeWidthStr::width(text); 31 | if len <= length { 32 | format!("{}{}", text, " ".repeat(length - len)) 33 | } else { 34 | let mut new_length = length; 35 | let mut actual_length = 9999; 36 | let mut txt = text.to_owned(); 37 | while actual_length >= length { 38 | txt = txt.chars().take(new_length - 1).collect::(); 39 | actual_length = UnicodeWidthStr::width(txt.as_str()); 40 | new_length -= 1; 41 | } 42 | format!("{}…{}", txt, " ".repeat(length - actual_length - 1)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/deser/raycast.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::structures::item::Item; 3 | 4 | const FIELD_SEP_ESCAPE_CHAR: char = '\x16'; 5 | 6 | pub fn write(item: &Item) -> String { 7 | format!( 8 | "{hash}{delimiter}{tags}{delimiter}{comment}{delimiter}{icon}{delimiter}{snippet}\n", 9 | hash = item.hash(), 10 | tags = item.tags, 11 | comment = item.comment, 12 | delimiter = FIELD_SEP_ESCAPE_CHAR, 13 | icon = item.icon.clone().unwrap_or_default(), 14 | snippet = &item.snippet.trim_end_matches(LINE_SEPARATOR), 15 | ) 16 | } 17 | 18 | pub fn read(line: &str) -> Result { 19 | let mut parts = line.split(FIELD_SEP_ESCAPE_CHAR); 20 | let hash: u64 = parts 21 | .next() 22 | .context("no hash")? 23 | .parse() 24 | .context("hash not a u64")?; 25 | let tags = parts.next().context("no tags")?.into(); 26 | let comment = parts.next().context("no comment")?.into(); 27 | let icon_str = parts.next().context("no icon")?; 28 | let snippet = parts.next().context("no snippet")?.into(); 29 | 30 | let icon = if icon_str.is_empty() { 31 | None 32 | } else { 33 | Some(icon_str.into()) 34 | }; 35 | 36 | let item = Item { 37 | tags, 38 | comment, 39 | icon, 40 | snippet, 41 | ..Default::default() 42 | }; 43 | 44 | if item.hash() != hash { 45 | dbg!(&item.hash()); 46 | dbg!(hash); 47 | Err(anyhow!("Incorrect hash")) 48 | } else { 49 | Ok(item) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/deser/terminal.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::common::terminal; 3 | use crate::structures::item::Item; 4 | use crossterm::style::{style, Stylize}; 5 | use std::cmp::max; 6 | 7 | pub fn get_widths() -> (usize, usize, usize) { 8 | let width = terminal::width(); 9 | let tag_width_percentage = max( 10 | CONFIG.tag_min_width(), 11 | width * CONFIG.tag_width_percentage() / 100, 12 | ); 13 | let comment_width_percentage = max( 14 | CONFIG.comment_min_width(), 15 | width * CONFIG.comment_width_percentage() / 100, 16 | ); 17 | let snippet_width_percentage = max( 18 | CONFIG.snippet_min_width(), 19 | width * CONFIG.snippet_width_percentage() / 100, 20 | ); 21 | ( 22 | usize::from(tag_width_percentage), 23 | usize::from(comment_width_percentage), 24 | usize::from(snippet_width_percentage), 25 | ) 26 | } 27 | 28 | pub const DELIMITER: &str = r" ⠀"; 29 | 30 | lazy_static! { 31 | pub static ref COLUMN_WIDTHS: (usize, usize, usize) = get_widths(); 32 | } 33 | 34 | pub fn write(item: &Item) -> String { 35 | let (tag_width_percentage, comment_width_percentage, snippet_width_percentage) = *COLUMN_WIDTHS; 36 | format!( 37 | "{tags_short}{delimiter}{comment_short}{delimiter}{snippet_short}{delimiter}{tags}{delimiter}{comment}{delimiter}{snippet}{delimiter}{file_index}{delimiter}\n", 38 | tags_short = style(limit_str(&item.tags, tag_width_percentage)).with(CONFIG.tag_color()), 39 | comment_short = style(limit_str(&item.comment, comment_width_percentage)).with(CONFIG.comment_color()), 40 | snippet_short = style(limit_str(&fix_newlines(&item.snippet), snippet_width_percentage)).with(CONFIG.snippet_color()), 41 | tags = item.tags, 42 | comment = item.comment, 43 | delimiter = DELIMITER, 44 | snippet = &item.snippet.trim_end_matches(LINE_SEPARATOR), 45 | file_index = item.file_index.unwrap_or(0), 46 | ) 47 | } 48 | 49 | pub fn read(raw_snippet: &str, is_single: bool) -> Result<(&str, Item)> { 50 | let mut lines = raw_snippet.split('\n'); 51 | let key = if is_single { 52 | "enter" 53 | } else { 54 | lines 55 | .next() 56 | .context("Key was promised but not present in `selections`")? 57 | }; 58 | 59 | let mut parts = lines 60 | .next() 61 | .context("No more parts in `selections`")? 62 | .split(DELIMITER) 63 | .skip(3); 64 | 65 | let tags = parts.next().unwrap_or("").into(); 66 | let comment = parts.next().unwrap_or("").into(); 67 | let snippet = parts.next().unwrap_or("").into(); 68 | let file_index = parts.next().unwrap_or("").parse().ok(); 69 | 70 | let item = Item { 71 | tags, 72 | comment, 73 | snippet, 74 | file_index, 75 | ..Default::default() 76 | }; 77 | 78 | Ok((key, item)) 79 | } 80 | -------------------------------------------------------------------------------- /src/env_var.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | pub use env::remove_var as remove; 3 | pub use env::set_var as set; 4 | pub use env::var as get; 5 | use std::env; 6 | 7 | pub const PREVIEW_INITIAL_SNIPPET: &str = "NAVI_PREVIEW_INITIAL_SNIPPET"; 8 | pub const PREVIEW_TAGS: &str = "NAVI_PREVIEW_TAGS"; 9 | pub const PREVIEW_COMMENT: &str = "NAVI_PREVIEW_COMMENT"; 10 | pub const PREVIEW_COLUMN: &str = "NAVI_PREVIEW_COLUMN"; 11 | pub const PREVIEW_DELIMITER: &str = "NAVI_PREVIEW_DELIMITER"; 12 | pub const PREVIEW_MAP: &str = "NAVI_PREVIEW_MAP"; 13 | 14 | pub const PATH: &str = "NAVI_PATH"; 15 | pub const FZF_OVERRIDES: &str = "NAVI_FZF_OVERRIDES"; 16 | pub const FZF_OVERRIDES_VAR: &str = "NAVI_FZF_OVERRIDES_VAR"; 17 | pub const FINDER: &str = "NAVI_FINDER"; 18 | 19 | pub const CONFIG: &str = "NAVI_CONFIG"; 20 | pub const CONFIG_YAML: &str = "NAVI_CONFIG_YAML"; 21 | 22 | pub fn parse(varname: &str) -> Option { 23 | if let Ok(x) = env::var(varname) { 24 | x.parse::().ok() 25 | } else { 26 | None 27 | } 28 | } 29 | 30 | pub fn must_get(name: &str) -> String { 31 | if let Ok(v) = env::var(name) { 32 | v 33 | } else { 34 | panic!("{name} not set") 35 | } 36 | } 37 | 38 | pub fn escape(name: &str) -> String { 39 | name.replace('-', "_") 40 | } 41 | -------------------------------------------------------------------------------- /src/filesystem.rs: -------------------------------------------------------------------------------- 1 | pub use crate::common::fs::{create_dir, exe_string, read_lines, remove_dir}; 2 | use crate::env_var; 3 | use crate::parser::Parser; 4 | use crate::prelude::*; 5 | 6 | use crate::structures::fetcher; 7 | use etcetera::BaseStrategy; 8 | use regex::Regex; 9 | 10 | use std::cell::RefCell; 11 | use std::path::MAIN_SEPARATOR; 12 | 13 | use walkdir::WalkDir; 14 | 15 | /// Multiple paths are joint by a platform-specific separator. 16 | /// FIXME: it's actually incorrect to assume a path doesn't containing this separator 17 | #[cfg(target_family = "windows")] 18 | pub const JOIN_SEPARATOR: &str = ";"; 19 | #[cfg(not(target_family = "windows"))] 20 | pub const JOIN_SEPARATOR: &str = ":"; 21 | 22 | pub fn all_cheat_files(path: &Path) -> Vec { 23 | WalkDir::new(path) 24 | .follow_links(true) 25 | .into_iter() 26 | .filter_map(|e| e.ok()) 27 | .map(|e| e.path().to_str().unwrap_or("").to_string()) 28 | .filter(|e| e.ends_with(".cheat") || e.ends_with(".cheat.md")) 29 | .collect::>() 30 | } 31 | 32 | fn paths_from_path_param(env_var: &str) -> impl Iterator { 33 | env_var.split(JOIN_SEPARATOR).filter(|folder| folder != &"") 34 | } 35 | 36 | fn compiled_default_path(path: Option<&str>) -> Option { 37 | match path { 38 | Some(path) => { 39 | let path = if path.contains(MAIN_SEPARATOR) { 40 | path.split(MAIN_SEPARATOR).next().unwrap() 41 | } else { 42 | path 43 | }; 44 | let path = Path::new(path); 45 | if path.exists() { 46 | Some(path.to_path_buf()) 47 | } else { 48 | None 49 | } 50 | } 51 | None => None, 52 | } 53 | } 54 | 55 | pub fn default_cheat_pathbuf() -> Result { 56 | let mut pathbuf = get_data_dir_by_platform()?; 57 | 58 | pathbuf.push("navi"); 59 | pathbuf.push("cheats"); 60 | 61 | if pathbuf.exists() { 62 | if let Some(path) = compiled_default_path(option_env!("NAVI_PATH")) { 63 | pathbuf = path; 64 | } 65 | } 66 | Ok(pathbuf) 67 | } 68 | 69 | pub fn default_config_pathbuf() -> Result { 70 | let mut pathbuf = get_config_dir_by_platform()?; 71 | 72 | pathbuf.push("navi"); 73 | pathbuf.push("config.yaml"); 74 | 75 | if !pathbuf.exists() { 76 | if let Some(path) = compiled_default_path(option_env!("NAVI_CONFIG")) { 77 | pathbuf = path; 78 | } 79 | } 80 | Ok(pathbuf) 81 | } 82 | 83 | pub fn cheat_paths(path: Option) -> Result { 84 | if let Some(p) = path { 85 | Ok(p) 86 | } else { 87 | Ok(default_cheat_pathbuf()?.to_string()) 88 | } 89 | } 90 | 91 | //////////////////////////////////////////////////////////////////////////////////////////////////// 92 | // 93 | // Here are other functions, unrelated to CLI commands (or at least not directly related) 94 | // 95 | //////////////////////////////////////////////////////////////////////////////////////////////////// 96 | 97 | /// Returns the data dir computed for each platform. 98 | /// 99 | /// We are currently handling two cases: When the platform is `macOS` and when the platform isn't (including `Windows` and `Linux/Unix` platforms) 100 | fn get_data_dir_by_platform() -> Result { 101 | if cfg!(target_os = "macos") { 102 | let base_dirs = etcetera::base_strategy::Apple::new()?; 103 | 104 | Ok(base_dirs.data_dir()) 105 | } else { 106 | let base_dirs = etcetera::choose_base_strategy()?; 107 | 108 | Ok(base_dirs.data_dir()) 109 | } 110 | } 111 | 112 | /// Returns the config dir computed for each platform. 113 | /// 114 | /// We are currently handling two cases: When the platform is `macOS` and when the platform isn't (including `Windows` and `Linux/Unix` platforms) 115 | fn get_config_dir_by_platform() -> Result { 116 | if cfg!(target_os = "macos") { 117 | let base_dirs = etcetera::base_strategy::Apple::new()?; 118 | 119 | Ok(base_dirs.config_dir()) 120 | } else { 121 | let base_dirs = etcetera::choose_base_strategy()?; 122 | 123 | Ok(base_dirs.config_dir()) 124 | } 125 | } 126 | 127 | pub fn tmp_pathbuf() -> Result { 128 | let mut root = default_cheat_pathbuf()?; 129 | root.push("tmp"); 130 | Ok(root) 131 | } 132 | 133 | fn interpolate_paths(paths: String) -> String { 134 | let re = Regex::new(r#"\$\{?[a-zA-Z_][a-zA-Z_0-9]*"#).unwrap(); 135 | let mut newtext = paths.to_string(); 136 | for capture in re.captures_iter(&paths) { 137 | if let Some(c) = capture.get(0) { 138 | let varname = c.as_str().replace(['$', '{', '}'], ""); 139 | if let Ok(replacement) = &env_var::get(&varname) { 140 | newtext = newtext 141 | .replace(&format!("${varname}"), replacement) 142 | .replace(&format!("${{{varname}}}"), replacement); 143 | } 144 | } 145 | } 146 | newtext 147 | } 148 | 149 | #[derive(Debug)] 150 | pub struct Fetcher { 151 | path: Option, 152 | files: RefCell>, 153 | } 154 | 155 | impl Fetcher { 156 | pub fn new(path: Option) -> Self { 157 | Self { 158 | path, 159 | files: Default::default(), 160 | } 161 | } 162 | } 163 | 164 | impl fetcher::Fetcher for Fetcher { 165 | fn fetch(&self, parser: &mut Parser) -> Result { 166 | let mut found_something = false; 167 | 168 | let path = self.path.clone(); 169 | let paths = cheat_paths(path); 170 | 171 | if paths.is_err() { 172 | return Ok(false); 173 | }; 174 | 175 | let paths = paths.expect("Unable to get paths"); 176 | let interpolated_paths = interpolate_paths(paths); 177 | let folders = paths_from_path_param(&interpolated_paths); 178 | 179 | let home_regex = Regex::new(r"^~").unwrap(); 180 | let home = etcetera::home_dir().ok(); 181 | 182 | // parser.filter = self.tag_rules.as_ref().map(|r| gen_lists(r.as_str())); 183 | 184 | for folder in folders { 185 | let interpolated_folder = match &home { 186 | Some(h) => home_regex.replace(folder, h.to_string_lossy()).to_string(), 187 | None => folder.to_string(), 188 | }; 189 | let folder_pathbuf = PathBuf::from(interpolated_folder); 190 | let cheat_files = all_cheat_files(&folder_pathbuf); 191 | debug!("read cheat files in `{folder_pathbuf:?}`: {cheat_files:#?}"); 192 | for file in cheat_files { 193 | self.files.borrow_mut().push(file.clone()); 194 | let index = self.files.borrow().len() - 1; 195 | let read_file_result = { 196 | let path = PathBuf::from(&file); 197 | let lines = read_lines(&path)?; 198 | parser.read_lines(lines, &file, Some(index)) 199 | }; 200 | 201 | if read_file_result.is_ok() && !found_something { 202 | found_something = true 203 | } 204 | } 205 | } 206 | 207 | debug!("FilesystemFetcher = {self:#?}"); 208 | Ok(found_something) 209 | } 210 | 211 | fn files(&self) -> Vec { 212 | self.files.borrow().clone() 213 | } 214 | } 215 | 216 | #[cfg(test)] 217 | mod tests { 218 | use super::*; 219 | 220 | /* TODO 221 | 222 | use crate::finder::structures::{Opts as FinderOpts, SuggestionType}; 223 | use crate::writer; 224 | use std::process::{Command, Stdio}; 225 | 226 | #[test] 227 | fn test_read_file() { 228 | let path = "tests/cheats/ssh.cheat"; 229 | let mut variables = VariableMap::new(); 230 | let mut child = Command::new("cat") 231 | .stdin(Stdio::piped()) 232 | .stdout(Stdio::null()) 233 | .spawn() 234 | .unwrap(); 235 | let child_stdin = child.stdin.as_mut().unwrap(); 236 | let mut visited_lines: HashSet = HashSet::new(); 237 | let mut writer: Box = Box::new(writer::terminal::Writer::new()); 238 | read_file( 239 | path, 240 | 0, 241 | &mut variables, 242 | &mut visited_lines, 243 | &mut *writer, 244 | child_stdin, 245 | ) 246 | .unwrap(); 247 | let expected_suggestion = ( 248 | r#" echo -e "$(whoami)\nroot" "#.to_string(), 249 | Some(FinderOpts { 250 | header_lines: 0, 251 | column: None, 252 | delimiter: None, 253 | suggestion_type: SuggestionType::SingleSelection, 254 | ..Default::default() 255 | }), 256 | ); 257 | let actual_suggestion = variables.get_suggestion("ssh", "user"); 258 | assert_eq!(Some(&expected_suggestion), actual_suggestion); 259 | } 260 | */ 261 | 262 | #[test] 263 | fn splitting_of_dirs_param_may_not_contain_empty_items() { 264 | // Trailing colon indicates potential extra path. Split returns an empty item for it. This empty item should be filtered away, which is what this test checks. 265 | let given_path_config = "SOME_PATH:ANOTHER_PATH:"; 266 | 267 | let found_paths = paths_from_path_param(given_path_config); 268 | 269 | let mut expected_paths = vec!["SOME_PATH", "ANOTHER_PATH"].into_iter(); 270 | 271 | for found in found_paths { 272 | let expected = expected_paths.next().unwrap(); 273 | assert_eq!(found, expected) 274 | } 275 | } 276 | 277 | #[test] 278 | fn test_default_config_pathbuf() { 279 | let base_dirs = etcetera::choose_base_strategy().expect("could not determine base directories"); 280 | 281 | let expected = { 282 | let mut e = base_dirs.config_dir(); 283 | e.push("navi"); 284 | e.push("config.yaml"); 285 | e.to_string_lossy().to_string() 286 | }; 287 | 288 | let config = default_config_pathbuf().expect("could not find default config path"); 289 | 290 | assert_eq!(expected, config.to_string_lossy().to_string()) 291 | } 292 | 293 | #[test] 294 | fn test_default_cheat_pathbuf() { 295 | let base_dirs = etcetera::choose_base_strategy().expect("could not determine base directories"); 296 | 297 | let expected = { 298 | let mut e = base_dirs.data_dir(); 299 | e.push("navi"); 300 | e.push("cheats"); 301 | e.to_string_lossy().to_string() 302 | }; 303 | 304 | let cheats = default_cheat_pathbuf().expect("could not find default config path"); 305 | 306 | assert_eq!(expected, cheats.to_string_lossy().to_string()) 307 | } 308 | 309 | #[test] 310 | #[cfg(target_family = "windows")] 311 | fn multiple_paths() { 312 | let p = r#"C:\Users\Administrator\AppData\Roaming\navi\config.yaml"#; 313 | let paths = &[p; 2].join(JOIN_SEPARATOR); 314 | assert_eq!(paths_from_path_param(paths).collect::>(), [p; 2]); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/finder/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::deser; 2 | use crate::prelude::*; 3 | use std::io::Write; 4 | use std::process::{self, Output}; 5 | use std::process::{Command, Stdio}; 6 | pub mod structures; 7 | use clap::ValueEnum; 8 | pub use post::process; 9 | use structures::Opts; 10 | use structures::SuggestionType; 11 | 12 | const MIN_FZF_VERSION_MAJOR: u32 = 0; 13 | const MIN_FZF_VERSION_MINOR: u32 = 23; 14 | const MIN_FZF_VERSION_PATCH: u32 = 1; 15 | 16 | mod post; 17 | 18 | #[derive(Debug, Clone, Copy, Deserialize, ValueEnum)] 19 | pub enum FinderChoice { 20 | Fzf, 21 | Skim, 22 | } 23 | 24 | impl FromStr for FinderChoice { 25 | type Err = &'static str; 26 | 27 | fn from_str(s: &str) -> Result { 28 | match s { 29 | "fzf" => Ok(FinderChoice::Fzf), 30 | "skim" => Ok(FinderChoice::Skim), 31 | _ => Err("no match"), 32 | } 33 | } 34 | } 35 | 36 | fn parse(out: Output, opts: Opts) -> Result { 37 | let text = match out.status.code() { 38 | Some(0) | Some(1) | Some(2) => { 39 | String::from_utf8(out.stdout).context("Invalid utf8 received from finder")? 40 | } 41 | Some(130) => process::exit(130), 42 | _ => { 43 | let err = String::from_utf8(out.stderr) 44 | .unwrap_or_else(|_| "".to_owned()); 45 | panic!("External command failed:\n {err}") 46 | } 47 | }; 48 | 49 | let output = post::parse_output_single(text, opts.suggestion_type)?; 50 | post::process(output, opts.column, opts.delimiter.as_deref(), opts.map) 51 | } 52 | 53 | impl FinderChoice { 54 | fn check_fzf_version() -> Option<(u32, u32, u32)> { 55 | let output = Command::new("fzf").arg("--version").output().ok()?.stdout; 56 | let version_string = String::from_utf8(output).ok()?; 57 | let version_parts: Vec<_> = version_string.split('.').collect(); 58 | if version_parts.len() == 3 { 59 | let major = version_parts[0].parse().ok()?; 60 | let minor = version_parts[1].parse().ok()?; 61 | let patch = version_parts[2].split_whitespace().next()?.parse().ok()?; 62 | Some((major, minor, patch)) 63 | } else { 64 | None 65 | } 66 | } 67 | 68 | pub fn call(&self, finder_opts: Opts, stdin_fn: F) -> Result<(String, R)> 69 | where 70 | F: Fn(&mut dyn Write) -> Result, 71 | { 72 | let finder_str = match self { 73 | Self::Fzf => "fzf", 74 | Self::Skim => "sk", 75 | }; 76 | 77 | if let Self::Fzf = self { 78 | if let Some((major, minor, patch)) = Self::check_fzf_version() { 79 | if major == MIN_FZF_VERSION_MAJOR 80 | && minor < MIN_FZF_VERSION_MINOR 81 | && patch < MIN_FZF_VERSION_PATCH 82 | { 83 | eprintln!( 84 | "Warning: Fzf version {major}.{minor} does not support the preview window layout used by navi.", 85 | ); 86 | eprintln!( 87 | "Consider updating Fzf to a version >= {MIN_FZF_VERSION_MAJOR}.{MIN_FZF_VERSION_MINOR}.{MIN_FZF_VERSION_PATCH} or use a compatible layout.", 88 | ); 89 | process::exit(1); 90 | } 91 | } 92 | } 93 | 94 | let mut command = Command::new(finder_str); 95 | let opts = finder_opts.clone(); 96 | 97 | let preview_height = match self { 98 | FinderChoice::Skim => 3, 99 | _ => 2, 100 | }; 101 | 102 | let bindings = if opts.suggestion_type == SuggestionType::MultipleSelections { 103 | ",ctrl-r:toggle-all" 104 | } else { 105 | "" 106 | }; 107 | 108 | command.args([ 109 | "--preview", 110 | "", 111 | "--preview-window", 112 | format!("up:{preview_height}:nohidden").as_str(), 113 | "--delimiter", 114 | deser::terminal::DELIMITER.to_string().as_str(), 115 | "--ansi", 116 | "--bind", 117 | format!("ctrl-j:down,ctrl-k:up{bindings}").as_str(), 118 | "--exact", 119 | ]); 120 | 121 | if !opts.show_all_columns { 122 | command.args(["--with-nth", "1,2,3"]); 123 | } 124 | 125 | if !opts.prevent_select1 { 126 | if let Self::Fzf = self { 127 | command.arg("--select-1"); 128 | } 129 | } 130 | 131 | match opts.suggestion_type { 132 | SuggestionType::MultipleSelections => { 133 | command.arg("--multi"); 134 | } 135 | SuggestionType::Disabled => { 136 | if let Self::Fzf = self { 137 | command.args(["--print-query", "--no-select-1"]); 138 | }; 139 | } 140 | SuggestionType::SnippetSelection => { 141 | command.args(["--expect", "ctrl-y,ctrl-o,enter"]); 142 | } 143 | SuggestionType::SingleRecommendation => { 144 | command.args(["--print-query", "--expect", "tab,enter"]); 145 | } 146 | _ => {} 147 | } 148 | 149 | if let Some(p) = opts.preview { 150 | command.args(["--preview", &p]); 151 | } 152 | 153 | if let Some(q) = opts.query { 154 | command.args(["--query", &q]); 155 | } 156 | 157 | if let Some(f) = opts.filter { 158 | command.args(["--filter", &f]); 159 | } 160 | 161 | if let Some(d) = opts.delimiter { 162 | command.args(["--delimiter", &d]); 163 | } 164 | 165 | if let Some(h) = opts.header { 166 | command.args(["--header", &h]); 167 | } 168 | 169 | if let Some(p) = opts.prompt { 170 | command.args(["--prompt", &p]); 171 | } 172 | 173 | if let Some(pw) = opts.preview_window { 174 | command.args(["--preview-window", &pw]); 175 | } 176 | 177 | if opts.header_lines > 0 { 178 | command.args(["--header-lines", format!("{}", opts.header_lines).as_str()]); 179 | } 180 | 181 | if let Some(o) = opts.overrides { 182 | shellwords::split(&o)? 183 | .into_iter() 184 | .filter(|s| !s.is_empty()) 185 | .for_each(|s| { 186 | command.arg(s); 187 | }); 188 | } 189 | 190 | command 191 | .env("SHELL", CONFIG.finder_shell()) 192 | .stdin(Stdio::piped()) 193 | .stdout(Stdio::piped()); 194 | debug!(cmd = ?command); 195 | 196 | let child = command.spawn(); 197 | 198 | let mut child = match child { 199 | Ok(x) => x, 200 | Err(_) => { 201 | let repo = match self { 202 | Self::Fzf => "https://github.com/junegunn/fzf", 203 | Self::Skim => "https://github.com/lotabout/skim", 204 | }; 205 | eprintln!( 206 | "navi was unable to call {cmd}. 207 | Please make sure it's correctly installed. 208 | Refer to {repo} for more info.", 209 | cmd = &finder_str, 210 | repo = repo 211 | ); 212 | process::exit(33) 213 | } 214 | }; 215 | 216 | let stdin = child 217 | .stdin 218 | .as_mut() 219 | .ok_or_else(|| anyhow!("Unable to acquire stdin of finder"))?; 220 | 221 | let mut writer: Box<&mut dyn Write> = Box::new(stdin); 222 | 223 | let return_value = stdin_fn(&mut writer).context("Failed to pass data to finder")?; 224 | 225 | let out = child.wait_with_output().context("Failed to wait for finder")?; 226 | 227 | let output = parse(out, finder_opts).context("Unable to get output")?; 228 | Ok((output, return_value)) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/finder/post.rs: -------------------------------------------------------------------------------- 1 | use crate::common::shell; 2 | use crate::finder::structures::SuggestionType; 3 | use crate::prelude::*; 4 | use shell::EOF; 5 | use std::process::Stdio; 6 | 7 | fn apply_map(text: String, map_fn: Option) -> Result { 8 | if let Some(m) = map_fn { 9 | let cmd = if CONFIG.shell().contains("fish") { 10 | format!(r#"printf "%s" "{text}" | {m}"#) 11 | } else { 12 | format!( 13 | r#"_navi_input() {{ 14 | cat <<'{EOF}' 15 | {text} 16 | {EOF} 17 | }} 18 | 19 | _navi_map_fn() {{ 20 | {m} 21 | }} 22 | 23 | _navi_nonewline() {{ 24 | printf "%s" "$(cat)" 25 | }} 26 | 27 | _navi_input | _navi_map_fn | _navi_nonewline"# 28 | ) 29 | }; 30 | 31 | let output = shell::out() 32 | .arg(cmd.as_str()) 33 | .stderr(Stdio::inherit()) 34 | .output() 35 | .context("Failed to execute map function")?; 36 | 37 | String::from_utf8(output.stdout).context("Invalid utf8 output for map function") 38 | } else { 39 | Ok(text) 40 | } 41 | } 42 | 43 | fn get_column(text: String, column: Option, delimiter: Option<&str>) -> String { 44 | if let Some(c) = column { 45 | let mut result = String::from(""); 46 | let re = regex::Regex::new(delimiter.unwrap_or(r"\s\s+")).expect("Invalid regex"); 47 | for line in text.split('\n') { 48 | if (line).is_empty() { 49 | continue; 50 | } 51 | let mut parts = re.split(line).skip((c - 1) as usize); 52 | if !result.is_empty() { 53 | result.push('\n'); 54 | } 55 | result.push_str(parts.next().unwrap_or("")); 56 | } 57 | result 58 | } else { 59 | text 60 | } 61 | } 62 | 63 | pub fn process( 64 | text: String, 65 | column: Option, 66 | delimiter: Option<&str>, 67 | map_fn: Option, 68 | ) -> Result { 69 | apply_map(get_column(text, column, delimiter), map_fn) 70 | } 71 | 72 | pub(super) fn parse_output_single(mut text: String, suggestion_type: SuggestionType) -> Result { 73 | Ok(match suggestion_type { 74 | SuggestionType::SingleSelection => text 75 | .lines() 76 | .next() 77 | .context("No sufficient data for single selection")? 78 | .to_string(), 79 | SuggestionType::MultipleSelections | SuggestionType::Disabled | SuggestionType::SnippetSelection => { 80 | let len = text.len(); 81 | if len > 1 { 82 | text.truncate(len - 1); 83 | } 84 | text 85 | } 86 | SuggestionType::SingleRecommendation => { 87 | let lines: Vec<&str> = text.lines().collect(); 88 | 89 | match (lines.first(), lines.get(1), lines.get(2)) { 90 | (Some(one), Some(termination), Some(two)) 91 | if *termination == "enter" || termination.is_empty() => 92 | { 93 | if two.is_empty() { 94 | (*one).to_string() 95 | } else { 96 | (*two).to_string() 97 | } 98 | } 99 | (Some(one), Some(termination), None) if *termination == "enter" || termination.is_empty() => { 100 | (*one).to_string() 101 | } 102 | (Some(one), Some(termination), _) if *termination == "tab" => (*one).to_string(), 103 | _ => "".to_string(), 104 | } 105 | } 106 | }) 107 | } 108 | 109 | #[cfg(test)] 110 | mod tests { 111 | use super::*; 112 | 113 | #[test] 114 | fn test_parse_output1() { 115 | let text = "palo\n".to_string(); 116 | let output = parse_output_single(text, SuggestionType::SingleSelection).unwrap(); 117 | assert_eq!(output, "palo"); 118 | } 119 | 120 | #[test] 121 | fn test_parse_output2() { 122 | let text = "\nenter\npalo".to_string(); 123 | let output = parse_output_single(text, SuggestionType::SingleRecommendation).unwrap(); 124 | assert_eq!(output, "palo"); 125 | } 126 | 127 | #[test] 128 | fn test_parse_recommendation_output_1() { 129 | let text = "\nenter\npalo".to_string(); 130 | let output = parse_output_single(text, SuggestionType::SingleRecommendation).unwrap(); 131 | assert_eq!(output, "palo"); 132 | } 133 | 134 | #[test] 135 | fn test_parse_recommendation_output_2() { 136 | let text = "p\nenter\npalo".to_string(); 137 | let output = parse_output_single(text, SuggestionType::SingleRecommendation).unwrap(); 138 | assert_eq!(output, "palo"); 139 | } 140 | 141 | #[test] 142 | fn test_parse_recommendation_output_3() { 143 | let text = "peter\nenter\n".to_string(); 144 | let output = parse_output_single(text, SuggestionType::SingleRecommendation).unwrap(); 145 | assert_eq!(output, "peter"); 146 | } 147 | 148 | #[test] 149 | fn test_parse_output3() { 150 | let text = "p\ntab\npalo".to_string(); 151 | let output = parse_output_single(text, SuggestionType::SingleRecommendation).unwrap(); 152 | assert_eq!(output, "p"); 153 | } 154 | 155 | #[test] 156 | fn test_parse_snippet_request() { 157 | let text = "enter\nssh ⠀login to a server and forward to ssh key (d… ⠀ssh -A @ ⠀ssh ⠀login to a server and forward to ssh key (dangerous but useful for bastion hosts) ⠀ssh -A @ ⠀\n".to_string(); 158 | let output = parse_output_single(text, SuggestionType::SnippetSelection).unwrap(); 159 | assert_eq!(output, "enter\nssh ⠀login to a server and forward to ssh key (d… ⠀ssh -A @ ⠀ssh ⠀login to a server and forward to ssh key (dangerous but useful for bastion hosts) ⠀ssh -A @ ⠀"); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/finder/structures.rs: -------------------------------------------------------------------------------- 1 | use crate::filesystem; 2 | use crate::prelude::*; 3 | 4 | #[derive(Debug, PartialEq, Clone)] 5 | pub struct Opts { 6 | pub query: Option, 7 | pub filter: Option, 8 | pub prompt: Option, 9 | pub preview: Option, 10 | pub preview_window: Option, 11 | pub overrides: Option, 12 | pub header_lines: u8, 13 | pub header: Option, 14 | pub suggestion_type: SuggestionType, 15 | pub delimiter: Option, 16 | pub column: Option, 17 | pub map: Option, 18 | pub prevent_select1: bool, 19 | pub show_all_columns: bool, 20 | } 21 | 22 | impl Default for Opts { 23 | fn default() -> Self { 24 | Self { 25 | query: None, 26 | filter: None, 27 | preview: None, 28 | preview_window: None, 29 | overrides: None, 30 | header_lines: 0, 31 | header: None, 32 | prompt: None, 33 | suggestion_type: SuggestionType::SingleSelection, 34 | column: None, 35 | delimiter: None, 36 | map: None, 37 | prevent_select1: true, 38 | show_all_columns: false, 39 | } 40 | } 41 | } 42 | 43 | impl Opts { 44 | pub fn snippet_default() -> Self { 45 | Self { 46 | suggestion_type: SuggestionType::SnippetSelection, 47 | overrides: CONFIG.fzf_overrides(), 48 | preview: Some(format!("{} preview {{}}", filesystem::exe_string())), 49 | prevent_select1: !CONFIG.best_match(), 50 | query: if CONFIG.best_match() { 51 | None 52 | } else { 53 | CONFIG.get_query() 54 | }, 55 | filter: if CONFIG.best_match() { 56 | CONFIG.get_query() 57 | } else { 58 | None 59 | }, 60 | ..Default::default() 61 | } 62 | } 63 | 64 | pub fn var_default() -> Self { 65 | Self { 66 | overrides: CONFIG.fzf_overrides_var(), 67 | suggestion_type: SuggestionType::SingleRecommendation, 68 | prevent_select1: false, 69 | delimiter: CONFIG.delimiter_var(), 70 | ..Default::default() 71 | } 72 | } 73 | } 74 | 75 | #[derive(Clone, Copy, Debug, PartialEq)] 76 | pub enum SuggestionType { 77 | /// finder will not print any suggestions 78 | Disabled, 79 | /// finder will only select one of the suggestions 80 | SingleSelection, 81 | /// finder will select multiple suggestions 82 | MultipleSelections, 83 | /// finder will select one of the suggestions or use the query 84 | SingleRecommendation, 85 | /// initial snippet selection 86 | SnippetSelection, 87 | } 88 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate lazy_static; 3 | 4 | mod clients; 5 | mod commands; 6 | mod common; 7 | mod config; 8 | mod deser; 9 | mod env_var; 10 | mod filesystem; 11 | mod finder; 12 | mod parser; 13 | pub mod prelude; 14 | mod structures; 15 | mod welcome; 16 | 17 | mod libs { 18 | pub mod dns_common; 19 | } 20 | 21 | pub use {commands::handle, filesystem::default_config_pathbuf}; 22 | -------------------------------------------------------------------------------- /src/libs/dns_common/component.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | pub trait Component: Any + AsAny + Send + Sync {} 4 | 5 | pub trait AsAny: Any { 6 | fn as_any(&self) -> &dyn Any; 7 | fn as_mut_any(&mut self) -> &mut dyn Any; 8 | } 9 | -------------------------------------------------------------------------------- /src/libs/dns_common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod component; 2 | mod tracing; 3 | -------------------------------------------------------------------------------- /src/libs/dns_common/tracing.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | #[derive(Deserialize, Serialize, Debug, Clone)] 4 | #[serde(deny_unknown_fields)] 5 | pub struct TracingConfig { 6 | pub time: bool, 7 | pub level: String, 8 | } 9 | -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use crate::common::deps::HasDeps; 2 | pub use crate::common::fs::ToStringExt; 3 | pub use crate::config::CONFIG; // TODO 4 | pub use crate::libs::dns_common; 5 | pub use anyhow::{anyhow, Context, Error, Result}; 6 | pub use regex::Regex; 7 | pub use serde::de::Deserializer; 8 | pub use serde::ser::Serializer; 9 | pub use serde::{Deserialize, Serialize}; 10 | pub use std::any::{Any, TypeId}; 11 | pub use std::collections::{HashMap, HashSet}; 12 | pub use std::convert::{TryFrom, TryInto}; 13 | pub use std::fs::File; 14 | pub use std::io::{BufRead, BufReader}; 15 | pub use std::path::{Path, PathBuf}; 16 | pub use std::process::Stdio; 17 | pub use std::str::FromStr; 18 | pub use std::sync::{Arc, Mutex, RwLock}; 19 | pub use tracing::{self, debug, error, event, info, instrument, span, subscriber, trace, warn}; 20 | 21 | pub trait Component: Any + AsAny + Send + Sync {} 22 | 23 | pub trait AsAny: Any { 24 | fn as_any(&self) -> &dyn Any; 25 | fn as_mut_any(&mut self) -> &mut dyn Any; 26 | } 27 | 28 | impl AsAny for T 29 | where 30 | T: Any, 31 | { 32 | fn as_any(&self) -> &dyn Any { 33 | self 34 | } 35 | 36 | fn as_mut_any(&mut self) -> &mut dyn Any { 37 | self 38 | } 39 | } 40 | 41 | pub trait Runnable { 42 | fn run(&self) -> Result<()>; 43 | } 44 | -------------------------------------------------------------------------------- /src/structures/cheat.rs: -------------------------------------------------------------------------------- 1 | use crate::common::hash::fnv; 2 | use crate::finder::structures::Opts; 3 | use crate::prelude::*; 4 | 5 | pub type Suggestion = (String, Option); 6 | 7 | #[derive(Clone, Default)] 8 | pub struct VariableMap { 9 | variables: HashMap>, 10 | dependencies: HashMap>, 11 | } 12 | 13 | impl VariableMap { 14 | pub fn insert_dependency(&mut self, tags: &str, tags_dependency: &str) { 15 | let k = fnv(&tags); 16 | if let Some(v) = self.dependencies.get_mut(&k) { 17 | v.push(fnv(&tags_dependency)); 18 | } else { 19 | let v: Vec = vec![fnv(&tags_dependency)]; 20 | self.dependencies.insert(k, v); 21 | } 22 | } 23 | 24 | pub fn insert_suggestion(&mut self, tags: &str, variable: &str, value: Suggestion) { 25 | let k1 = fnv(&tags); 26 | let k2 = String::from(variable); 27 | if let Some(m) = self.variables.get_mut(&k1) { 28 | m.insert(k2, value); 29 | } else { 30 | let mut m = HashMap::new(); 31 | m.insert(k2, value); 32 | self.variables.insert(k1, m); 33 | } 34 | } 35 | 36 | pub fn get_suggestion(&self, tags: &str, variable: &str) -> Option<&Suggestion> { 37 | let k = fnv(&tags); 38 | 39 | if let Some(vm) = self.variables.get(&k) { 40 | let res = vm.get(variable); 41 | if res.is_some() { 42 | return res; 43 | } 44 | } 45 | 46 | if let Some(dependency_keys) = self.dependencies.get(&k) { 47 | for dependency_key in dependency_keys { 48 | if let Some(vm) = self.variables.get(dependency_key) { 49 | let res = vm.get(variable); 50 | if res.is_some() { 51 | return res; 52 | } 53 | } 54 | } 55 | } 56 | 57 | None 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/structures/fetcher.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::Parser; 2 | use crate::prelude::*; 3 | 4 | pub trait Fetcher { 5 | fn fetch(&self, parser: &mut Parser) -> Result; 6 | 7 | fn files(&self) -> Vec { 8 | vec![] 9 | } 10 | } 11 | 12 | pub struct StaticFetcher { 13 | lines: Vec, 14 | } 15 | 16 | impl StaticFetcher { 17 | pub fn new(lines: Vec) -> Self { 18 | Self { lines } 19 | } 20 | } 21 | 22 | impl Fetcher for StaticFetcher { 23 | fn fetch(&self, parser: &mut Parser) -> Result { 24 | parser.read_lines(self.lines.clone().into_iter().map(Ok), "static", None)?; 25 | Ok(true) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/structures/item.rs: -------------------------------------------------------------------------------- 1 | use crate::common::hash::fnv; 2 | 3 | #[derive(Default, Debug)] 4 | pub struct Item { 5 | pub tags: String, 6 | pub comment: String, 7 | pub snippet: String, 8 | pub file_index: Option, 9 | pub icon: Option, 10 | } 11 | 12 | impl Item { 13 | pub fn new(file_index: Option) -> Self { 14 | Self { 15 | file_index, 16 | ..Default::default() 17 | } 18 | } 19 | 20 | pub fn hash(&self) -> u64 { 21 | fnv(&format!( 22 | "{}{}{}", 23 | &self.tags.trim(), 24 | &self.comment.trim(), 25 | &self.snippet.trim() 26 | )) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/structures/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cheat; 2 | pub mod fetcher; 3 | pub mod item; 4 | -------------------------------------------------------------------------------- /src/welcome.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::Parser; 2 | use crate::prelude::*; 3 | use crate::structures::fetcher; 4 | 5 | pub fn populate_cheatsheet(parser: &mut Parser) -> Result<()> { 6 | let cheatsheet = include_str!("../docs/examples/cheatsheet/navi.cheat"); 7 | let lines = cheatsheet.split('\n').map(|s| Ok(s.to_string())); 8 | 9 | parser.read_lines(lines, "welcome", None)?; 10 | 11 | Ok(()) 12 | } 13 | 14 | pub struct Fetcher {} 15 | 16 | impl Fetcher { 17 | pub fn new() -> Self { 18 | Self {} 19 | } 20 | } 21 | 22 | impl fetcher::Fetcher for Fetcher { 23 | fn fetch(&self, parser: &mut Parser) -> Result { 24 | populate_cheatsheet(parser)?; 25 | Ok(true) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/cheats/more_cases.cheat: -------------------------------------------------------------------------------- 1 | ; author: CI/CD 2 | 3 | % test, ci/cd 4 | 5 | # escape code + subshell 6 | echo -ne "\033]0;$(hostname)\007" 7 | 8 | # env var 9 | echo "$HOME" 10 | 11 | # multi + column 12 | myfn() { 13 | for i in $@; do 14 | echo -e "arg: $i\n" 15 | done 16 | } 17 | folders=($(echo "")) 18 | myfn "${folders[@]}" 19 | 20 | # second column: default delimiter 21 | echo " is cool" 22 | 23 | # second column: custom delimiter 24 | echo " is cool" 25 | 26 | # return multiple results: single words 27 | echo "I like these languages: "$(printf '%s' "" | tr '\n' ',' | sed 's/,/, /g')"" 28 | 29 | # return multiple results: multiple words 30 | echo "I like these examples: "$(printf '%s' "" | sed 's/^..*$/"&"/' | awk 1 ORS=', ' | sed 's/, $//')"" 31 | 32 | # multiple replacements -> "foo" 33 | echo " " 34 | 35 | # with preview 36 | cat "" 37 | 38 | # with map 39 | echo "" 40 | 41 | # empty 42 | echo "http://google.com?q=" 43 | 44 | # fzf 45 | ls / | fzf 46 | 47 | # 48 | echo description space 49 | 50 | # 51 | echo description blank 52 | 53 | # x 54 | echo description one character 55 | 56 | # map can be used to expand into multiple arguments 57 | for l in ; do echo "line: $l"; done 58 | 59 | # x 60 | echo 61 | 62 | # Concatenate pdf files 63 | files=($(echo "")) 64 | echo pdftk "${files[@]:-}" cat output 65 | 66 | $ files: echo 'file1.pdf file2.pdf file3.pdf' | tr ' ' '\n' --- --multi --fzf-overrides '--tac' 67 | $ x: echo '1 2 3' | tr ' ' '\n' 68 | $ y: echo 'a b c' | tr ' ' '\n' 69 | $ z: echo 'foo bar' | tr ' ' '\n' 70 | $ table_elem: echo -e '0 rust rust-lang.org\n1 clojure clojure.org' --- --column 2 71 | $ table_elem2: echo -e '0;rust;rust-lang.org\n1;clojure;clojure.org' --- --column 2 --delimiter ';' 72 | $ multi_col: ls -la | awk '{print $1, $9}' --- --column 2 --delimiter '\s' --multi 73 | $ langs: echo 'clojure rust javascript' | tr ' ' '\n' --- --multi 74 | $ mapped: echo 'true false' | tr ' ' '\n' --- --map "grep -q t && echo 1 || echo 0" 75 | $ examples: echo -e 'foo bar\nlorem ipsum\ndolor sit' --- --multi 76 | $ multiword: echo -e 'foo bar\nlorem ipsum\ndolor sit\nbaz'i 77 | $ file: ls . --- --preview 'cat {}' --preview-window 'right:50%' 78 | $ phrases: echo -e "foo bar\nlorem ipsum\ndolor sit" --- --multi --map "navi fn map::expand" 79 | $ with_overrides: echo -e "foo bar\nlorem ipsum\ndolor sit" --- --fzf-overrides "--margin=15% --bind=ctrl-u:replace-query" 80 | 81 | # this should be displayed 82 | echo hi 83 | 84 | % 85 | 86 | # Without tag 87 | echo hi 1 2 3 -------------------------------------------------------------------------------- /tests/cheats/ssh.cheat: -------------------------------------------------------------------------------- 1 | % ssh 2 | 3 | # login to a server with a key and port 4 | ssh -i -p @ 5 | 6 | $ user : echo -e "$(whoami)\nroot" --- --prevent-extra 7 | -------------------------------------------------------------------------------- /tests/config.yaml: -------------------------------------------------------------------------------- 1 | style: 2 | tag: 3 | color: cyan 4 | width_percentage: 26 5 | min_width: 20 6 | comment: 7 | color: yellow 8 | width_percentage: 42 9 | min_width: 45 10 | snippet: 11 | color: white 12 | 13 | finder: 14 | command: fzf 15 | 16 | shell: 17 | finder_command: bash 18 | command: env BASH_ENV="${NAVI_HOME}/tests/helpers.sh" bash --norc --noprofile 19 | -------------------------------------------------------------------------------- /tests/core.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # vim: filetype=sh 3 | 4 | source "${NAVI_HOME}/scripts/install" 5 | 6 | NEWLINE_CHAR="\036" 7 | 8 | PASSED=0 9 | FAILED=0 10 | SKIPPED=0 11 | SUITE="" 12 | 13 | test::set_suite() { 14 | SUITE="$*" 15 | } 16 | 17 | test::success() { 18 | PASSED=$((PASSED+1)) 19 | log::success "Test passed!" 20 | } 21 | 22 | test::fail() { 23 | FAILED=$((FAILED+1)) 24 | log::error "Test failed..." 25 | return 26 | } 27 | 28 | test::skip() { 29 | echo 30 | log::note "${SUITE:-unknown} - ${1:-unknown}" 31 | SKIPPED=$((SKIPPED+1)) 32 | log::warning "Test skipped..." 33 | return 34 | } 35 | 36 | test::run() { 37 | echo 38 | log::note "${SUITE:-unknown} - ${1:-unknown}" 39 | shift 40 | "$@" && test::success || test::fail 41 | } 42 | 43 | test::_escape() { 44 | tr '\n' "$NEWLINE_CHAR" | sed -E "s/[\s$(printf "$NEWLINE_CHAR") ]+$//g" 45 | } 46 | 47 | test::equals() { 48 | local -r actual="$(cat)" 49 | local -r expected="${1:-}" 50 | 51 | local -r actual2="$(echo "$actual" | test::_escape)" 52 | local -r expected2="$(echo "$expected" | test::_escape)" 53 | 54 | if [[ "$actual2" != "$expected2" ]]; then 55 | log::error "Expected '${expected}' but got '${actual}'" 56 | return 2 57 | fi 58 | } 59 | 60 | test::contains() { 61 | local -r haystack="$(cat)" 62 | local -r needle="${1:-}" 63 | 64 | local -r haystack2="$(echo "$haystack" | test::_escape)" 65 | local -r needle2="$(echo "$needle" | test::_escape)" 66 | 67 | if [[ "$haystack2" != *"$needle2"* ]]; then 68 | log::error "Expected '${haystack}' to include '${needle2}'" 69 | return 2 70 | fi 71 | } 72 | 73 | test::finish() { 74 | echo 75 | if [ $SKIPPED -gt 0 ]; then 76 | log::warning "${SKIPPED} tests skipped!" 77 | fi 78 | if [ $FAILED -gt 0 ]; then 79 | log::error "${PASSED} tests passed but ${FAILED} failed... :(" 80 | exit "${FAILED}" 81 | else 82 | log::success "All ${PASSED} tests passed! :)" 83 | exit 0 84 | fi 85 | } 86 | -------------------------------------------------------------------------------- /tests/helpers.sh: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/env bash 2 | 3 | myhelperfn() { 4 | echo "inside helper: $*" 5 | } 6 | -------------------------------------------------------------------------------- /tests/no_prompt_cheats/cases.cheat: -------------------------------------------------------------------------------- 1 | ; author: CI/CD 2 | 3 | % test, first 4 | 5 | # trivial case -> "foo" 6 | echo "foo" 7 | 8 | # map with underscores -> "_foo_" 9 | echo "" 10 | 11 | # expand -> "foo" 12 | echo "" 13 | 14 | # duplicated lines -> "foo\nlorem ipsum\nlorem ipsum\nbaz" 15 | echo foo 16 | echo lorem ipsum 17 | echo lorem ipsum 18 | echo baz 19 | 20 | # empty line -> "foo\n\n\nbar" 21 | echo "$(cat < "172.17.0.2" 30 | echo "8.8.8.8 via 172.17.0.1 dev eth0 src 172.17.0.2" | sed -E 's/.*src ([0-9.]+).*/\1/p' | head -n1 31 | 32 | # 2nd column with default delimiter -> "rust is cool" 33 | echo " is cool" 34 | 35 | # 2nd column with custom delimiter -> "clojure is cool" 36 | echo " is cool" 37 | 38 | # multiple words -> "lorem foo bar ipsum" 39 | echo "lorem ipsum" 40 | 41 | # variable dependency, full -> "2 12 a 2" 42 | echo " " 43 | 44 | ; # variable dependency, we can ignore intermediate values -> "foo 12" 45 | ; printf "foo "; : ; echo "" 46 | 47 | # nested unused value -> "path: /my/pictures" 48 | echo "path: " 49 | 50 | # multiline command: no backslash -> "foo\nbar" 51 | echo "foo" 52 | echo "bar" 53 | 54 | # multiline command: with backslash -> "lorem ipsum\nno match" 55 | echo 'lorem ipsum' 56 | echo "foo" \ 57 | | grep -q "bar" \ 58 | && echo "match" \ 59 | || echo "no match" 60 | 61 | # multiline variable -> "foo bar" 62 | echo "" 63 | 64 | # helper -> "inside helper: 42" 65 | myhelperfn 42 66 | 67 | $ x: echo '2' 68 | $ x2: echo "$((x+10))" 69 | $ y: echo 'a' 70 | $ language: echo '0 rust rust-lang.org' --- --column 2 71 | $ language2: echo '1;clojure;clojure.org' --- --column 2 --delimiter ';' 72 | $ multiword: echo 'foo bar' 73 | $ pictures_folder: echo "/my/pictures" 74 | $ map1: echo "foo" --- --map 'echo _$(cat)_' 75 | $ multilinevar: echo "xoo yar" \ 76 | | tr 'x' 'f' \ 77 | | tr 'y' 'b' 78 | $ expand1: echo "foo" --- --expand 79 | 80 | 81 | # this should be displayed -> "hi" 82 | echo hi 83 | 84 | 85 | % test, second 86 | 87 | @ test, first 88 | @ test, third 89 | 90 | # nested used value -> "path: /my/pictures/wallpapers" 91 | echo "path: " 92 | 93 | # same command as before -> "12" 94 | : ; echo "" 95 | 96 | # the order isn't relevant -> "br" 97 | echo "" 98 | 99 | $ wallpaper_folder: echo "/wallpapers" 100 | 101 | 102 | % test, third 103 | 104 | ; this cheathsheet doesn't have any commands 105 | $ country: echo "br" -------------------------------------------------------------------------------- /tests/no_prompt_cheats/one.cheat: -------------------------------------------------------------------------------- 1 | ; author: CI/CD 2 | 3 | % test, first 4 | 5 | # trivial case -> "foo" 6 | echo "foo" 7 | 8 | -------------------------------------------------------------------------------- /tests/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)" 5 | source "${NAVI_HOME}/tests/core.bash" 6 | 7 | export TEST_CHEAT_PATH="${NAVI_HOME}/tests/no_prompt_cheats" 8 | export NAVI_EXE="${NAVI_HOME}/target/debug/navi" 9 | 10 | if ! command_exists navi; then 11 | navi() { 12 | "$NAVI_EXE" "$@" 13 | } 14 | export -f navi 15 | fi 16 | 17 | _navi() { 18 | stty sane || true 19 | local path="${NAVI_TEST_PATH:-$TEST_CHEAT_PATH}" 20 | path="${path//$HOME/~}" 21 | export NAVI_ENV_VAR_PATH="$path" 22 | RUST_BACKTRACE=1 NAVI_PATH='$NAVI_ENV_VAR_PATH' NAVI_CONFIG="${NAVI_HOME}/tests/config.yaml" "$NAVI_EXE" "$@" 23 | } 24 | 25 | _navi_cases() { 26 | local -r filter="${1::-2}" 27 | _navi --query "$filter" --best-match 28 | } 29 | 30 | _navi_cases_test() { 31 | _navi_cases "$1" \ 32 | | test::equals "$2" 33 | } 34 | 35 | _get_all_tests() { 36 | cat "${TEST_CHEAT_PATH}/cases.cheat" \ 37 | | grep '^#' \ 38 | | grep ' ->' \ 39 | | sed 's/\\n/'"$(printf "$NEWLINE_CHAR")"'/g' \ 40 | | sed -E 's/# (.*) -> "(.*)"/\1|\2/g' 41 | } 42 | 43 | _get_tests() { 44 | local -r filter="$1" 45 | 46 | if [ -n "$filter" ]; then 47 | _get_all_tests \ 48 | | grep "$filter" 49 | else 50 | _get_all_tests 51 | fi 52 | } 53 | 54 | _navi_tldr() { 55 | _navi --tldr docker --query ps --print --best-match \ 56 | | test::contains "docker ps" 57 | } 58 | 59 | _navi_cheatsh() { 60 | _navi --cheatsh docker --query remove --print --best-match \ 61 | | test::contains "docker container prune" 62 | } 63 | 64 | _navi_widget() { 65 | local -r out="$(_navi widget "$1")" 66 | if ! echo "$out" | grep -q "navi "; then 67 | echo "$out" 68 | return 1 69 | fi 70 | } 71 | 72 | _navi_cheatspath() { 73 | _navi info cheats-path \ 74 | | grep -q "/cheats" 75 | } 76 | 77 | _kill_tmux() { 78 | pkill -f tmux 2>/dev/null || true 79 | } 80 | 81 | _assert_tmux() { 82 | local -r log_file="$1" 83 | local -r sessions="$(tmux list-sessions)" 84 | if [ -z "$sessions" ]; then 85 | _kill_tmux 86 | cat "$log_file" 87 | return 1 88 | fi 89 | } 90 | 91 | _integration() { 92 | _kill_tmux 93 | local -r log_file="${NAVI_HOME}/target/ci.log" 94 | local -r cheats_path="$($NAVI_EXE info cheats-path)" 95 | rm -rf "$cheats_path" 2>/dev/null || true 96 | mkdir -p "$cheats_path" 2>/dev/null || true 97 | local -r bak_cheats_path="$(mktemp -d "${cheats_path}_XXXXX")" 98 | rm "$log_file" 2>/dev/null || true 99 | mv "$cheats_path" "$bak_cheats_path" 2>/dev/null || true 100 | 101 | log::note "Starting sessions..." 102 | tmux new-session -d -s ci "export NAVI_TEST_PATH='${cheats_path}'; ${NAVI_HOME}/tests/run _navi |& tee '${log_file}'" 103 | sleep 5 104 | _assert_tmux "$log_file" 105 | 106 | log::note "Downloading default cheatsheets..." 107 | tmux send-key -t ci "download default"; tmux send-key -t ci "Enter" 108 | sleep 1 109 | _assert_tmux "$log_file" 110 | 111 | log::note "Confirming import..." 112 | tmux send-key -t ci "y" 113 | sleep 1 114 | tmux send-key -t ci "Enter" 115 | sleep 6 116 | _assert_tmux "$log_file" 117 | 118 | log::note "Running snippet..." 119 | tmux send-key -t ci "pwd" 120 | sleep 1 121 | tmux send-key -t ci "Enter" 122 | 123 | log::note "Checking paths..." 124 | sleep 2 125 | local -r downloaded_path="$(cat "$log_file" | grep 'They are now located at' | sed 's/They are now located at //')" 126 | ls "$downloaded_path" | grep -q '^pkg_mgr__brew.cheat$' 127 | } 128 | 129 | if ! command_exists fzf; then 130 | export PATH="$PATH:$HOME/.fzf/bin" 131 | fi 132 | 133 | cd "$NAVI_HOME" 134 | 135 | filter="${1:-}" 136 | 137 | # TODO: remove this 138 | if [[ $filter == "_navi" ]]; then 139 | shift 140 | _navi "$@" 141 | exit 0 142 | fi 143 | 144 | test::set_suite "cases" 145 | ifs="$IFS" 146 | IFS=$'\n' 147 | for i in $(_get_tests "$filter"); do 148 | IFS="$ifs" 149 | query="$(echo "$i" | cut -d'|' -f1)" 150 | expected="$(echo "$i" | tr "$NEWLINE_CHAR" '\n' | cut -d'|' -f2)" 151 | test::run "$query" _navi_cases_test "$query" "$expected" 152 | done 153 | 154 | test::set_suite "info" 155 | test::run "cheats_path" _navi_cheatspath 156 | 157 | test::set_suite "widget" 158 | test::run "bash" _navi_widget "bash" 159 | test::run "zsh" _navi_widget "zsh" 160 | test::run "fish" _navi_widget "fish" 161 | test::run "elvish" _navi_widget "elvish" 162 | test::run "nu" _navi_widget "nushell" 163 | 164 | test::set_suite "3rd party" 165 | test::run "tldr" _navi_tldr 166 | test::run "cheatsh" _navi_cheatsh 167 | 168 | test::set_suite "integration" 169 | test::run "welcome->pwd" _integration 170 | 171 | test::finish 172 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | #[test] 4 | fn it_works() { 5 | //let _x = navi::handle_config(navi::config_from_iter( 6 | //"navi best trivial".split(' ').collect(), 7 | //)); 8 | // assert_eq!(x, 3); 9 | } 10 | } 11 | --------------------------------------------------------------------------------