Repository: denisidoro/navi Branch: master Commit: b4ce885253df Files: 109 Total size: 208.8 KB Directory structure: gitextract_8cygam37/ ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── config.yml │ └── workflows/ │ ├── cd.yml │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODEOWNERS ================================================ # See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners * @denisidoro .github/* @denisidoro shell/navi.plugin.ps1 @alexis-opolka docs/* @alexis-opolka docs/**/* @alexis-opolka ================================================ FILE: .github/FUNDING.yml ================================================ github: denisidoro ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: "" labels: bug assignees: "" --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Versions:** - OS: [e.g. macOS, WSL ubuntu, ubuntu] - Shell Version [replace this text with the output of `sh --version`] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: "" labels: new feature assignees: "" --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/config.yml ================================================ # Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome # Comment to be posted to on first time issues newIssueWelcomeComment: > 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! :) # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome # Comment to be posted to on PRs from first time contributors in your repository newPRWelcomeComment: > Thanks for opening this pull request! # Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge # Comment to be posted to on pull requests merged by a first time user firstPRMergeComment: > Congrats on merging your first pull request! ================================================ FILE: .github/workflows/cd.yml ================================================ name: Publish on: push: tags: - "*" release: jobs: binary: name: Publish ${{ matrix.target }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: macos-latest target: x86_64-apple-darwin - os: ubuntu-latest target: x86_64-unknown-linux-musl - os: ubuntu-latest target: x86_64-pc-windows-gnu - os: ubuntu-latest target: armv7-unknown-linux-musleabihf - os: ubuntu-latest target: armv7-linux-androideabi - os: ubuntu-latest target: aarch64-linux-android - os: ubuntu-latest target: aarch64-unknown-linux-gnu - os: macos-latest target: aarch64-apple-darwin steps: ### We're checking out the repository at the triggered ref - uses: actions/checkout@v4 - name: Get the version id: get_version run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - name: Check if release exists id: check_release run: | if gh release view ${{ steps.get_version.outputs.VERSION }} > /dev/null 2>&1; then echo "RELEASE_EXISTS=true" >> $GITHUB_OUTPUT else echo "RELEASE_EXISTS=false" >> $GITHUB_OUTPUT fi env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create release continue-on-error: true if: steps.check_release.outputs.RELEASE_EXISTS == 'false' run: | gh release create ${{ steps.get_version.outputs.VERSION }} \ --title "Release ${{ steps.get_version.outputs.VERSION }}" \ --notes "Release notes for ${{ steps.get_version.outputs.VERSION }}" \ env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build id: build run: scripts/release ${{ matrix.target }} - name: Upload binaries to release run: | cd ./target/${{ matrix.target }}/release/ cp navi.${{ steps.build.outputs.EXTENSION }} navi-${{ steps.get_version.outputs.VERSION }}-${{ matrix.target }}.${{ steps.build.outputs.EXTENSION }} gh release upload ${{ steps.get_version.outputs.VERSION }} navi-${{ steps.get_version.outputs.VERSION }}-${{ matrix.target }}.${{ steps.build.outputs.EXTENSION }} env: GH_TOKEN: ${{ github.token }} ================================================ FILE: .github/workflows/ci.yml ================================================ # Based on https://github.com/actions-rs/meta/blob/master/recipes/quickstart.md # # While our "example" application has the platform-specific code, # for simplicity we are compiling and testing everything on the Ubuntu environment only. # For multi-OS testing see the `cross.yml` workflow. on: push: pull_request: branches: [master] name: CI jobs: # check: # name: Check # runs-on: ubuntu-latest # steps: # - name: Checkout sources # uses: actions/checkout@v2 # - name: Install stable toolchain # uses: actions-rs/toolchain@v1 # with: # profile: minimal # toolchain: stable # override: true # - name: Run cargo check # uses: actions-rs/cargo@v1 # continue-on-error: false # with: # command: check test: name: Tests runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v2 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Prep environment to test compiled-in paths run: | mkdir /tmp/cheats-dir touch /tmp/config-file - name: Run cargo test uses: actions-rs/cargo@v1 continue-on-error: false env: NAVI_PATH: /tmp/cheats-dir NAVI_CONFIG: /tmp/config-file with: command: test - name: Run cargo test uses: actions-rs/cargo@v1 continue-on-error: false with: command: test - name: Install deps run: ./scripts/dot pkg add git bash npm tmux - name: Install fzf run: git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf; yes | ~/.fzf/install; - name: Install tealdeer run: sudo npm install -g tldr - name: Run bash tests run: ./tests/run lints: name: Lints runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v2 # - name: Install stable toolchain # uses: actions-rs/toolchain@v1 # with: # profile: minimal # toolchain: stable # override: true # components: rustfmt, clippy - name: Run cargo fmt uses: actions-rs/cargo@v1 continue-on-error: false with: command: fmt args: --all -- --check - name: Run cargo clippy uses: actions-rs/cargo@v1 continue-on-error: false with: command: clippy args: -- -D warnings ================================================ FILE: .gitignore ================================================ /target **/*.rs.bk navi.log ================================================ FILE: Cargo.toml ================================================ [package] name = "navi" version = "2.25.0-beta1" authors = ["Denis Isidoro ", "Alexis Opolka "] edition = "2021" description = "An interactive cheatsheet tool for the command-line" homepage = "https://github.com/denisidoro/navi" documentation = "https://github.com/denisidoro/navi" repository = "https://github.com/denisidoro/navi" keywords = ["cheatsheets", "terminal", "cli", "tui", "shell"] categories = ["command-line-utilities"] license = "Apache-2.0" [features] disable-command-execution = [] disable-repo-management = [] [badges] travis-ci = { repository = "denisidoro/navi", branch = "master" } [dependencies] regex = { version = "1.7.3", default-features = false, features = [ "std", "unicode-perl", ] } clap = { version = "4.2.1", features = ["derive", "cargo"] } crossterm = "0.28.0" lazy_static = "1.4.0" etcetera = "0.10.0" walkdir = "2.3.3" shellwords = "1.1.0" anyhow = "1.0.70" thiserror = "2.0.0" strip-ansi-escapes = "0.2.0" edit = "0.1.4" remove_dir_all = "1.0.0" serde = { version = "1.0.219", features = ["derive"] } serde_yaml = "0.9.21" unicode-width = "0.2.0" tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } [target.'cfg(windows)'.dependencies] dunce = "1" [lib] name = "navi" path = "src/lib.rs" [[bin]] name = "navi" path = "src/bin/main.rs" bench = false ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ export PATH := /usr/local/opt/bash/bin/:$(PATH) install: scripts/make install uninstall: scripts/make uninstall fix: scripts/make fix test: scripts/test build: cargo build ================================================ FILE: README.md ================================================ # 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) An interactive cheatsheet tool for the command-line. [![Demo](https://asciinema.org/a/406461.svg)](https://asciinema.org/a/406461) **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. ## Pros - it will spare you from knowing CLIs by heart - it will spare you from copy-pasting output from intermediate commands - it will make you type less - it will teach you new one-liners It uses [fzf](https://github.com/junegunn/fzf) or [skim](https://github.com/lotabout/skim) under the hood and it can be either used as a command or as a shell widget (_à la_ Ctrl-R). ## Table of contents - [Installation](#installation) - [Usage](#usage) - [Cheatsheet repositories](#cheatsheet-repositories) - [Cheatsheet syntax](#cheatsheet-syntax) - [Customization](#customization) - [More info](#more-info) ## Installation The recommended way to install **navi** is by running: ```sh brew install navi ``` > [!NOTE] > For more details on how to install Navi, see [docs/installation](docs/installation/README.md) **navi** can be installed with the following package managers: [![Packaging status](https://repology.org/badge/vertical-allrepos/navi.svg)](https://repology.org/project/navi/versions) ## Usage There are multiple ways to use **navi**: - by typing `navi` in the terminal - pros: you have access to all possible subcommands and flags - as a [shell widget](docs/widgets/README.md#installing-the-shell-widget) for the terminal - 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 - as a [Tmux widget](docs/widgets/howto/TMUX.md) - pros: you can use your cheatsheets in any command-line app even in SSH sessions - as [aliases](docs/cheatsheet/syntax/README.md#aliases) - as a [shell scripting tool](docs/usage/shell-scripting/README.md) In particular, check [these instructions](https://github.com/denisidoro/navi/issues/491) if you want to replicate what's shown in the demo above. ## Cheatsheet repositories Running **navi** for the first time will help you download and manage cheatsheets. By default, they are stored at `~/.local/share/navi/cheats/`. You can also: - [browse through featured cheatsheets](docs/usage/commands/repo/README.md#browsing-through-cheatsheet-repositories) - [import cheatsheets from git repositories](docs/cheatsheet/repositories/README.md#importing-cheatsheet-repositories) - [write your own cheatsheets](#cheatsheet-syntax) (and [share them](docs/cheatsheet/repositories/README.md#submitting-cheatsheets), if you want) - [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) - [auto-update repositories](docs/cheatsheet/repositories/README.md#auto-updating-repositories) - auto-export cheatsheets from your [TiddlyWiki](https://tiddlywiki.com/) notes using a [TiddlyWiki plugin](https://bimlas.github.io/tw5-navi-cheatsheet/) ## Cheatsheet syntax Cheatsheets are described in `.cheat` files that look like this: ```sh % git, code # Change branch git checkout $ branch: git branch | awk '{print $NF}' ``` The full syntax and examples can be found [here](docs/cheatsheet/syntax/README.md). ## Customization You can: - [setup your own config file](docs/configuration/README.md) - [set custom paths for your config file and cheat sheets](docs/configuration/README.md#paths-and-environment-variables) - [change colors](docs/configuration/README.md#changing-colors) - [resize columns](docs/configuration/README.md#resizing-columns) - [change how search is performed](docs/configuration/README.md#overriding-fzf-options) ## More info Please run the following command to read more about all possible options: ```sh navi --help ``` In addition, please check the [/docs](docs) folder or the website. ================================================ FILE: docs/README.md ================================================ # 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) ## Table of Contents * [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--) * [Table of Contents](#table-of-contents) * [About](#about) * [Navi Pros](#navi-pros) * [Similar tools](#similar-tools) * [Etymology](#etymology) ## About Navi is an interactive cheatsheet tool for the command-line.\ It allows you to browse through cheatsheets (that you may write yourself or download from maintainers) and execute commands. [![Demo](https://asciinema.org/a/406461.svg)](https://asciinema.org/a/406461) 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). ## Navi Pros - it will spare you from knowing CLIs by heart - it will spare you from copy-pasting output from intermediate commands - it will make you type less - it will teach you new one-liners ## Similar tools 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). They are excellent projects, but **navi** remains unique in the following ways: - it's natural to write cheatsheets tailored to your needs - arguments are neither hardcoded nor a simple template ## Etymology [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. ================================================ FILE: docs/cheatsheet/README.md ================================================ # Navi cheatsheets * [Navi cheatsheets](#navi-cheatsheets) * [Working with `cheatsheet repositories`](#working-with-cheatsheet-repositories) * [Manually adding cheatsheets to navi](#manually-adding-cheatsheets-to-navi) * [Choosing between queries and selection with variables](#choosing-between-queries-and-selection-with-variables) * [Using cheatsheets from other tools](#using-cheatsheets-from-other-tools) ## Working with `cheatsheet repositories` Navi works best with what we call `cheatsheet repositories`, for more details see [cheatsheet/repositories](repositories/README.md). ## Manually adding cheatsheets to navi If you don't want to work with `cheatsheet repositories`, you can manually add your cheatsheets to navi by putting them into the `cheats_path` of your platform. You can find out your path using the [info](/docs/usage/commands/info/README.md) subcommands but a quick working command to go there would be: - Before 2.25.0 ```bash cd $(navi info cheats-path) ``` - After 2.25.0 ```bash cd $(navi info default-cheats-path) ``` ## Choosing between queries and selection with variables Navi lets you use different methods to fill a variable value, when prompted. | Keyboard key | Preference | |:------------------:|:--------------------------:| | tab | The query is preferred | | enter | The selection is preferred | It means if you enter the tab key, navi will let you enter the value. ## Using cheatsheets from other tools > [!WARNING] > Navi **DOESN'T SUPPORT** as of now importing cheatsheets from other tools > but is able to **work with** TLDR and Cheat.sh. ![Demo](https://user-images.githubusercontent.com/3226564/91878474-bae27500-ec55-11ea-8b19-17876178e887.gif) You can use cheatsheets from [tldr](https://github.com/tldr-pages/tldr) by running: ```sh navi --tldr ``` You can use cheatsheets from [cheat.sh](https://github.com/chubin/cheat.sh) by running: ```sh navi --cheatsh ``` ================================================ FILE: docs/cheatsheet/getting-started/README.md ================================================ # Cheatsheets - Getting started ================================================ FILE: docs/cheatsheet/repositories/README.md ================================================ # Cheatsheet repositories * [Cheatsheet repositories](#cheatsheet-repositories) * [About](#about) * [Importing cheatsheet repositories](#importing-cheatsheet-repositories) * [Submitting cheatsheets](#submitting-cheatsheets) * [Auto-updating repositories](#auto-updating-repositories) ## About Navi lets you work with what we call `cheatsheet repositories`, they are git repositories and mainly consists of `.cheat` files. This page is dedicated to the information you might need to work with `cheatsheet repositories`. ## Importing cheatsheet repositories You can import `cheatsheet repositories` with the `repo add` subcommand.\ See [/docs/usage/commands/repo](/docs/usage/commands/repo/README.md#importing-cheatsheet-repositories) for more details. ## Submitting cheatsheets The featured repository for cheatsheets is [denisidoro/cheats](https://github.com/denisidoro/cheats), feel free to open a PR[^1] there for me to include your contributions. 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]. ## Auto-updating repositories Right now, **navi** doesn't have support for auto-updating out of the box. However, you can achieve this by using `git` and `crontab`. - First make sure you cloned your repo using `git` to the correct folder: ```sh user="" repo="" git clone "https://github.com/${user}/${repo}" "$(navi info cheats-path)/${user}__${repo}" ``` - Then, add a cron job: ```sh crontab -e */0 11 * * * bash -c 'cd "$(/usr/local/bin/navi info cheats-path)/__" && /usr/local/bin/git pull -q origin master' ``` > [!NOTE] > Please note the cron job above is just an example **AND** you should edit it accordingly: > >- In this example, the cron job is triggered every day at 11am. > > You might want to check out [crontab guru](https://crontab.guru/) regarding crontab. > >- The full paths to `navi` and `git` may differ in your setup. > > Check their actual values using `which` as `which `. > >- Don't forget to replace `__` with the actual folder name [^1]: A *PR* is short for Pull Request ================================================ FILE: docs/cheatsheet/syntax/README.md ================================================ # The syntax of a Navi cheatsheet * [The syntax of a Navi cheatsheet](#the-syntax-of-a-navi-cheatsheet) * [Syntax overview](#syntax-overview) * [Variables](#variables) * [Advanced variable options](#advanced-variable-options) * [Variable dependency](#variable-dependency) * [Implicit dependencies](#implicit-dependencies) * [Explicit dependencies](#explicit-dependencies) * [Variable as multiple arguments](#variable-as-multiple-arguments) * [Extending cheats](#extending-cheats) * [Multiline commands/snippets](#multiline-commandssnippets) * [Aliases](#aliases) ## Syntax overview Cheats are described in cheatsheet files.\ A cheatsheet is a file that has a `.cheat` or `.cheat.md` extension and looks like this: ```sh % git, code # Change branch git checkout $ branch: git branch | awk '{print $NF}' ``` A cheatsheet can have the following elements: | Element | Syntax | Description | |:--------------------------------:|:------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| | Tags as cheat titles | `%` | Lines starting with this character are considered the start of a new cheat command and should contain tags. | | Cheat Description | `#` | Lines starting with this character should be the description of the cheat you're writing. | | Cheat Comments (or Metacomments) | `;` | Lines starting with this character will be ignored by navi but they can be great as editor's comments. | | 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. | | 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. | | Executable commands | N/A | All other non-empty lines are considered as executable commands. | > [!TIP] > If you are editing cheatsheets in Visual Studio Code, you could enable syntax highlighting > by installing this extension: [@yanivmo/navi-cheatsheet-language](https://marketplace.visualstudio.com/items?itemName=yanivmo.navi-cheatsheet-language). ## Variables Variables are defined with brackets inside executable commands (e.g. ``).\ Variable names should only include alphanumeric characters and `_`. You can show suggestions by using the Pre-defined variable lines (i.e. lines starting with`$`).\ Otherwise, the user will be able to type any value for it. ### Advanced variable options For Pre-Defined variable lines, you can use `---` to customize the behavior of `fzf` or how the value is going to be used. Below are examples of such customization: - We define what column to use, the number of header lines and a delimiter between values. ```sh # This will pick the 3rd column and use the first line as header docker rmi $ image_id: docker images --- --column 3 --header-lines 1 --delimiter '\s\s+' ``` - We modify the output values of a command ```shell # Even though "false/true" is displayed, this will print "0/1" echo $ mapped: echo 'false true' | tr ' ' '\n' --- --map "grep -q t && echo 1 || echo 0" ``` The supported parameters are: | Parameter | Description | |:------------------------|:------------------------------------------------------------------------------------------| | `--column ` | `` is the column number to extract from the result. | | `--map ` | **_[EXPERIMENTAL]_** `` is a map function to apply to the variable value. | | `--prevent-extra` | **_[EXPERIMENTAL]_** This parameter will limit the user to select one of the suggestions. | | `--fzf-overrides ` | **_[EXPERIMENTAL]_** `` is an arbitrary argument to override `fzf` behaviour. | | `--expand` | **_[EXPERIMENTAL]_** This parameter will convert each line into a separate argument. | In addition, it's possible to forward the following parameters to `fzf`: | Parameter forwarded to `fzf` | |:-----------------------------| | `--multi` | | `--header-lines ` | | `--delimiter ` | | `--query ` | | `--filter ` | | `--header ` | | `--preview ` | | `--preview-window ` | ### Variable dependency Pre-Defined variables can refer other pre-defined variables in two different ways, an implicit and explicit way. #### Implicit dependencies An implicit dependency is when you refer another variable with the same syntax used in executable commands (i.e. ``). Below is an example of using implicit dependencies to construct a path: ```sh # Should print /my/pictures/wallpapers echo "" $ pictures_folder: echo "/my/pictures" $ wallpaper_folder: echo "/wallpapers" ``` #### Explicit dependencies An explicit dependency is when you prepend a dollar sign (i.e. `$`) to the variable name. Below is an example of using explicit dependencies to give multiple choices: ```sh # If you select "hello" for , the possible values of will be "hello foo" and "hello bar" echo # If you want to ignore the contents of and only print : ; echo $ x: echo "hello hi" | tr ' ' '\n' $ y: echo "$x foo;$x bar" | tr ';' '\n' ``` ### Variable as multiple arguments Variables can have multiple arguments, below is an example of using multiple arguments to cat multiple files at the same time. ```sh # This will result into: cat "file1.json" "file2.json" cat $ jsons: find . -iname '*.json' -type f -print --- --multi --expand ``` ## Extending cheats Navi allows you to extend a cheat context with `Extended cheats` lines (i.e. starting with `@`).\ If you put the same tags from another cheat, you will be able to share the same context and will be able to use the same variables, for example. ```sh % dirs, common $ pictures_folder: echo "/my/pictures" % wallpapers @ dirs, common # Should print /my/pictures/wallpapers echo "/wallpapers" % screenshots @ dirs, common # Should print /my/pictures/screenshots echo "/screenshots" ``` ## Multiline commands/snippets Commands can be multiline, we call them snippets. - You can write them as follows: ```sh % bash, foo # This will output "foo\nyes" echo foo true \ && echo yes \ || echo no ``` - Or, you can place them inside Markdown code blocks, delimited by triple backticks (```` ``` ````): ````sh % git, code # Change branch ```sh git checkout ``` $ branch: git branch | awk '{print $NF}' ```` ## Aliases **navi** doesn't have support for aliases as first-class citizens at the moment.\ However, it is easy to create aliases using **navi** + a few conventions. > [!CAUTION] > The examples below will only work if you use **navi** as a shell scripting tool. > > See [/docs/usage/shell-scripting](/docs/usage/shell-scripting/README.md) for more details. For example, suppose you decide to end some of your commands with `:: `: ```bash % aliases # This is one command :: el echo lorem ipsum # This is another command :: ef echo foo bar ``` You could add something similar to this in your `.bashrc`-like file: ```bash navialias() { navi --query ":: $1" --best-match } alias el="navialias el" alias ef="navialias ef" ``` If you don't want to use these conventions, you can even add full comments in your aliases: ```bash navibestmatch() { navi --query "$1" --best-match } alias el="navibestmatch 'This is one command'" alias ef="navibestmatch 'This is another command'" ``` ================================================ FILE: docs/configuration/README.md ================================================ # Configuring Navi Navi allows you to configure it with a YAML configuration. * [Configuring Navi](#configuring-navi) * [Paths and Environment Variables](#paths-and-environment-variables) * [The default configuration file path](#the-default-configuration-file-path) * [Cheatsheets paths](#cheatsheets-paths) * [The default cheatsheets path](#the-default-cheatsheets-path) * [Defining the cheatsheets path with the environment variable](#defining-the-cheatsheets-path-with-the-environment-variable) * [Defining the cheatsheets path in the configuration file](#defining-the-cheatsheets-path-in-the-configuration-file) * [[DEPRECATED] - Using the `path` directive](#deprecated---using-the-path-directive) * [Customization](#customization) * [Changing colors](#changing-colors) * [fzf color scheme](#fzf-color-scheme) * [Navi colors](#navi-colors) * [Resizing columns](#resizing-columns) * [Overriding fzf options](#overriding-fzf-options) * [Overriding during cheats selection](#overriding-during-cheats-selection) * [Overriding during values selection](#overriding-during-values-selection) * [Overriding for all cases](#overriding-for-all-cases) * [Defining your own delimiter](#defining-your-own-delimiter) ## Paths and Environment Variables On the technical side, navi uses the `directories-next` crate for rust, which defines platform-specific locations to store the configuration files, the cache and other types of files an application might need. > [!TIP] > For example, this is why cheatsheets are being stored in `~/Library/Application Support/navi` on macOS. > [!NOTE] > Interested on how `directories-next` works?\ > Go see their `crates.io` page: [crates.io/crates/directories-next](https://crates.io/crates/directories-next) ### The default configuration file path During the compilation of navi, the default configuration file path is set by the `$NAVI_CONFIG` environment variable.\ If it is not set, it fallbacks to `~/.config/navi/config.yaml`. You can check your default configuration file path with the info subcommand, see [/docs/usage/commands/info/](/docs/usage/commands/info/README.md#default-configuration-path) for more details. ### Cheatsheets paths Navi checks the paths in the following order until it finds a value: 1. the `$NAVI_PATH` environment variable 2. the configuration file 3. The default value of navi #### The default cheatsheets path By default, navi stores the cheatsheets in the `~/.local/share/navi/cheats/` directory. You can check your default cheatsheets path with the info subcommand, see [/docs/usage/commands/info/](/docs/usage/commands/info/README.md#default-cheatsheets-path) for more details. #### Defining the cheatsheets path with the environment variable The cheatsheets path can be defined using the `$NAVI_PATH` environment variable in a colon-separated list, for example: ```sh export NAVI_PATH='/path/to/a/dir:/path/to/another/dir:/yet/another/dir' ``` #### Defining the cheatsheets path in the configuration file You can define the cheatsheets path in the configuration file with the following syntax: ```yaml cheats: paths: - /path/to/some/dir # on unix-like os - F:\\path\\to\\dir # on Windows ``` ##### [DEPRECATED] - Using the `path` directive Until `2.17.0`, you could define your cheatsheets path with the `path` directive with the following syntax: ```yaml cheats: path: /path/to/some/dir ``` The directive is now deprecated and will be removed in `2.27.0`. ## Customization ### Changing colors #### fzf color scheme You can change the color scheme of `fzf` by overriding fzf options. > [!NOTE] > See [@junegunn/fzf/wiki/Color-schemes](https://github.com/junegunn/fzf/wiki/Color-schemes) and > [#overriding-fzf-options](#overriding-fzf-options) for more details. #### Navi colors You can change the text color for each column of navi in the configuration file with the following syntax: ```yaml style: tag: color: comment: color: snippet: color: ``` Below is an example of what to do if you'd like navi to look like the French flag: - `config.yaml`: ```yaml style: tag: color: blue comment: color: white snippet: color: red ``` - The result: ![navi-custom-colors](https://github.com/user-attachments/assets/d80352c5-d888-43e6-927d-805a8de1a7e2) ### Resizing columns You can change the column width of each column of navi in the configuration file with the following syntax: ```yaml style: tag: width_percentage: min_width: comment: width_percentage: min_width: snippet: width_percentage: min_width: ``` ### Overriding fzf options You can override fzf options for two different cases: - During the cheats selection Navi exposes the `overrides` directive in the configuration file and the `NAVI_FZF_OVERRIDES` environment variable. - During the pre-defined variable values selection Navi exposes the `overrides_var` directive in the configuration file and the `NAVI_FZF_OVERRIDES_VAR` environment variable. For all cases, navi exposes the `FZF_DEFAULT_OPTS` environment variable. #### Overriding during cheats selection If you want to do the override with `--height 3`, you can do it with the following syntax in the configuration file: ```yaml finder: command: fzf overrides: --height 3 ``` But you can also define the environment variable like this: ```bash export NAVI_FZF_OVERRIDES='--height 3' ``` #### Overriding during values selection If you want to do the override with `--height 3`, you can do it with the following syntax in the configuration file: ```yaml finder: command: fzf overrides_var: --height 3 ``` But you can also define the environment variable like this: ```bash export NAVI_FZF_OVERRIDES_VAR='--height 3' ``` #### Overriding for all cases You can define the environment variable like this: ```bash export FZF_DEFAULT_OPTS="--height 3" ``` > [!NOTE] > See [@junegunn/fzf](https://github.com/junegunn/fzf#layout) for more details on `$FZF_DEFAULT_OPTS`. ## Defining your own delimiter Navi allows you to define your own delimiter to parse the selected result for a variable in your cheats.\ It is equivalent to defining `--delimiter` used with `--column`. You can define it as such: ```yaml finder: delimiter_var: ### By default the expression is \s\s+ ``` > [!CAUTION] > Defining the delimiter via the configuration file means that Navi will use this delimiter by default for > every variable using the `--column` instruction. You can override this configuration with the `--delimiter` instruction in the variable definition of your cheat.\ See [/docs/cheatsheet/syntax/](/docs/cheatsheet/syntax/README.md#advanced-variable-options) for more details. ================================================ FILE: docs/contributions/README.md ================================================ # Navi contributors This section is about the ways you can contribute to Navi and its ecosystem. * [Navi contributors](#navi-contributors) * [How to contribute to Navi](#how-to-contribute-to-navi) * [Versioning Scheme](#versioning-scheme) * [Deprecation of features](#deprecation-of-features) ## How to contribute to Navi You have multiple ways to contribute to navi, here are the documented ones: - [Write code for Navi](code/README.md) - [Write documentation for Navi](documentation/README.md) - [Open Bug tickets](bugs/README.md) Please see each section for more details. ## Versioning Scheme | Type | Description | |-------|--------------------------------------------------------------------------------------------------| | Major | Anything which introduces a major breaking change. The bash to rust rewrite was such an example. | | Minor | Almost everything. | | Fix | A fix, just like its name. It should be micro releases with minimal changes. | ## Deprecation of features Once you introduce a feature, you need to have a clear view of when we're going to remove its support within navi. In order to offer stability to the users, we prefer having 10 minor versions between the deprecation notice and the removal of its support. ````txt Version where the feature is being deprecated: 0.10.0 Version where the support is dropped: 0.20.0 ```` > [!NOTE] > This rule is not absolute and each feature deprecation needs to be handled > carefully given its own circumstances, but try to stick as close as possible > to this rule. ================================================ FILE: docs/contributions/bugs/README.md ================================================ # Contribute in opening bug tickets Like any other software, navi has bugs. If you encounter an issue with Navi, we encourage you to open a bug ticket.\ Please see [https://github.com/denisidoro/navi/issues/](https://github.com/denisidoro/navi/issues/) to open an issue. ================================================ FILE: docs/contributions/code/README.md ================================================ # Contribute code to Navi Navi is written in Rust, the widgets may be written in any language given it can be integrated with Navi. If you take the example of the most common widgets for Navi they are written in their shell scripting language because they intend to integrate Navi with the shell in question (Fish, Zsh, NuShell, PowerShell, etc.). We separate Navi into two categories: - `Navi Core` which refers to Navi's code in Rust - `Navi Widgets` which refers to code that intends to integrate Navi with a 3rd-party software ## Contribute to Navi Core If you want to contribute to Navi Core there are certain steps you need to follow for your changes to be accepted. 1. First, open an issue if no opened issues are related to the change you want to contribute. 2. [Optional] Wait to have an opinion from the maintainers, developers or contributors from Navi. > This step is marked as *Optional* as you can open a Merge Request (MR)/Pull Request (PR) > without having to open an issue beforehand, although it is recommended to not do so. We ask you to wait before working on a PR as the way you see a feature and its implementation might not be similar on how a maintainer of Navi sees it. This will save you and the maintainers time. 3. Fork the repository and iterate over your changes. 4. Update Navi documentation If you implement a new feature, you will need to create a new entry in the project's documentation for users to know what has changed. No significant modification in Navi's behaviour should be merged without being documented.\ For more details I recommend you to see [contributions/documentation/](../documentation/README.md). 5. Open a PR on [denisidoro/navi](https://github.com/denisidoro/navi/pulls) and request a review 6. [Optional] Your PR needs revisions and changes before it can be merged It's not rare that your PR will need changes before it can be accepted by the maintainers and then merged into the main branch. 7. Your PR has been merged Congratulations! Your PR has been reviewed and merged, you should be proud of it, and we thank you for your contribution. The next release cycle will package all contributions into a new release and users throughout the world will be able to use your new feature(s). ================================================ FILE: docs/contributions/documentation/README.md ================================================ # Contribute documentation to Navi If you don't want or can't code in Rust, we welcome all contributions, even more so if it's related to documentation. The documentation of Navi is currently made in Markdown. ## Markdown documentation The documentation source files are located in the `docs/` folder and are mainly grouped by features. The current documentation follows a structure where one folder equals one topic. Here is a quick representation of the folder structure this documentation currently follows: ```txt . +-- docs | +-- examples | | +-- | +-- src | | +-- | | | +-- | +-- | | +-- README.md ``` You can see that we have separated the `src` and `examples` folder from the topic with the intent to make it easier to find each type of documentation. > [!NOTE] > It is recommended to not go deeper than 3 levels in the documentation. ================================================ FILE: docs/deprecated/Alfred/README.md ================================================ # Alfred > [!CAUTION] > This feature has been deprecated and support has been dropped since 2.16.0. > > The latest version with support is [2.15.1](https://github.com/denisidoro/navi/releases/tag/v2.15.1). This is _experimental_. If you face any issues, please report [here](https://github.com/denisidoro/navi/issues/348). ![Alfred demo](https://user-images.githubusercontent.com/3226564/80294838-582b1b00-8743-11ea-9eb5-a335d8eed833.gif) ### Instructions - make sure you have [Alfred Powerpack](https://www.alfredapp.com/powerpack/) - make sure **navi** [2.15.1](https://github.com/denisidoro/navi/releases/tag/v2.15.1) is installed - make sure that the `navi` binary is in the `$PATH` determined by `~/.bashrc` - download and install the [latest .alfredworkflow available](https://github.com/denisidoro/navi/releases/tag/v2.15.1) ================================================ FILE: docs/examples/cheatsheet/example.cheat ================================================ % first cheat # print something echo "My name is !" $ name: whoami ================================================ FILE: docs/examples/cheatsheet/navi.cheat ================================================ % cheatsheets # Download default cheatsheets navi repo add denisidoro/cheats # Browse for cheatsheet repos navi repo browse # Edit main local cheatsheets f="$(navi info cheats-path)/main.cheat" [ -f "$f" ] || navi info cheats-example > "$f" ${EDITOR:-nano} "$f" % config # Edit config file f="$(navi info config-path)" [ -f "$f" ] || navi info config-example > "$f" ${EDITOR:-nano} "$f" % 3rd-party # Search using tldr navi --tldr "" # Search using cheatsh navi --cheatsh "" % widget # Load shell widget shell="$(basename $SHELL)"; eval "$(navi widget $shell)" % help # Read command-line help text navi --help # Read project README.md navi fn url::open "https://github.com/denisidoro/navi" ================================================ FILE: docs/examples/configuration/config-example.yaml ================================================ # THIS IS EXPERIMENTAL # the config file schema may change at any time style: tag: color: cyan # text color. possible values: https://bit.ly/3gloNNI width_percentage: 26 # column width relative to the terminal window min_width: 20 # minimum column width as number of characters comment: color: blue width_percentage: 42 min_width: 45 snippet: color: white finder: command: fzf # equivalent to the --finder option # overrides: --tac # equivalent to the --fzf-overrides option # overrides_var: --tac # equivalent to the --fzf-overrides-var option # 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 # cheats: # paths: # - /path/to/some/dir # on unix-like os # - F:\\path\\to\\dir # on Windows # search: # tags: git,!checkout # equivalent to the --tag-rules option # client: # tealdeer: true # enables tealdeer support for navi --tldr shell: # Shell used for shell out. Possible values: bash, zsh, dash, ... # For Windows, use `cmd.exe` instead. command: bash # finder_command: bash # similar, but for fzf's internals ================================================ FILE: docs/installation/README.md ================================================ # Installation of navi This is a reference of all known methods to install navi. > [!CAUTION] > Navi, as of now, has only two official builds, the released binaries on GitHub > and the published package on brew. > > All the other packages are community-maintained. ## Using package managers ### Homebrew ```sh brew install navi ``` > [!NOTE] > See [brew.sh](https://brew.sh/) for more details. ### Using Gentoo > [!WARNING] > You need to enable the GURU overlay for the instructions below to work correctly. > > For more details see: > > - [wiki.gentoo.org/wiki/Ebuild_repository](https://wiki.gentoo.org/wiki/Ebuild_repository) > - [gpo.zugaina.org/Overlays/guru/app-misc/navi](https://gpo.zugaina.org/Overlays/guru/app-misc/navi). ```sh emerge -a app-misc/navi ``` > [!NOTE] > See [Gentoo.org](https://gentoo.org/) for more details. ### Using Pacman ```sh pacman -S navi ``` > [!NOTE] > See [wiki.archlinux.org/title/Pacman](https://wiki.archlinux.org/title/Pacman) for more details. ### Using nix ```sh nix-env -iA nixpkgs.navi ``` > [!NOTE] > See [nixos.org](https://nixos.org/) for more details ### Using Cargo ```bash cargo install --locked navi ``` > [!NOTE] > See [@rust-lang/cargo](https://github.com/rust-lang/cargo) for more details. ### Using Chocolatey ```bash choco install navi ``` > [!CAUTION] > You currently need to create the config file `$env:USERPROFILE\AppData\Roaming\navi\config.yaml` > and define the `shell.command` directive as `powershell` for navi to work correctly. > > ```yaml > shell: > command: powershell > ``` > [!NOTE] > See [community.chocolatey.org](https://community.chocolatey.org) for more details. ## Using the installation script Navi has an installation script ready for you to use, you can call it like this: ```bash bash <(curl -sL https://raw.githubusercontent.com/denisidoro/navi/master/scripts/install) ``` If you need to define the directory for the binary, you can call it like this: ```bash BIN_DIR=/usr/local/bin bash <(curl -sL https://raw.githubusercontent.com/denisidoro/navi/master/scripts/install) ``` ## Downloading pre-compiled binaries With each release, we try our best to build and publish a binary for each supported platform, you can find them here: [@denisidoro/navi/releases/latest](https://github.com/denisidoro/navi/releases/latest) What you need to do is: - to download the binary corresponding to the version you want to install - to extract the content of the archive to your `$PATH` ## Building from source You can also build navi from source, it's mainly used by contributors to test their modifications but can be used by end users who want to build their own version. - You need to clone the repository: ```bash git clone https://github.com/denisidoro/navi && cd navi ``` - Call `make` ```bash make install ``` You can specify the binary directory with: ```bash make BIN_DIR=/usr/local/bin install ``` ## Compile time environment variables **navi** supports environment variables at compile time that will modify the behavior of navi at runtime, they are: | Environment variable | Description | |----------------------|-------------------------------------------------------------| | `NAVI_PATH` | This defines the default path used by navi for cheatsheets. | | `NAVI_CONFIG` | This defines the default configuration file used by navi. | ## Other package managers You can find **navi** for more package managers by clicking on the image below: [![Packaging status](https://repology.org/badge/vertical-allrepos/navi.svg)](https://repology.org/project/navi/versions) Feel free to be the maintainer of **navi** for any package manager you'd like! ================================================ FILE: docs/usage/README.md ================================================ # The usage of Navi Navi can be used in multiple ways #### Defining the cheatsheets path at runtime You can define the paths to use for cheatsheets at runtime using the `--path` parameter and a colon-separated paths list For example, if we want to search for cheatsheets in `/some/dir` and in `/other/dir`: ```sh navi --path '/some/dir:/other/dir' ``` ## Logging The log file will be created under the same directory where the configuration file is located.\ You can use the `RUST_LOG` environment variable to set the log level. For example, to have the logging in debug mode when running navi: ```bash RUST_LOG=debug navi ``` > [!NOTE] > If the directory of the configuration file doesn't exist, no log file > is going to be created. ================================================ FILE: docs/usage/commands/info/README.md ================================================ # The info subcommands of navi Navi exposes information about its default values or examples for you to use. * [The info subcommands of navi](#the-info-subcommands-of-navi) * [Commands Reference](#commands-reference) * [Default configuration information](#default-configuration-information) * [Default configuration path](#default-configuration-path) * [Example configuration file](#example-configuration-file) * [Default cheatsheets path](#default-cheatsheets-path) ## Commands Reference | Command | Description | |---------------------|----------------------------------------------------| | config-path | [DEPRECATED] Lets you see the default config path | | cheats-path | [DEPRECATED] Lets you see the default cheats path | | default-config-path | Lets you see the default config path | | default-cheats-path | Lets you see the default cheats path | | config-example | Lets you see an example for the configuration file | | cheats-example | Lets you see an example for a cheat file | ## Default configuration information ### Default configuration path Navi exposes its default configuration path with: ```sh navi info config-path ``` > [!NOTE] > See [/docs/configuration/](/docs/configuration/README.md#the-default-configuration-file-path) for more details on how the default configuration path is defined. ### Example configuration file Navi lets you get an example configuration file with: ```sh navi info config-example ``` > [!NOTE] > You can retrieve this file at the following address: [/docs/examples/configuration/config-example.yaml](/docs/examples/configuration/config-example.yaml) For example, you can use this command to create the default configuration file, if not already present: ```sh navi info config-example > "$(navi info config-path)" ``` ## Default cheatsheets path Navi exposes its default cheatsheets path with: ```sh navi info cheats-path ``` > [!NOTE] > See [/docs/configuration/](/docs/configuration/README.md#the-default-cheatsheets-path) for more details on how the cheatsheets path is defined. ================================================ FILE: docs/usage/commands/repo/README.md ================================================ # The repo subcommands of navi * [The repo subcommands of navi](#the-repo-subcommands-of-navi) * [Commands Reference](#commands-reference) * [Browsing through cheatsheet repositories](#browsing-through-cheatsheet-repositories) * [Importing cheatsheet repositories](#importing-cheatsheet-repositories) ## Commands Reference | Command | Description | |---------|-------------------------------------------------------------------| | add | Lets you import a cheatsheet repository | | browser | Lets you browse through a curated list of cheatsheet repositories | ## Browsing through cheatsheet repositories 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). You can find them within navi with the following command: ```sh navi repo browse ``` ## Importing cheatsheet repositories You can import `cheatsheet repositories` using a working git-clone format.\ This includes using an HTTPS URL or an SSH URI. - Import using HTTPS ```sh navi repo add https://github.com/denisidoro/cheats ``` - Import using SSH ```shell navi repo add git@github.com:denisidoro/cheats ``` > [!CAUTION] > Despite `$NAVI_PATH` being set, it will not be used when installing cheat sheets directly via navi's own commands.\ > For example when running `navi add repo `, the default paths will still be used. > > To avoid this, you may simply clone repos via a regular `git clone` command, directly into `$NAVI_PATH`. ================================================ FILE: docs/usage/fzf-overrides/README.md ================================================ # The FZF Overrides of Navi Navi allows you to override certain parts of FZF in multiple ways. * [The FZF Overrides of Navi](#the-fzf-overrides-of-navi) * [Command line arguments](#command-line-arguments) * [Environment variables](#environment-variables) ## Command line arguments Navi allows you to use command line arguments in order to override fzf values: ```sh # if you want to override only when selecting snippets navi --fzf-overrides '--height 3' # if you want to override only when selecting argument values navi --fzf-overrides-var '--height 3' ``` ## Environment variables Navi allows you to use environment variables in order to override fzf values. ```bash # if you want to override for all cases FZF_DEFAULT_OPTS="--height 3" navi ``` ================================================ FILE: docs/usage/shell-scripting/README.md ================================================ # Navi and shell scripting You can use Navi with shell scripting. * [Navi and shell scripting](#navi-and-shell-scripting) * [Simply calling a cheat](#simply-calling-a-cheat) * [Defining variables while calling](#defining-variables-while-calling) * [Filtering results for a variable](#filtering-results-for-a-variable) * [Selecting the best match for a variable](#selecting-the-best-match-for-a-variable) > [NOTE!] > 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/) ## Simply calling a cheat Below is an example on how to call a cheat from within navi: ```sh navi --query "change branch" --best-match ``` > [!NOTE] > Navi will ask the user to fill all arguments/variables needed. ## Defining variables while calling If you want to set the `` beforehand in your script, you can do as follows: ```sh branch="master" navi --query "change branch" --best-match ``` Navi will not show any interactive input and `` will be exactly the one defined while calling. ## Filtering results for a variable If you want to filter some results for ``, you can do as follows: ```sh branch__query="master" navi --query "change branch" --best-match ``` Navi will show any interactive input, unless a single entry is automatically selected and the value for `` will be the one selected by the user. ## Selecting the best match for a variable If you want to select the best match for ``, you can do as follows: ```sh branch__best="master" navi --query "change branch" --best-match ``` Navi will not show any interactive input, and the value for `` will be the one that best matches the value passed as argument. ================================================ FILE: docs/widgets/README.md ================================================ # Navi widgets You want to launch Navi with a shortcut?\ Widgets are here for you! Widgets are 3rd-party contributions and integrates Navi with 3rd-party software such as shells. ## List of shell widgets | Shell | Navi support | |------------|--------------------| | Bash | :white_check_mark: | | Fish | | | Zsh | | | NuShell | :white_check_mark: | | PowerShell | :white_check_mark: | ## PowerShell Widget - Removal ```powershell Remove-Module navi.plugin ``` ## Other widgets - Tmux - Vim ### Installing the shell widget If you want to install it, add this line to your `.bashrc`-like file: ```sh # bash eval "$(navi widget bash)" # zsh eval "$(navi widget zsh)" # fish navi widget fish | source # elvish eval (navi widget elvish | slurp) # xonsh # xpip install xontrib-navi # ← run in your xonsh session to install xontrib xontrib load navi # ← add to your xonsh run control file ``` #### Nushell 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: 1. run `^navi widget nushell | save ($nu.default-config-dir | path join "navi-integration.nu")` 2. add the following lines to `config.nu`: ```nushell source ($nu.default-config-dir | path join "navi-integration.nu") ``` 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). 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: 1. run, e.g., `navi widget bash` in your terminal 2. copy the output 3. paste the output in your `.bashrc`-like file 4. edit the contents accordingly ================================================ FILE: docs/widgets/howto/TMUX.md ================================================ # Tmux widget You can use **navi** as a [Tmux](https://github.com/tmux/tmux/wiki) widget to reach your Vim commands, often used SQL queries, etc. in any command-line app even in SSH sessions. * [Tmux widget](#tmux-widget) * [Keybinding navi](#keybinding-navi) * [Example cheatsheet](#example-cheatsheet) ## Keybinding navi To be able to open navi via prefix + C-g , you need to add the following lines to your Tmux configuration file. ```sh bind-key -N "Open Navi (cheat sheets)" -T prefix C-g split-window \ "$SHELL --login -i -c 'navi --print | tmux load-buffer -b tmp - ; tmux paste-buffer -p -t {last} -b tmp -d'" ``` ## Example cheatsheet Here is an example cheatsheet to use inside Tmux: ```sh % vim # Quit without save qa! # Delete a paragraph normal dap # Generate sequence of numbers put =range(, ) % postgresql # Describe table columns in `psql` or `pgcli` select table_name, column_name, data_type from information_schema.columns where table_name = ''; ``` ================================================ FILE: docs/widgets/howto/VIM.md ================================================ # Vim widget * [Vim widget](#vim-widget) * [Syntax Highlighting](#syntax-highlighting) ## Syntax Highlighting If you want syntax highlighting support for Navi in Vim, you need to add those syntax rules to your syntax files such as at `$VIMRUNTIME/syntax/navi.vim`. The rules are defined based on the [Cheatsheet syntax](/docs/cheatsheet/syntax/README.md). Here is an example: ```vim syntax match Comment "\v^;.*$" syntax match Statement "\v^\%.*$" syntax match Operator "\v^\#.*$" syntax match String "\v\<.{-}\>" syntax match String "\v^\$.*$" ``` ================================================ FILE: rust-toolchain.toml ================================================ [toolchain] channel = "1.81.0" components = [ "rustfmt", "clippy" ] ================================================ FILE: rustfmt.toml ================================================ max_width = 110 ================================================ FILE: scripts/docker ================================================ #!/usr/bin/env bash set -euo pipefail export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)" _start() { cd "$NAVI_HOME" ./scripts/release x86_64-unknown-linux-musl docker run \ -e HOMEBREW_NO_AUTO_UPDATE=1 \ -e HOMEBREW_NO_INSTALL_CLEANUP=1 \ -v "$(pwd):/navi" \ -it 'bashell/alpine-bash' \ bash -c '/navi/scripts docker setup; exec bash' } _setup() { apk add git apk add curl git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf ln -s /navi/target/debug/navi /usr/local/bin/navi } main() { local -r fn="$1" shift || true "_${fn}" "$@" } main "$@" ================================================ FILE: scripts/dot ================================================ #!/usr/bin/env bash set -euo pipefail export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)" export PROJ_HOME="$NAVI_HOME" export PROJ_NAME="navi" export CARGO_PATH="${NAVI_HOME}/core/Cargo.toml" # TODO: bump dotfiles + remove this fn log::note() { log::info "$@"; } cargo() { if [ "${1:-}" = "install" ] && [ "${2:-}" = "cross" ]; then shift 2 || true command cargo install cross --git https://github.com/cross-rs/cross "$@" else command cargo "$@" fi } export -f log::note cargo dot::clone() { git clone 'https://github.com/denisidoro/dotfiles' "$DOTFILES" cd "$DOTFILES" git checkout 'v2022.07.16' } dot::clone_if_necessary() { [ -n "${DOTFILES:-}" ] && [ -x "${DOTFILES}/bin/dot" ] && return export DOTFILES="${NAVI_HOME}/target/dotfiles" dot::clone } dot::clone_if_necessary "${DOTFILES}/bin/dot" "$@" ================================================ FILE: scripts/install ================================================ #!/usr/bin/env bash set -euo pipefail if ${X_MODE:-false}; then set -x fi # ===================== # paths # ===================== export CARGO_DEFAULT_BIN="${HOME}/.cargo/bin" export BIN_DIR="${BIN_DIR:-"$CARGO_DEFAULT_BIN"}" # ===================== # logging # ===================== echoerr() { echo "$@" 1>&2 } tap() { local -r x="$(cat)" echoerr "$x" echo "$x" } log::ansi() { local bg=false case "$@" in *reset*) echo "\e[0m"; return 0 ;; *black*) color=30 ;; *red*) color=31 ;; *green*) color=32 ;; *yellow*) color=33 ;; *blue*) color=34 ;; *purple*) color=35 ;; *cyan*) color=36 ;; *white*) color=37 ;; esac case "$@" in *regular*) mod=0 ;; *bold*) mod=1 ;; *underline*) mod=4 ;; esac case "$@" in *background*) bg=true ;; *bg*) bg=true ;; esac if $bg; then echo "\e[${color}m" else echo "\e[${mod:-0};${color}m" fi } _log() { local template="$1" shift echoerr "$(printf "$template" "$@")" } _header() { local TOTAL_CHARS=60 local total=$TOTAL_CHARS-2 local size=${#1} local left=$((($total - $size) / 2)) local right=$(($total - $size - $left)) printf "%${left}s" '' | tr ' ' = printf " $1 " printf "%${right}s" '' | tr ' ' = } log::header() { _log "\n$(log::ansi bold)$(log::ansi purple)$(_header "$1")$(log::ansi reset)\n"; } log::success() { _log "$(log::ansi green)✔ %s$(log::ansi reset)\n" "$@"; } log::error() { _log "$(log::ansi red)✖ %s$(log::ansi reset)\n" "$@"; } log::warning() { _log "$(log::ansi yellow)➜ %s$(log::ansi reset)\n" "$@"; } log::note() { _log "$(log::ansi blue)%s$(log::ansi reset)\n" "$@"; } # TODO: remove header() { echoerr "$*" echoerr } die() { log::error "$@" exit 42 } no_binary_warning() { log::note "There's no precompiled binary for your platform: $(uname -a)" } installation_finish_instructions() { local -r shell="$(get_shell)" log::note -e "Finished. To call navi, restart your shell or reload the config file:\n source ~/.${shell}rc" local code if [[ "$shell" == "zsh" ]]; then code="navi widget ${shell} | source" else code='source <(navi widget '"$shell"')' fi log::note -e "\nTo add the Ctrl-G keybinding, add the following to ~/.${shell}rc:\n ${code}" } # ===================== # security # ===================== sha256() { if command_exists sha256sum; then sha256sum elif command_exists shasum; then shasum -a 256 elif command_exists openssl; then openssl dgst -sha256 else log::note "Unable to calculate sha256!" exit 43 fi } # ===================== # github # ===================== latest_version_released() { curl -s 'https://api.github.com/repos/denisidoro/navi/releases/latest' \ | grep -Eo '"html_url": "https://github.com/denisidoro/navi/releases/tag/v([0-9\.]+)' \ | sed 's|"html_url": "https://github.com/denisidoro/navi/releases/tag/v||' } asset_url() { local -r version="$1" local -r variant="${2:-}" if [[ -n "$variant" ]]; then echo "https://github.com/denisidoro/navi/releases/download/v${version}/navi-v${version}-${variant}.tar.gz" else echo "https://github.com/denisidoro/navi/archive/v${version}.tar.gz" fi } download_asset() { local -r tmp_dir="$(mktemp -d -t navi-install-XXXX)" local -r url="$(asset_url "$@")" log::note "Downloading ${url}..." cd "$tmp_dir" curl -L "$url" -o navi.tar.gz tar xvzf navi.tar.gz mkdir -p "${BIN_DIR}" &>/dev/null || true mv "./navi" "${BIN_DIR}/navi" } sha_for_asset_on_github() { local -r url="$(asset_url "$@")" curl -sL "$url" | sha256 | awk '{print $1}' } error_installing() { log::error "Unable to install navi. Please check https://github.com/denisidoro/navi for alternative installation instructions" exit 33 } # ===================== # code # ===================== version_from_toml() { cat "${NAVI_HOME}/Cargo.toml" \ | grep version \ | head -n1 \ | awk '{print $NF}' \ | tr -d '"' \ | tr -d "'" } # ===================== # platform # ===================== command_exists() { type "$1" &>/dev/null } get_target() { local -r unamea="$(uname -a)" local -r archi="$(uname -sm)" local target case "$unamea $archi" in *arwin*) target="x86_64-apple-darwin" ;; *inux*x86*) target="x86_64-unknown-linux-musl" ;; *ndroid*aarch*|*ndroid*arm*) target="aarch64-linux-android" ;; *inux*aarch*|*inux*arm*) target="armv7-unknown-linux-musleabihf" ;; *) target="" ;; esac echo "$target" } get_shell() { echo $SHELL | xargs basename } # ===================== # main # ===================== export_path_cmd() { echo echo ' export PATH="${PATH}:'"$1"'"' } append_to_file() { local -r path="$1" local -r text="$2" if [ -f "$path" ]; then echo "$text" >> "$path" fi } get_navi_bin_path() { local file="${BIN_DIR}/navi" if [ -f "$file" ]; then echo "$file" return 0 fi file="${CARGO_DEFAULT_BIN}/navi" if [ -f "$file" ]; then echo "$file" return 0 fi } install_navi() { local -r target="$(get_target)" if command_exists navi; then log::success "navi is already installed" exit 0 elif command_exists brew; then brew install navi elif [[ -n "$target" ]]; then local -r version="$(latest_version_released)" download_asset "$version" "$target" || error_installing elif command_exists cargo; then cargo install navi else error_installing fi hash -r 2>/dev/null || true local navi_bin_path="$(which navi || get_navi_bin_path)" ln -s "$navi_bin_path" "${BIN_DIR}/navi" &>/dev/null || true if [ -f "${BIN_DIR}/navi" ]; then navi_bin_path="${BIN_DIR}/navi" fi local -r navi_bin_dir="$(dirname "$navi_bin_path")" echoerr log::success "Finished" log::success "navi is now available at ${navi_bin_path}" echoerr if echo "$PATH" | grep -q "$navi_bin_dir"; then : else local -r cmd="$(export_path_cmd "$navi_bin_dir")" append_to_file "${HOME}/.bashrc" "$cmd" append_to_file "${ZDOTDIR:-"$HOME"}/.zshrc" "$cmd" append_to_file "${HOME}/.fishrc" "$cmd" fi log::note "To call navi, restart your shell or reload your .bashrc-like config file" echo log::note "Check https://github.com/denisidoro/navi for more info" export PATH="${PATH}:${navi_bin_dir}" return 0 } (return 0 2>/dev/null) || install_navi "$@" ================================================ FILE: scripts/make ================================================ #!/usr/bin/env bash set -euo pipefail ##? make install ##? make uninstall export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)" source "${NAVI_HOME}/scripts/install" install() { cargo install --path . } uninstall() { cargo uninstall } fix() { "${NAVI_HOME}/scripts/fix" } cmd="$1" shift export X_MODE=true set -x case "$cmd" in "install") install "$@" ;; "uninstall") uninstall "$@" ;; "fix") fix "$@" ;; esac ================================================ FILE: scripts/release ================================================ #!/usr/bin/env bash set -euo pipefail ### -------------------------------------------------------------------------------------------------------------------- ### Logging functions ### -------------------------------------------------------------------------------------------------------------------- log::info() { ### Will print `[INFO]` in black foreground colour and magenta background colour ### then will print the given text in a magenta foreground colour and default background colour. printf "\033[35m\033[7m[INFO]\033[27;39m \033[35m$*\033[39m\n" } log::error() { ### Will print `[ERROR]` in black foreground colour and red background colour ### then will print the given text in a red foreground colour and default background colour. printf "\033[31m\033[7m[ERROR]\033[27;39m \033[31m$*\033[39m\n" } log::warn() { ### Will print `[WARNING]` in black foreground colour and yellow background colour ### then will print the given text in a yellow foreground colour and default background colour. printf "\033[33m\033[7m[WARNING]\033[27;39m \033[33m$*\033[39m\n" } ### -------------------------------------------------------------------------------------------------------------------- ### Utils functions ### -------------------------------------------------------------------------------------------------------------------- ### Permits us to know if the current target environment ### is a windows platform or not. is_windows() { local -r target="$1" echo "$target" | grep -q "windows" } ### NOTE: This function is currently not in use but kept as ### a backup function in case something breaks ### ### Returns the target environment, with a fix for the x86_64 target. get_env_target() { eval "$(rustc --print cfg | grep target)" local -rr raw="${target_arch:-}-${target_vendor:-}-${target_os:-}-${target_env:-}" if echo "$raw" | grep -q "x86_64-apple-macos"; then echo "x86_64-apple-darwin" else echo "$raw" fi } ### NOTE: This function is currently not in use but kept as ### a backup function in case something breaks ### ### Logs the given arguments then execute it _tap() { log::info "$@" "$@" } ### NOTE: This function is currently not in use but kept as ### a backup function in case something breaks ### ### Lists the content of a path, given as parameter. _ls() { log::info "contents from $*:" ls -la "$@" || true } ### -------------------------------------------------------------------------------------------------------------------- ### Release-Related functions ### -------------------------------------------------------------------------------------------------------------------- release() { local -r env_target="$1" log::info "env target: $env_target" local -r cross_target="${1:-"$env_target"}" log::info "desired target: $cross_target" TAR_DIR="$(pwd)/target/tar" ### We clean up the target folder, just in case rm -rf "$(pwd)/target" 2> /dev/null || true ### We add the target for rustup in case cross doesn't find it. ### Since the default behaviour of cross is to compile from ### a rustup target if it doesn't find one for itself. rustup target add $env_target cargo install cross --locked ### We're building the release via cross for the target environment cross build --release --target "$env_target" cd target/"$env_target"/release/ if is_windows "$env_target"; then ### If our target is windows, we can simply zip our executable ### since having tar is not the norm and neither the default zip -r "navi.zip" "navi.exe" ### We export a CI/CD variable to be used later in the pipeline echo "EXTENSION=zip" >> $GITHUB_OUTPUT else ### @alexis-opolka - I'm currently disabling the usage of UPX since I cannot find how ### it was used before the merge of the code from the @denisidoro/dotfiles repository. ### #if upx --best --lzma "navi"; then # log::info "upx succeeded" #else # log::info "upx failed" #fi ### For all other targets, they have tar as the norm ### or have it installed by default. tar -czf "navi.tar.gz" "navi" ### We export a CI/CD variable to be used later in the pipeline echo "EXTENSION=tar.gz" >> $GITHUB_OUTPUT fi } ### -------------------------------------------------------------------------------------------------------------------- ### Main script ### -------------------------------------------------------------------------------------------------------------------- release "$@" ================================================ FILE: scripts/test ================================================ #!/usr/bin/env bash set -euo pipefail export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)" source "${NAVI_HOME}/scripts/install" "${NAVI_HOME}/tests/run" ================================================ FILE: shell/navi.plugin.bash ================================================ #!/usr/bin/env bash _navi_call() { local result="$(navi "$@" /dev/tty 2>&1 } ================================================ FILE: shell/navi.plugin.fish ================================================ function _navi_smart_replace set --local query (commandline --current-process | string trim) set --local version_parts "" if test -n "$version" set version_parts (string split '.' $version) else set version_parts (string split '.' (string match -r '\d+\.\d+\.\d+' (fish --version))) end set --local force_repaint false # https://github.com/fish-shell/fish-shell/blob/d663f553dffba460d6d0bcdf93df21bda9ec6f3f/doc_src/interactive.rst?plain=1#L440 # > Bindings that change the mode are supposed to call the repaint-mode bind function # # Related issues # - https://github.com/fish-shell/fish-shell/issues/5033 # - https://github.com/fish-shell/fish-shell/issues/5860 # - https://github.com/fish-shell/fish-shell/blob/d663f553dffba460d6d0bcdf93df21bda9ec6f3f/src/screen.rs#L531 # # Introduced with: https://github.com/denisidoro/navi/pull/982 if test $version_parts[1] -ge 4 set force_repaint true end if test -n "$query" set --local best_match (navi --print --query "$query" --best-match) if test -n "$best_match" # --replace without --current-process: --current-process treats newlines as process # boundaries and flattens multi-line snippets into a single line commandline --replace -- "$best_match" commandline --function end-of-line end end if test -z "$best_match" set --local candidate (navi --print --query "$query") if test -n "$candidate" commandline --replace -- "$candidate" commandline --function end-of-line end end # always repaint to restore the prompt after fzf clobbers the terminal if test "$force_repaint" = true commandline --function repaint end end bind \cg _navi_smart_replace bind --mode insert \cg _navi_smart_replace ================================================ FILE: shell/navi.plugin.nu ================================================ export def navi_widget [] { let current_input = (commandline) let last_command = ($current_input | navi fn widget::last_command | str trim) match ($last_command | is-empty) { true => {^navi --print | complete | get "stdout"} false => { let find = $"($last_command)_NAVIEND" let replacement = (^navi --print --query $'($last_command)' | complete | get "stdout") match ($replacement | str trim | is-empty) { false => {$"($current_input)_NAVIEND" | str replace $find $replacement} true => $current_input } } } | str trim | commandline edit --replace $in commandline set-cursor --end } let nav_keybinding = { name: "navi", modifier: control, keycode: char_g, mode: [emacs, vi_normal, vi_insert], event: { send: executehostcommand, cmd: navi_widget, } } $env.config.keybindings = ($env.config.keybindings | append $nav_keybinding) ================================================ FILE: shell/navi.plugin.ps1 ================================================ $null = New-Module { function Invoke-Navi { $startArgs = @{ FileName = "navi"; Arguments = $args; RedirectStandardOutput = $true; WorkingDirectory = $PWD; UseShellExecute = $false; } $p = [System.Diagnostics.Process]@{StartInfo = $startArgs} [void]$p.Start() $result = $p.StandardOutput.ReadToEnd() $p.WaitForExit() $result } ### Initial code from @lurebat (https://github.com/lurebat/) ### See #570 (https://github.com/denisidoro/navi/issues/570) for its original contribution function Invoke-NaviWidget { $ast = $tokens = $errors = $cursor = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref] $ast, [ref] $tokens, [ref] $errors, [ref] $cursor) $line = $ast.ToString().Trim() $output = $null if ([String]::IsNullOrEmpty($line)) { $output = (Invoke-Navi "--print" | Out-String).Trim() } else { $best_match = (Invoke-Navi "--print --best-match --query `"$line`"" | Out-String).Trim() if ([String]::IsNullOrEmpty($best_match)) { $output = (Invoke-Navi "--print --query `"$line`"" | Out-String).Trim() } else { $output = $best_match } } [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine() [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt() ### Handling the case when the user escapes without selecting any entry if (-Not([String]::IsNullOrEmpty($output))) { [Microsoft.PowerShell.PSConsoleReadLine]::Insert([String]$output) } } Set-PSReadlineKeyHandler -BriefDescription "A keybinding to open Navi Widget" -Chord Ctrl+g -ScriptBlock { Invoke-NaviWidget } Export-ModuleMember -Function @() } ================================================ FILE: shell/navi.plugin.zsh ================================================ #!/usr/bin/env zsh _navi_call() { local result="$(navi "$@" (source: SourceError) -> Self where SourceError: Into, { FileAnIssue { source: source.into(), } } } fn main() -> anyhow::Result<()> { if let Err(err) = init_logger() { // may need redir stderr to a file to show this log initialization error eprintln!("failed to initialize logging: {err:?}"); } navi::handle().map_err(|e| { error!("{e:?}"); FileAnIssue::new(e).into() }) } fn init_logger() -> anyhow::Result<()> { const FILE_NAME: &str = "navi.log"; let mut file = navi::default_config_pathbuf()?; file.set_file_name(FILE_NAME); // If config path doesn't exist, navi won't log. if file.parent().map(|p| !p.exists()).unwrap_or(true) { return Ok(()); } let writer = std::fs::File::create(&file).with_context(|| format!("{file:?} is not created"))?; tracing::subscriber::set_global_default( tracing_subscriber::fmt() .with_ansi(false) .with_writer(writer) .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .finish(), )?; debug!("tracing initialized"); Ok(()) } ================================================ FILE: src/clients/cheatsh.rs ================================================ use crate::prelude::*; use std::process::Command; fn map_line(line: &str) -> String { line.trim().trim_end_matches(':').to_string() } fn as_lines(query: &str, markdown: &str) -> Vec { format!( "% {query}, cheat.sh {markdown}" ) .lines() .map(map_line) .collect() } pub fn call(query: &str) -> Result> { let args = ["-qO-", &format!("cheat.sh/{query}")]; let child = Command::new("wget") .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn(); let child = match child { Ok(x) => x, Err(_) => { let msg = "navi was unable to call wget. Make sure wget is correctly installed."; return Err(anyhow!(msg)); } }; let out = child.wait_with_output().context("Failed to wait for wget")?; if let Some(0) = out.status.code() { let stdout = out.stdout; let plain_bytes = strip_ansi_escapes::strip(stdout); let markdown = String::from_utf8(plain_bytes).context("Output is invalid utf8")?; if markdown.starts_with("Unknown topic.") { let msg = format!( "`{}` not found in cheatsh. Output: {} ", &query, markdown, ); return Err(anyhow!(msg)); } let lines = as_lines(query, &markdown); Ok(lines) } else { let msg = format!( "Failed to call: wget {} Output: {} Error: {} ", args.join(" "), String::from_utf8(out.stdout).unwrap_or_else(|_e| "Unable to get output message".to_string()), String::from_utf8(out.stderr).unwrap_or_else(|_e| "Unable to get error message".to_string()) ); Err(anyhow!(msg)) } } ================================================ FILE: src/clients/mod.rs ================================================ pub mod cheatsh; pub mod tldr; ================================================ FILE: src/clients/tldr.rs ================================================ use crate::config::CONFIG; use crate::prelude::*; use std::process::{Command, Stdio}; lazy_static! { pub static ref VAR_TLDR_REGEX: Regex = Regex::new(r"\{\{(.*?)\}\}").expect("Invalid regex"); pub static ref NON_VAR_CHARS_REGEX: Regex = Regex::new(r"[^\da-zA-Z_]").expect("Invalid regex"); } static VERSION_DISCLAIMER: &str = "tldr-c-client (the default one in Homebrew) doesn't support markdown files, so navi can't use it. The recommended client is tealdeer(https://github.com/dbrgn/tealdeer)."; fn convert_tldr_vars(line: &str) -> String { let caps = VAR_TLDR_REGEX.find_iter(line); let mut new_line: String = line.to_string(); for cap in caps { let braced_var = cap.as_str(); let var = &braced_var[2..braced_var.len() - 2]; let mut new_var = NON_VAR_CHARS_REGEX.replace_all(var, "_").to_string(); if let Some(c) = new_var.chars().next() { if c.to_string().parse::().is_ok() { new_var = format!("example_{new_var}"); } } let bracketed_var = format!("<{new_var}>"); new_line = new_line.replace(braced_var, &bracketed_var); } new_line } fn convert_tldr(line: &str) -> String { let line = line.trim(); if line.starts_with('-') { format!("{}{}", "# ", &line[2..line.len() - 1]) } else if line.starts_with('`') { convert_tldr_vars(&line[1..line.len() - 1]) } else if line.starts_with('%') { line.to_string() } else { "".to_string() } } fn markdown_lines(query: &str, markdown: &str) -> Vec { format!( "% {query}, tldr {markdown}" ) .lines() .map(convert_tldr) .collect() } pub fn call(query: &str) -> Result> { let tealdeer = CONFIG.tealdeer(); let output_flag = if tealdeer { "--raw" } else { "--markdown" }; let args = [query, output_flag]; let child = Command::new("tldr") .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn(); let child = match child { Ok(x) => x, Err(_) => { let msg = format!( "navi was unable to call tldr. Make sure tldr is correctly installed. Note: {VERSION_DISCLAIMER} " ); return Err(anyhow!(msg)); } }; let out = child.wait_with_output().context("Failed to wait for tldr")?; if let Some(0) = out.status.code() { let stdout = out.stdout; let markdown = String::from_utf8(stdout).context("Output is invalid utf8")?; let lines = markdown_lines(query, &markdown); Ok(lines) } else { let msg = format!( "Failed to call: tldr {} Output: {} Error: {} Note: The client.tealdeer config option can be set to enable tealdeer support. If you want to use another client, please make sure it supports the --markdown flag. If you are already using a supported version you can ignore this message. {} ", args.join(" "), String::from_utf8(out.stdout).unwrap_or_else(|_e| "Unable to get output message".to_string()), String::from_utf8(out.stderr).unwrap_or_else(|_e| "Unable to get error message".to_string()), VERSION_DISCLAIMER, ); Err(anyhow!(msg)) } } ================================================ FILE: src/commands/core/actor.rs ================================================ use crate::common::clipboard; use crate::common::fs; use crate::common::shell; use crate::common::shell::ShellSpawnError; use crate::config::Action; use crate::deser; use crate::env_var; use crate::finder::structures::{Opts as FinderOpts, SuggestionType}; use crate::prelude::*; use crate::structures::cheat::{Suggestion, VariableMap}; use crate::structures::item::Item; use shell::EOF; use std::process::Stdio; fn prompt_finder( variable_name: &str, suggestion: Option<&Suggestion>, variable_count: usize, ) -> Result { env_var::remove(env_var::PREVIEW_COLUMN); env_var::remove(env_var::PREVIEW_DELIMITER); env_var::remove(env_var::PREVIEW_MAP); let mut extra_preview: Option = None; let (suggestions, initial_opts) = if let Some(s) = suggestion { let (suggestion_command, suggestion_opts) = s; if let Some(sopts) = suggestion_opts { if let Some(c) = &sopts.column { env_var::set(env_var::PREVIEW_COLUMN, c.to_string()); } if let Some(d) = &sopts.delimiter { env_var::set(env_var::PREVIEW_DELIMITER, d); } if let Some(m) = &sopts.map { env_var::set(env_var::PREVIEW_MAP, m); } if let Some(p) = &sopts.preview { extra_preview = Some(p.into()); } } let mut cmd = shell::out(); cmd.stdout(Stdio::piped()).arg(suggestion_command); debug!(cmd = ?cmd); let child = cmd .spawn() .map_err(|e| ShellSpawnError::new(suggestion_command, e))?; let text = String::from_utf8( child .wait_with_output() .context("Failed to wait and collect output from bash")? .stdout, ) .context("Suggestions are invalid utf8")?; (text, suggestion_opts) } else { ('\n'.to_string(), &None) }; let exe = fs::exe_string(); let preview = if CONFIG.shell().contains("powershell") || CONFIG.shell().contains("pwsh") { format!( r#"{exe} preview-var {{+}} "{{q}}" "{name}"; {extra}"#, exe = exe, name = variable_name, extra = extra_preview .clone() .map(|e| format!(" echo; {e}")) .unwrap_or_default(), ) } else if CONFIG.shell().contains("cmd.exe") { format!( r#"(@echo.{{+}}{eof}{{q}}{eof}{name}{eof}{extra}) | {exe} preview-var-stdin"#, exe = exe, name = variable_name, extra = extra_preview.clone().unwrap_or_default(), eof = EOF, ) } else if CONFIG.shell().contains("fish") { format!( r#"{exe} preview-var "{{+}}" "{{q}}" "{name}"; {extra}"#, exe = exe, name = variable_name, extra = extra_preview .clone() .map(|e| format!(" echo; {e}")) .unwrap_or_default(), ) } else { format!( r#"{exe} preview-var "$(cat <<{eof} {{+}} {eof} )" "$(cat <<{eof} {{q}} {eof} )" "{name}"; {extra}"#, exe = exe, name = variable_name, extra = extra_preview .clone() .map(|e| format!(" echo; {e}")) .unwrap_or_default(), eof = EOF, ) }; let mut opts = FinderOpts { preview: Some(preview), show_all_columns: true, ..initial_opts.clone().unwrap_or_else(FinderOpts::var_default) }; opts.query = env_var::get(format!("{variable_name}__query")).ok(); if let Ok(f) = env_var::get(format!("{variable_name}__best")) { opts.filter = Some(f); opts.suggestion_type = SuggestionType::SingleSelection; } if opts.preview_window.is_none() { opts.preview_window = Some(if extra_preview.is_none() { format!("up:{}", variable_count + 3) } else { "right:50%".to_string() }); } if suggestion.is_none() { opts.suggestion_type = SuggestionType::Disabled; }; let (output, _) = CONFIG .finder() .call(opts, |stdin| { stdin .write_all(suggestions.as_bytes()) .context("Could not write to finder's stdin")?; Ok(()) }) .context("finder was unable to prompt with suggestions")?; Ok(output) } fn unique_result_count(results: &[&str]) -> usize { let mut vars = results.to_owned(); vars.sort_unstable(); vars.dedup(); vars.len() } fn replace_variables_from_snippet(snippet: &str, tags: &str, variables: VariableMap) -> Result { let mut interpolated_snippet = String::from(snippet); if CONFIG.prevent_interpolation() { return Ok(interpolated_snippet); } let variables_found: Vec<&str> = deser::VAR_REGEX.find_iter(snippet).map(|m| m.as_str()).collect(); let variable_count = unique_result_count(&variables_found); for bracketed_variable_name in variables_found { let variable_name = &bracketed_variable_name[1..bracketed_variable_name.len() - 1]; let env_variable_name = env_var::escape(variable_name); let env_value = env_var::get(&env_variable_name); let value = if let Ok(e) = env_value { e } else if let Some(suggestion) = variables.get_suggestion(tags, variable_name) { let mut new_suggestion = suggestion.clone(); new_suggestion.0 = replace_variables_from_snippet(&new_suggestion.0, tags, variables.clone())?; prompt_finder(variable_name, Some(&new_suggestion), variable_count)? } else { prompt_finder(variable_name, None, variable_count)? }; env_var::set(env_variable_name, &value); interpolated_snippet = if value.as_str() == "\n" { interpolated_snippet.replacen(bracketed_variable_name, "", 1) } else { interpolated_snippet.replacen(bracketed_variable_name, value.as_str(), 1) }; } Ok(interpolated_snippet) } pub fn with_absolute_path(snippet: String) -> String { if let Some(s) = snippet.strip_prefix("navi ") { return format!("{} {}", fs::exe_string(), s); } snippet } pub fn act( extractions: Result<(&str, Item)>, files: Vec, variables: Option, ) -> Result<()> { let ( key, Item { tags, comment, snippet, file_index, .. }, ) = extractions.unwrap(); if key == "ctrl-o" { edit::edit_file(Path::new(&files[file_index.expect("No files found")])) .expect("Could not open file in external editor"); return Ok(()); } env_var::set(env_var::PREVIEW_INITIAL_SNIPPET, &snippet); env_var::set(env_var::PREVIEW_TAGS, &tags); env_var::set(env_var::PREVIEW_COMMENT, comment); let interpolated_snippet = { let mut s = replace_variables_from_snippet( &snippet, &tags, variables.expect("No variables received from finder"), ) .context("Failed to replace variables from snippet")?; s = with_absolute_path(s); s = deser::with_new_lines(s); s }; match CONFIG.action() { Action::Print => { println!("{interpolated_snippet}"); } Action::Execute => match key { "ctrl-y" => { clipboard::copy(interpolated_snippet)?; } _ => { let mut cmd = shell::out(); cmd.arg(&interpolated_snippet[..]); debug!(cmd = ?cmd); cmd.spawn() .map_err(|e| ShellSpawnError::new(&interpolated_snippet[..], e))? .wait() .context("bash was not running")?; } }, }; Ok(()) } ================================================ FILE: src/commands/core/mod.rs ================================================ mod actor; use crate::clients::{cheatsh, tldr}; use crate::config::Source; use crate::deser; use crate::filesystem; use crate::finder::structures::Opts as FinderOpts; use crate::parser::Parser; use crate::prelude::*; use crate::structures::fetcher::{Fetcher, StaticFetcher}; use crate::welcome; pub fn init(fetcher: Box) -> Result<()> { let config = &CONFIG; let opts = FinderOpts::snippet_default(); debug!("opts = {opts:#?}"); // let fetcher = config.fetcher(); let (raw_selection, (variables, files)) = config .finder() .call(opts, |writer| { let mut parser = Parser::new(writer, true); let found_something = fetcher .fetch(&mut parser) .context("Failed to parse variables intended for finder")?; if !found_something { welcome::populate_cheatsheet(&mut parser)?; } Ok((Some(parser.variables), fetcher.files())) }) .context("Failed getting selection and variables from finder")?; debug!(raw_selection = ?raw_selection); let extractions = deser::terminal::read(&raw_selection, config.best_match()); if extractions.is_err() { return init(fetcher); } actor::act(extractions, files, variables)?; Ok(()) } pub fn get_fetcher() -> Result> { let source = CONFIG.source(); debug!(source = ?source); match source { Source::Cheats(query) => { let lines = cheatsh::call(&query)?; let fetcher = Box::new(StaticFetcher::new(lines)); Ok(fetcher) } Source::Tldr(query) => { let lines = tldr::call(&query)?; let fetcher = Box::new(StaticFetcher::new(lines)); Ok(fetcher) } Source::Filesystem(path) => { let fetcher = Box::new(filesystem::Fetcher::new(path)); Ok(fetcher) } Source::Welcome => { let fetcher = Box::new(welcome::Fetcher::new()); Ok(fetcher) } } } pub fn main() -> Result<()> { let fetcher = get_fetcher()?; init(fetcher) } ================================================ FILE: src/commands/func/map.rs ================================================ use crate::common::shell::{self, ShellSpawnError}; use crate::prelude::*; pub fn expand() -> Result<()> { let cmd = r#"sed -e 's/^.*$/"&"/' | tr '\n' ' '"#; shell::out() .arg(cmd) .spawn() .map_err(|e| ShellSpawnError::new(cmd, e))? .wait()?; Ok(()) } ================================================ FILE: src/commands/func/mod.rs ================================================ mod map; mod widget; use super::core; use super::temp; use crate::common::url; use crate::prelude::*; use clap::Args; use clap::ValueEnum; #[derive(Debug, Clone, ValueEnum)] pub enum Func { #[value(name = "url::open")] UrlOpen, #[value(name = "welcome")] Welcome, #[value(name = "widget::last_command")] WidgetLastCommand, #[value(name = "map::expand")] MapExpand, #[value(name = "temp")] Temp, } #[derive(Debug, Clone, Args)] pub struct Input { /// Function name (example: "url::open") #[arg(ignore_case = true)] pub func: Func, /// List of arguments (example: "https://google.com") pub args: Vec, } impl Runnable for Input { fn run(&self) -> Result<()> { let func = &self.func; let args = self.args.clone(); // TODO match func { Func::UrlOpen => url::open(args), Func::Welcome => core::main(), Func::WidgetLastCommand => widget::last_command(), Func::MapExpand => map::expand(), Func::Temp => temp::main(), } } } ================================================ FILE: src/commands/func/widget.rs ================================================ use crate::prelude::*; use std::io::{self, Read}; pub fn last_command() -> Result<()> { let mut text = String::new(); io::stdin().read_to_string(&mut text)?; let replacements = vec![("||", "ග"), ("|", "ඛ"), ("&&", "ඝ")]; let parts = shellwords::split(&text).unwrap_or_else(|_| text.split('|').map(|s| s.to_string()).collect()); for p in parts { for (pattern, escaped) in replacements.clone() { if p.contains(pattern) && p != pattern && p != format!("{pattern}{pattern}") { let replacement = p.replace(pattern, escaped); text = text.replace(&p, &replacement); } } } let mut extracted = text.clone(); for (pattern, _) in replacements.clone() { let mut new_parts = text.rsplit(pattern); if let Some(extracted_attempt) = new_parts.next() { if extracted_attempt.len() <= extracted.len() { extracted = extracted_attempt.to_string(); } } } for (pattern, escaped) in replacements.clone() { text = text.replace(escaped, pattern); extracted = extracted.replace(escaped, pattern); } println!("{}", extracted.trim_start()); Ok(()) } ================================================ FILE: src/commands/info.rs ================================================ use crate::filesystem; use crate::prelude::*; use clap::{Args, Subcommand}; #[derive(Debug, Clone, Args)] pub struct Input { #[clap(subcommand)] pub info: Info, } #[derive(Debug, Clone, Subcommand)] pub enum Info { /// Prints a cheatsheet example. CheatsExample, /// Prints a configuration file example. ConfigExample, /// [DEPRECATED] Prints the default cheatsheets path. /// Please use `info default-cheats-path` instead. CheatsPath, /// [DEPRECATED] Prints the default configuration path. /// Please use `info default-config-path` instead. ConfigPath, /// Prints the default cheatsheets path. DefaultCheatsPath, /// Prints the default configuration path. DefaultConfigPath, } impl Runnable for Input { fn run(&self) -> Result<()> { let info = &self.info; match info { // Here should be the example commands Info::CheatsExample => { println!("{}", include_str!("../../docs/examples/cheatsheet/example.cheat")) } Info::ConfigExample => println!( "{}", include_str!("../../docs/examples/configuration/config-example.yaml") ), // Here should be the old deprecated default value commands Info::CheatsPath => println!("{}", &filesystem::default_cheat_pathbuf()?.to_string()), Info::ConfigPath => println!("{}", &filesystem::default_config_pathbuf()?.to_string()), // Here should be the default values (computed at compile time) Info::DefaultCheatsPath => println!("{}", &filesystem::default_cheat_pathbuf()?.to_string()), Info::DefaultConfigPath => println!("{}", &filesystem::default_config_pathbuf()?.to_string()), } Ok(()) } } ================================================ FILE: src/commands/mod.rs ================================================ pub mod core; pub mod func; pub mod info; pub mod preview; pub mod repo; pub mod shell; pub mod temp; use crate::commands; use crate::prelude::*; pub fn handle() -> Result<()> { use crate::config::Command::*; debug!("CONFIG = {:#?}", &*CONFIG); match CONFIG.cmd() { None => commands::core::main(), Some(c) => match c { Preview(input) => input.run(), PreviewVarStdin(input) => input.run(), PreviewVar(input) => input.run(), Widget(input) => input.run().context("Failed to print shell widget code"), Fn(input) => input .run() .with_context(|| format!("Failed to execute function `{:#?}`", input.func)), Info(input) => input .run() .with_context(|| format!("Failed to fetch info `{:#?}`", input.info)), #[cfg(not(feature = "disable-repo-management"))] Repo(input) => input.run(), }, } } ================================================ FILE: src/commands/preview/mod.rs ================================================ use crate::deser; use crate::prelude::*; use clap::Args; use crossterm::style::{style, Stylize}; use std::process; pub mod var; pub mod var_stdin; #[derive(Debug, Clone, Args)] pub struct Input { /// Selection line pub line: String, } fn extract_elements(argstr: &str) -> Result<(&str, &str, &str)> { let mut parts = argstr.split(deser::terminal::DELIMITER).skip(3); let tags = parts.next().context("No `tags` element provided.")?; let comment = parts.next().context("No `comment` element provided.")?; let snippet = parts.next().context("No `snippet` element provided.")?; Ok((tags, comment, snippet)) } impl Runnable for Input { fn run(&self) -> Result<()> { let line = &self.line; let (tags, comment, snippet) = extract_elements(line)?; println!( "{comment} {tags} \n{snippet}", comment = style(comment).with(CONFIG.comment_color()), tags = style(format!("[{tags}]")).with(CONFIG.tag_color()), snippet = style(deser::fix_newlines(snippet)).with(CONFIG.snippet_color()), ); process::exit(0) } } ================================================ FILE: src/commands/preview/var.rs ================================================ use crate::deser; use crate::env_var; use crate::finder; use crate::prelude::*; use clap::Args; use crossterm::style::style; use crossterm::style::Stylize; use std::iter; use std::process; #[derive(Debug, Clone, Args)] pub struct Input { /// Selection line pub selection: String, /// Query match pub query: String, /// Typed text pub variable: String, } impl Runnable for Input { fn run(&self) -> Result<()> { let selection = &self.selection; let query = &self.query; let variable = &self.variable; let snippet = env_var::must_get(env_var::PREVIEW_INITIAL_SNIPPET); let tags = env_var::must_get(env_var::PREVIEW_TAGS); let comment = env_var::must_get(env_var::PREVIEW_COMMENT); let column = env_var::parse(env_var::PREVIEW_COLUMN); let delimiter = env_var::get(env_var::PREVIEW_DELIMITER).ok(); let map = env_var::get(env_var::PREVIEW_MAP).ok(); let active_color = CONFIG.tag_color(); let inactive_color = CONFIG.comment_color(); let mut colored_snippet = String::from(&snippet); let mut visited_vars: HashSet<&str> = HashSet::new(); let mut variables = String::from(""); println!( "{comment} {tags}", comment = style(comment).with(CONFIG.comment_color()), tags = style(format!("[{tags}]")).with(CONFIG.tag_color()), ); let bracketed_current_variable = format!("<{variable}>"); let bracketed_variables: Vec<&str> = { if snippet.contains(&bracketed_current_variable) { deser::VAR_REGEX.find_iter(&snippet).map(|m| m.as_str()).collect() } else { iter::once(&bracketed_current_variable) .map(|s| s.as_str()) .collect() } }; for bracketed_variable_name in bracketed_variables { let variable_name = &bracketed_variable_name[1..bracketed_variable_name.len() - 1]; if visited_vars.contains(variable_name) { continue; } else { visited_vars.insert(variable_name); } let is_current = variable_name == variable; let variable_color = if is_current { active_color } else { inactive_color }; let env_variable_name = env_var::escape(variable_name); let value = if is_current { let v = selection.trim_matches('\''); if v.is_empty() { query.trim_matches('\'') } else { v }.to_string() } else if let Ok(v) = env_var::get(&env_variable_name) { v } else { "".to_string() }; let replacement = format!( "{variable}", variable = style(bracketed_variable_name).with(variable_color), ); colored_snippet = colored_snippet.replace(bracketed_variable_name, &replacement); variables = format!( "{variables}\n{variable} = {value}", variables = variables, variable = style(variable_name).with(variable_color), value = if env_var::get(&env_variable_name).is_ok() { value } else if is_current { finder::process(value, column, delimiter.as_deref(), map.clone()) .expect("Unable to process value") } else { "".to_string() } ); } println!("{snippet}", snippet = deser::fix_newlines(&colored_snippet)); println!("{variables}"); process::exit(0) } } ================================================ FILE: src/commands/preview/var_stdin.rs ================================================ use clap::Args; use super::var; use crate::common::shell::{self, ShellSpawnError, EOF}; use crate::prelude::*; use std::io::{self, Read}; #[derive(Debug, Clone, Args)] pub struct Input {} impl Runnable for Input { fn run(&self) -> Result<()> { let mut text = String::new(); io::stdin().read_to_string(&mut text)?; let mut parts = text.split(EOF); let selection = parts.next().expect("Unable to get selection").to_owned(); let query = parts.next().expect("Unable to get query").to_owned(); let variable = parts.next().expect("Unable to get variable").trim().to_owned(); let input = var::Input { selection, query, variable, }; input.run()?; if let Some(extra) = parts.next() { if !extra.is_empty() { print!(""); let mut cmd = shell::out(); cmd.arg(extra); debug!(?cmd); cmd.spawn().map_err(|e| ShellSpawnError::new(extra, e))?.wait()?; } } Ok(()) } } ================================================ FILE: src/commands/repo/add.rs ================================================ use crate::common::git; use crate::filesystem; use crate::finder::structures::{Opts as FinderOpts, SuggestionType}; use crate::finder::FinderChoice; use crate::prelude::*; use std::fs; use std::path; fn ask_if_should_import_all(finder: &FinderChoice) -> Result { let opts = FinderOpts { column: Some(1), header: Some("Do you want to import all files from this repo?".to_string()), ..Default::default() }; let (response, _) = finder .call(opts, |stdin| { stdin .write_all(b"Yes\nNo") .context("Unable to writer alternatives")?; Ok(()) }) .context("Unable to get response")?; Ok(response.to_lowercase().starts_with('y')) } pub fn main(uri: String) -> Result<()> { let finder = CONFIG.finder(); let should_import_all = ask_if_should_import_all(&finder).unwrap_or(false); let (actual_uri, user, repo) = git::meta(uri.as_str()); let cheat_pathbuf = filesystem::default_cheat_pathbuf()?; let tmp_pathbuf = filesystem::tmp_pathbuf()?; let tmp_path_str = &tmp_pathbuf.to_string(); let _ = filesystem::remove_dir(&tmp_pathbuf); filesystem::create_dir(&tmp_pathbuf)?; eprintln!("Cloning {} into {}...\n", &actual_uri, &tmp_path_str); git::shallow_clone(actual_uri.as_str(), tmp_path_str) .with_context(|| format!("Failed to clone `{actual_uri}`"))?; let all_files = filesystem::all_cheat_files(&tmp_pathbuf).join("\n"); let opts = FinderOpts { suggestion_type: SuggestionType::MultipleSelections, preview: Some(format!("cat '{tmp_path_str}/{{}}'")), header: Some("Select the cheatsheets you want to import with then hit \nUse Ctrl-R for (de)selecting all".to_string()), preview_window: Some("right:30%".to_string()), ..Default::default() }; let files = if should_import_all { all_files } else { let (files, _) = finder .call(opts, |stdin| { stdin .write_all(all_files.as_bytes()) .context("Unable to prompt cheats to import")?; Ok(()) }) .context("Failed to get cheatsheet files from finder")?; files }; let to_folder = { let mut p = cheat_pathbuf; p.push(format!("{user}__{repo}")); p }; for file in files.split('\n') { let from = { let mut p = tmp_pathbuf.clone(); p.push(file); p }; let filename = file .replace(&format!("{}{}", &tmp_path_str, path::MAIN_SEPARATOR), "") .replace(path::MAIN_SEPARATOR, "__"); let to = { let mut p = to_folder.clone(); p.push(filename); p }; fs::create_dir_all(&to_folder).unwrap_or(()); fs::copy(&from, &to) .with_context(|| format!("Failed to copy `{}` to `{}`", &from.to_string(), &to.to_string()))?; } filesystem::remove_dir(&tmp_pathbuf)?; eprintln!( "The following .cheat files were imported successfully:\n{}\n\nThey are now located at {}", files, to_folder.to_string() ); Ok(()) } ================================================ FILE: src/commands/repo/browse.rs ================================================ use crate::filesystem; use crate::finder::structures::{Opts as FinderOpts, SuggestionType}; use crate::common::git; use crate::prelude::*; use std::fs; pub fn main() -> Result { let finder = CONFIG.finder(); let repo_pathbuf = { let mut p = filesystem::tmp_pathbuf()?; p.push("featured"); p }; let repo_path_str = &repo_pathbuf.to_string(); let _ = filesystem::remove_dir(&repo_pathbuf); filesystem::create_dir(&repo_pathbuf)?; let (repo_url, _, _) = git::meta("denisidoro/cheats"); git::shallow_clone(repo_url.as_str(), repo_path_str) .with_context(|| format!("Failed to clone `{repo_url}`"))?; let feature_repos_file = { let mut p = repo_pathbuf.clone(); p.push("featured_repos.txt"); p }; let repos = fs::read_to_string(feature_repos_file).context("Unable to fetch featured repositories")?; let opts = FinderOpts { column: Some(1), suggestion_type: SuggestionType::SingleSelection, ..Default::default() }; let (repo, _) = finder .call(opts, |stdin| { stdin .write_all(repos.as_bytes()) .context("Unable to prompt featured repositories")?; Ok(()) }) .context("Failed to get repo URL from finder")?; filesystem::remove_dir(&repo_pathbuf)?; Ok(repo) } ================================================ FILE: src/commands/repo/mod.rs ================================================ use crate::commands; use crate::prelude::*; use clap::{Args, Subcommand}; pub mod add; pub mod browse; #[derive(Debug, Clone, Subcommand)] pub enum RepoCommand { /// Imports cheatsheets from a repo Add { /// A URI to a git repository containing .cheat files ("user/repo" will download cheats from github.com/user/repo) uri: String, }, /// Browses for featured cheatsheet repos Browse, } #[derive(Debug, Clone, Args)] pub struct Input { #[clap(subcommand)] pub cmd: RepoCommand, } impl Runnable for Input { fn run(&self) -> Result<()> { match &self.cmd { RepoCommand::Add { uri } => { add::main(uri.clone()) .with_context(|| format!("Failed to import cheatsheets from `{uri}`"))?; commands::core::main() } RepoCommand::Browse => { let repo = browse::main().context("Failed to browse featured cheatsheets")?; add::main(repo.clone()) .with_context(|| format!("Failed to import cheatsheets from `{repo}`"))?; commands::core::main() } } } } ================================================ FILE: src/commands/shell.rs ================================================ use std::fmt; use std::fmt::Display; use clap::Args; use crate::common::shell::Shell; use crate::prelude::*; impl Display for Shell { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { Self::Bash => "bash", Self::Zsh => "zsh", Self::Fish => "fish", Self::Elvish => "elvish", Self::Nushell => "nushell", Self::Powershell => "powershell", }; write!(f, "{s}") } } #[derive(Debug, Clone, Args)] pub struct Input { #[clap(ignore_case = true, default_value_t = Shell::Bash)] pub shell: Shell, } impl Runnable for Input { fn run(&self) -> Result<()> { let shell = &self.shell; let content = match shell { Shell::Bash => include_str!("../../shell/navi.plugin.bash"), Shell::Zsh => include_str!("../../shell/navi.plugin.zsh"), Shell::Fish => include_str!("../../shell/navi.plugin.fish"), Shell::Elvish => include_str!("../../shell/navi.plugin.elv"), Shell::Nushell => include_str!("../../shell/navi.plugin.nu"), Shell::Powershell => include_str!("../../shell/navi.plugin.ps1"), }; println!("{content}"); Ok(()) } } ================================================ FILE: src/commands/temp.rs ================================================ use crate::commands::core::get_fetcher; use crate::common::shell::{self, ShellSpawnError}; use crate::finder::structures::Opts as FinderOpts; use crate::parser::Parser; use crate::{deser, prelude::*}; use std::io::{self, Write}; pub fn main() -> Result<()> { let _config = &CONFIG; let _opts = FinderOpts::snippet_default(); let fetcher = get_fetcher()?; let hash: u64 = 2087294461664323320; let mut buf = vec![]; let mut parser = Parser::new(&mut buf, false); parser.set_hash(hash); let _res = fetcher .fetch(&mut parser) .context("Failed to parse variables intended for finder")?; let variables = parser.variables; let item_str = String::from_utf8(buf)?; let item = deser::raycast::read(&item_str)?; dbg!(&item); let x = variables.get_suggestion(&item.tags, "local_branch").expect("foo"); dbg!(&x); let suggestion_command = x.0.clone(); let child = shell::out() .stdout(Stdio::piped()) .arg(&suggestion_command) .spawn() .map_err(|e| ShellSpawnError::new(suggestion_command, e))?; let text = String::from_utf8( child .wait_with_output() .context("Failed to wait and collect output from bash")? .stdout, ) .context("Suggestions are invalid utf8")?; dbg!(&text); Ok(()) } pub fn _main0() -> Result<()> { let _config = &CONFIG; let fetcher = get_fetcher()?; let mut stdout = io::stdout(); let mut writer: Box<&mut dyn Write> = Box::new(&mut stdout); let mut parser = Parser::new(&mut writer, false); let _res = fetcher .fetch(&mut parser) .context("Failed to parse variables intended for finder")?; Ok(()) } ================================================ FILE: src/common/clipboard.rs ================================================ use crate::common::shell::{self, ShellSpawnError, EOF}; use crate::prelude::*; pub fn copy(text: String) -> Result<()> { let cmd = r#" exst() { type "$1" &>/dev/null } _copy() { if exst pbcopy; then pbcopy elif exst xclip; then xclip -selection clipboard elif exst clip.exe; then clip.exe else exit 55 fi }"#; shell::out() .arg( format!( r#"{cmd} read -r -d '' x <<'{EOF}' {text} {EOF} echo -n "$x" | _copy"#, ) .as_str(), ) .spawn() .map_err(|e| ShellSpawnError::new(cmd, e))? .wait()?; Ok(()) } ================================================ FILE: src/common/deps.rs ================================================ use crate::prelude::*; pub trait HasDeps { fn deps(&self) -> HashSet { HashSet::new() } } ================================================ FILE: src/common/fs.rs ================================================ use crate::prelude::*; use remove_dir_all::remove_dir_all; use std::ffi::OsStr; use std::fs::{self, create_dir_all, File}; use std::io; use thiserror::Error; pub trait ToStringExt { fn to_string(&self) -> String; } impl ToStringExt for Path { fn to_string(&self) -> String { self.to_string_lossy().to_string() } } impl ToStringExt for OsStr { fn to_string(&self) -> String { self.to_string_lossy().to_string() } } #[derive(Error, Debug)] #[error("Invalid path `{0}`")] pub struct InvalidPath(pub PathBuf); #[derive(Error, Debug)] #[error("Unable to read directory `{dir}`")] pub struct UnreadableDir { dir: PathBuf, #[source] source: anyhow::Error, } pub fn open(filename: &Path) -> Result { File::open(filename).with_context(|| { let x = filename.to_string(); format!("Failed to open file {}", &x) }) } pub fn read_lines(filename: &Path) -> Result>> { let file = open(filename)?; Ok(io::BufReader::new(file) .lines() .map(|line| line.map_err(Error::from))) } pub fn pathbuf_to_string(pathbuf: &Path) -> Result { Ok(pathbuf .as_os_str() .to_str() .ok_or_else(|| InvalidPath(pathbuf.to_path_buf())) .map(str::to_string)?) } fn follow_symlink(pathbuf: PathBuf) -> Result { fs::read_link(pathbuf.clone()) .map(|o| { let o_str = o .as_os_str() .to_str() .ok_or_else(|| InvalidPath(o.to_path_buf()))?; if o_str.starts_with('.') { let p = pathbuf .parent() .ok_or_else(|| anyhow!("`{}` has no parent", pathbuf.display()))?; let mut p = PathBuf::from(p); p.push(o_str); follow_symlink(p) } else { follow_symlink(o) } }) .unwrap_or(Ok(pathbuf)) } fn exe_pathbuf() -> Result { let pathbuf = std::env::current_exe().context("Unable to acquire executable's path")?; #[cfg(target_family = "windows")] let pathbuf = dunce::canonicalize(pathbuf)?; debug!(current_exe = ?pathbuf); follow_symlink(pathbuf) } fn exe_abs_string() -> Result { pathbuf_to_string(&exe_pathbuf()?) } pub fn exe_string() -> String { exe_abs_string().unwrap_or_else(|_| "navi".to_string()) } pub fn create_dir(path: &Path) -> Result<()> { create_dir_all(path).with_context(|| { format!( "Failed to create directory `{}`", pathbuf_to_string(path).expect("Unable to parse {path}") ) }) } pub fn remove_dir(path: &Path) -> Result<()> { remove_dir_all(path).with_context(|| { format!( "Failed to remove directory `{}`", pathbuf_to_string(path).expect("Unable to parse {path}") ) }) } ================================================ FILE: src/common/git.rs ================================================ use crate::common::shell::ShellSpawnError; use crate::prelude::*; use std::process::Command; pub fn shallow_clone(uri: &str, target: &str) -> Result<()> { Command::new("git") .args(["clone", uri, target, "--depth", "1"]) .spawn() .map_err(|e| ShellSpawnError::new("git clone", e))? .wait() .context("Unable to git clone")?; Ok(()) } pub fn meta(uri: &str) -> (String, String, String) { let actual_uri = if uri.contains("://") || uri.contains('@') { uri.to_string() } else { format!("https://github.com/{uri}") }; let uri_to_split = actual_uri.replace(':', "/"); let parts: Vec<&str> = uri_to_split.split('/').collect(); let user = parts[parts.len() - 2]; let repo = parts[parts.len() - 1].replace(".git", ""); (actual_uri, user.to_string(), repo) } #[cfg(test)] mod tests { use super::*; #[test] fn test_meta_github_https() { let (actual_uri, user, repo) = meta("https://github.com/denisidoro/navi"); assert_eq!(actual_uri, "https://github.com/denisidoro/navi".to_string()); assert_eq!(user, "denisidoro".to_string()); assert_eq!(repo, "navi".to_string()); } #[test] fn test_meta_github_ssh() { let (actual_uri, user, repo) = meta("git@github.com:denisidoro/navi.git"); assert_eq!(actual_uri, "git@github.com:denisidoro/navi.git".to_string()); assert_eq!(user, "denisidoro".to_string()); assert_eq!(repo, "navi".to_string()); } #[test] fn test_meta_gitlab_https() { let (actual_uri, user, repo) = meta("https://gitlab.com/user/repo.git"); assert_eq!(actual_uri, "https://gitlab.com/user/repo.git".to_string()); assert_eq!(user, "user".to_string()); assert_eq!(repo, "repo".to_string()); } } ================================================ FILE: src/common/hash.rs ================================================ use std::hash::{Hash, Hasher}; const MAGIC_INIT: u64 = 0x811C_9DC5; pub fn fnv(x: &T) -> u64 { let mut hasher = FnvHasher::new(); x.hash(&mut hasher); hasher.finish() } struct FnvHasher(u64); impl FnvHasher { fn new() -> Self { FnvHasher(MAGIC_INIT) } } impl Hasher for FnvHasher { fn finish(&self) -> u64 { self.0 } fn write(&mut self, bytes: &[u8]) { for byte in bytes.iter() { self.0 ^= u64::from(*byte); self.0 = self.0.wrapping_mul(0x0100_0000_01b3); } } } ================================================ FILE: src/common/mod.rs ================================================ pub mod clipboard; pub mod deps; pub mod fs; pub mod git; pub mod hash; pub mod shell; pub mod terminal; pub mod url; ================================================ FILE: src/common/shell.rs ================================================ use crate::prelude::*; use clap::ValueEnum; use std::process::Command; use thiserror::Error; pub const EOF: &str = "NAVIEOF"; #[derive(Debug, Clone, ValueEnum)] pub enum Shell { Bash, Zsh, Fish, Elvish, Nushell, Powershell, } #[derive(Error, Debug)] #[error("Failed to spawn child process `bash` to execute `{command}`")] pub struct ShellSpawnError { command: String, #[source] source: anyhow::Error, } impl ShellSpawnError { pub fn new(command: impl Into, source: SourceError) -> Self where SourceError: std::error::Error + Sync + Send + 'static, { ShellSpawnError { command: command.into(), source: source.into(), } } } pub fn out() -> Command { let words_str = CONFIG.shell(); let mut words_vec = shellwords::split(&words_str).expect("empty shell command"); let mut words = words_vec.iter_mut(); let first_cmd = words.next().expect("absent shell binary"); let mut cmd = Command::new(first_cmd); cmd.args(words); let dash_c = if words_str.contains("cmd.exe") { "/c" } else { "-c" }; cmd.arg(dash_c); cmd } ================================================ FILE: src/common/terminal.rs ================================================ use crate::prelude::*; use crossterm::style; use crossterm::terminal; use std::process::Command; const FALLBACK_WIDTH: u16 = 80; fn width_with_shell_out() -> Result { let output = if cfg!(target_os = "macos") { Command::new("stty") .arg("-f") .arg("/dev/stderr") .arg("size") .stderr(Stdio::inherit()) .output()? } else { Command::new("stty") .arg("size") .arg("-F") .arg("/dev/stderr") .stderr(Stdio::inherit()) .output()? }; if let Some(0) = output.status.code() { let stdout = String::from_utf8(output.stdout).expect("Invalid utf8 output from stty"); let mut data = stdout.split_whitespace(); data.next(); return data .next() .expect("Not enough data") .parse::() .map_err(|_| anyhow!("Invalid width")); } Err(anyhow!("Invalid status code")) } pub fn width() -> u16 { if let Ok((w, _)) = terminal::size() { w } else { width_with_shell_out().unwrap_or(FALLBACK_WIDTH) } } pub fn parse_ansi(ansi: &str) -> Option { style::Color::parse_ansi(&format!("5;{ansi}")) } #[derive(Debug, Clone)] pub struct Color(#[allow(unused)] pub style::Color); // suppress warning: field `0` is never read. impl FromStr for Color { type Err = &'static str; fn from_str(ansi: &str) -> Result { if let Some(c) = parse_ansi(ansi) { Ok(Color(c)) } else { Err("Invalid color") } } } ================================================ FILE: src/common/url.rs ================================================ use crate::common::shell::{self, ShellSpawnError}; use crate::prelude::*; use anyhow::Result; use shell::EOF; pub fn open(args: Vec) -> Result<()> { let url = args .into_iter() .next() .ok_or_else(|| anyhow!("No URL specified"))?; let code = r#" exst() { type "$1" &>/dev/null } _open_url() { local -r url="$1" if exst xdg-open; then xdg-open "$url" &disown elif exst open; then echo "$url" | xargs -I% open "%" else exit 55 fi }"#; let cmd = format!( r#"{code} read -r -d '' url <<'{EOF}' {url} {EOF} _open_url "$url""#, ); shell::out() .arg(cmd.as_str()) .spawn() .map_err(|e| ShellSpawnError::new(cmd, e))? .wait()?; Ok(()) } ================================================ FILE: src/config/cli.rs ================================================ use crate::commands; use crate::finder::FinderChoice; use clap::{crate_version, Parser, Subcommand}; #[derive(Debug, Parser)] #[command(after_help = "\x1b[0;33mMORE INFO:\x1b[0;0m Please refer to \x1b[0;32mhttps://github.com/denisidoro/navi\x1b[0;0m \x1b[0;33mENVIRONMENT VARIABLES:\x1b[0m \x1b[0;32mNAVI_CONFIG\x1b[0;0m # path to config file \x1b[0;32mNAVI_CONFIG_YAML\x1b[0;0m # config file content \x1b[0;33mFEATURE STABILITY:\x1b[0m \x1b[0;32mexperimental\x1b[0;0m # may be removed or changed at any time \x1b[0;32mdeprecated\x1b[0;0m # may be removed in 3 months after first being deprecated \x1b[0;33mCOMMON NAVI COMMANDS:\x1b[0m Run \x1b[0;32mnavi fn welcome\x1b[0;0m to browse the cheatsheet for navi itself \x1b[0;33mEXAMPLES:\x1b[0m navi # default behavior navi fn welcome # show cheatsheets for navi itself navi --print # doesn't execute the snippet navi --tldr docker # search for docker cheatsheets using tldr navi --cheatsh docker # search for docker cheatsheets using cheatsh navi --path '/some/dir:/other/dir' # use .cheat files from custom paths navi --query git # filter results by \"git\" navi --query 'create db' --best-match # autoselect the snippet that best matches a query db=my navi --query 'create db' --best-match # same, but set the value for the variable navi repo add denisidoro/cheats # import cheats from a git repository eval \"$(navi widget zsh)\" # load the zsh widget navi --finder 'skim' # set skim as finder, instead of fzf navi --fzf-overrides '--with-nth 1,2' # show only the comment and tag columns navi --fzf-overrides '--no-select-1' # prevent autoselection in case of single line navi --fzf-overrides-var '--no-select-1' # same, but for variable selection navi --fzf-overrides '--nth 1,2' # only consider the first two columns for search navi --fzf-overrides '--no-exact' # use looser search algorithm navi --tag-rules='git,!checkout' # show non-checkout git snippets only")] #[clap(version = crate_version!())] pub(super) struct ClapConfig { /// Colon-separated list of paths containing .cheat files #[arg(short, long)] pub path: Option, /// Instead of executing a snippet, prints it to stdout #[arg(long)] #[cfg(not(feature = "disable-command-execution"))] pub print: bool, /// Returns the best match #[arg(long)] pub best_match: bool, /// Prevents variable interpolation #[arg(long)] pub prevent_interpolation: bool, /// Searches for cheatsheets using the tldr-pages repository #[arg(long)] pub tldr: Option, /// [Experimental] Comma-separated list that acts as filter for tags. Parts starting with ! represent negation #[arg(long)] pub tag_rules: Option, /// Searches for cheatsheets using the cheat.sh repository #[arg(long)] pub cheatsh: Option, /// Prepopulates the search field #[arg(short, long, allow_hyphen_values = true)] pub query: Option, /// Finder overrides for snippet selection #[arg(long, allow_hyphen_values = true)] pub fzf_overrides: Option, /// Finder overrides for variable selection #[arg(long, allow_hyphen_values = true)] pub fzf_overrides_var: Option, /// Finder application to use #[arg(long, ignore_case = true)] pub finder: Option, #[command(subcommand)] pub cmd: Option, } impl ClapConfig { pub fn new() -> Self { Self::parse() } } // #[derive(Subcommand, Debug, Clone, Runnable, HasDeps)] #[derive(Subcommand, Debug, Clone)] pub enum Command { /// [Experimental] Calls internal functions Fn(commands::func::Input), /// Manages cheatsheet repositories #[cfg(not(feature = "disable-repo-management"))] Repo(commands::repo::Input), /// Used for fzf's preview window when selecting snippets #[command(hide = true)] Preview(commands::preview::Input), /// Used for fzf's preview window when selecting variable suggestions #[command(hide = true)] PreviewVar(commands::preview::var::Input), /// Used for fzf's preview window when selecting variable suggestions #[command(hide = true)] PreviewVarStdin(commands::preview::var_stdin::Input), /// Outputs shell widget source code Widget(commands::shell::Input), /// Shows info Info(commands::info::Input), } #[derive(Debug)] pub enum Source { Filesystem(Option), Tldr(String), Cheats(String), Welcome, } pub enum Action { Print, Execute, } ================================================ FILE: src/config/env.rs ================================================ use crate::env_var; use crate::finder::FinderChoice; use crate::prelude::*; #[derive(Debug)] pub struct EnvConfig { pub config_yaml: Option, pub config_path: Option, pub path: Option, pub finder: Option, pub fzf_overrides: Option, pub fzf_overrides_var: Option, } impl EnvConfig { pub fn new() -> Self { Self { config_yaml: env_var::get(env_var::CONFIG_YAML).ok(), config_path: env_var::get(env_var::CONFIG).ok(), path: env_var::get(env_var::PATH).ok(), finder: env_var::get(env_var::FINDER) .ok() .and_then(|x| FinderChoice::from_str(&x).ok()), fzf_overrides: env_var::get(env_var::FZF_OVERRIDES).ok(), fzf_overrides_var: env_var::get(env_var::FZF_OVERRIDES_VAR).ok(), } } } ================================================ FILE: src/config/mod.rs ================================================ mod cli; mod env; mod yaml; use crate::commands::func::Func; use crate::finder::FinderChoice; use crate::prelude::debug; pub use cli::*; use crossterm::style::Color; use env::EnvConfig; use yaml::YamlConfig; lazy_static! { pub static ref CONFIG: Config = Config::new(); } #[derive(Debug)] pub struct Config { yaml: YamlConfig, clap: ClapConfig, env: EnvConfig, } impl Config { pub fn new() -> Self { let env = EnvConfig::new(); let yaml = YamlConfig::get(&env).unwrap_or_else(|e| { eprintln!("Error parsing config file: {e}"); eprintln!("Fallbacking to default one..."); eprintln!(); YamlConfig::default() }); let clap = ClapConfig::new(); Self { yaml, clap, env } } pub fn best_match(&self) -> bool { self.clap.best_match } pub fn prevent_interpolation(&self) -> bool { self.clap.prevent_interpolation } pub fn cmd(&self) -> Option<&Command> { self.clap.cmd.as_ref() } pub fn source(&self) -> Source { if let Some(query) = self.clap.tldr.clone() { Source::Tldr(query) } else if let Some(query) = self.clap.cheatsh.clone() { Source::Cheats(query) } else if let Some(Command::Fn(input)) = self.cmd() { if let Func::Welcome = input.func { Source::Welcome } else { Source::Filesystem(self.path()) } } else { Source::Filesystem(self.path()) } } pub fn path(&self) -> Option { if self.clap.path.is_some() { debug!("CLAP PATH: {}", self.clap.path.as_ref().unwrap()); } self.clap .path .clone() .or_else(|| { if self.env.path.is_some() { debug!("ENV PATH: {}", self.env.path.as_ref().unwrap()); } self.env.path.clone() }) .or_else(|| { let p = self.yaml.cheats.paths.clone(); if p.is_empty() { None } else { debug!("MULTIPLE YAML PATH: {}", p.as_slice().join(",")); Some(p.join(crate::filesystem::JOIN_SEPARATOR)) } }) .or_else(|| { if self.yaml.cheats.path.is_some() { debug!( "DEPRECATED UNIQUE YAML PATH: {}", self.yaml.cheats.path.as_ref().unwrap() ); } self.yaml.cheats.path.clone() }) .or_else(|| { debug!("No specific path given!"); None }) } pub fn finder(&self) -> FinderChoice { self.clap .finder .or(self.env.finder) .unwrap_or(self.yaml.finder.command) } pub fn fzf_overrides(&self) -> Option { self.clap .fzf_overrides .clone() .or_else(|| self.env.fzf_overrides.clone()) .or_else(|| self.yaml.finder.overrides.clone()) } pub fn fzf_overrides_var(&self) -> Option { self.clap .fzf_overrides_var .clone() .or_else(|| self.env.fzf_overrides_var.clone()) .or_else(|| self.yaml.finder.overrides_var.clone()) } pub fn delimiter_var(&self) -> Option { self.yaml.finder.delimiter_var.clone() } pub fn tealdeer(&self) -> bool { self.yaml.client.tealdeer } pub fn shell(&self) -> String { self.yaml.shell.command.clone() } pub fn finder_shell(&self) -> String { self.yaml .shell .finder_command .clone() .unwrap_or_else(|| self.yaml.shell.command.clone()) } pub fn tag_rules(&self) -> Option { self.clap .tag_rules .clone() .or_else(|| self.yaml.search.tags.clone()) } pub fn tag_color(&self) -> Color { self.yaml.style.tag.color.get() } pub fn comment_color(&self) -> Color { self.yaml.style.comment.color.get() } pub fn snippet_color(&self) -> Color { self.yaml.style.snippet.color.get() } pub fn tag_width_percentage(&self) -> u16 { self.yaml.style.tag.width_percentage } pub fn comment_width_percentage(&self) -> u16 { self.yaml.style.comment.width_percentage } pub fn snippet_width_percentage(&self) -> u16 { self.yaml.style.snippet.width_percentage } pub fn tag_min_width(&self) -> u16 { self.yaml.style.tag.min_width } pub fn comment_min_width(&self) -> u16 { self.yaml.style.comment.min_width } pub fn snippet_min_width(&self) -> u16 { self.yaml.style.snippet.min_width } #[cfg(feature = "disable-command-execution")] fn print(&self) -> bool { true } #[cfg(not(feature = "disable-command-execution"))] fn print(&self) -> bool { self.clap.print } pub fn action(&self) -> Action { if self.print() { Action::Print } else { Action::Execute } } pub fn get_query(&self) -> Option { let q = self.clap.query.clone(); if q.is_some() { return q; } if self.best_match() { match self.source() { Source::Tldr(q) => Some(q), Source::Cheats(q) => Some(q), _ => Some(String::from("")), } } else { None } } } impl Default for Config { fn default() -> Self { Self::new() } } ================================================ FILE: src/config/yaml.rs ================================================ use super::env::EnvConfig; use crate::common::fs; use crate::filesystem::default_config_pathbuf; use crate::finder::FinderChoice; use crate::prelude::*; use crossterm::style::Color as TerminalColor; use serde::de; #[derive(Deserialize, Debug)] pub struct Color(#[serde(deserialize_with = "color_deserialize")] TerminalColor); impl Color { pub fn get(&self) -> TerminalColor { self.0 } } fn color_deserialize<'de, D>(deserializer: D) -> Result where D: de::Deserializer<'de>, { let s: String = Deserialize::deserialize(deserializer)?; TerminalColor::try_from(s.as_str()) .map_err(|_| de::Error::custom(format!("Failed to deserialize color: {s}"))) } #[derive(Deserialize, Debug)] #[serde(default)] pub struct ColorWidth { pub color: Color, pub width_percentage: u16, pub min_width: u16, } #[derive(Deserialize, Debug)] #[serde(default)] pub struct Style { pub tag: ColorWidth, pub comment: ColorWidth, pub snippet: ColorWidth, } #[derive(Deserialize, Debug)] #[serde(default)] pub struct Finder { #[serde(deserialize_with = "finder_deserialize")] pub command: FinderChoice, pub overrides: Option, pub overrides_var: Option, pub delimiter_var: Option, } fn finder_deserialize<'de, D>(deserializer: D) -> Result where D: de::Deserializer<'de>, { let s: String = Deserialize::deserialize(deserializer)?; FinderChoice::from_str(s.to_lowercase().as_str()) .map_err(|_| de::Error::custom(format!("Failed to deserialize finder: {s}"))) } #[derive(Deserialize, Default, Debug)] #[serde(default)] pub struct Cheats { pub path: Option, pub paths: Vec, } #[derive(Deserialize, Default, Debug)] #[serde(default)] pub struct Search { pub tags: Option, } #[derive(Deserialize, Debug)] #[serde(default)] pub struct Shell { pub command: String, pub finder_command: Option, } #[derive(Deserialize, Debug)] #[serde(default)] #[derive(Default)] pub struct Client { pub tealdeer: bool, } #[derive(Deserialize, Debug)] #[serde(default)] pub struct YamlConfig { pub style: Style, pub finder: Finder, pub cheats: Cheats, pub search: Search, pub shell: Shell, pub client: Client, pub source: String, // <= The source of the current configuration } impl YamlConfig { fn from_str(text: &str) -> Result { serde_yaml::from_str(text).map_err(|e| e.into()) } fn from_path(path: &Path) -> Result { let file = fs::open(path)?; let reader = BufReader::new(file); serde_yaml::from_reader(reader).map_err(|e| e.into()) } pub fn get(env: &EnvConfig) -> Result { if let Some(yaml) = env.config_yaml.as_ref() { // We're getting the configuration from the environment variable `NAVI_CONFIG_YAML` let mut cfg = Self::from_str(yaml)?; cfg.source = "ENV_NAVI_CONFIG_YAML".to_string(); return Ok(cfg); } if let Some(path_str) = env.config_path.as_ref() { // We're getting the configuration from a file given in the environment variable 'NAVI_CONFIG' let p = PathBuf::from(path_str); let mut cfg = YamlConfig::from_path(&p)?; cfg.source = "ENV_NAVI_CONFIG".to_string(); return Ok(cfg); } if let Ok(p) = default_config_pathbuf() { // We're getting the configuration from the default path if p.exists() { let mut cfg = YamlConfig::from_path(&p)?; cfg.source = "DEFAULT_CONFIG_FILE".to_string(); return Ok(cfg); } } // As no configuration has been found, we set the YAML configuration // to be its default (built-in) value. Ok(YamlConfig::default()) } } impl Default for ColorWidth { fn default() -> Self { Self { color: Color(TerminalColor::Blue), width_percentage: 26, min_width: 20, } } } impl Default for Style { fn default() -> Self { Self { tag: ColorWidth { color: Color(TerminalColor::Cyan), width_percentage: 26, min_width: 20, }, comment: ColorWidth { color: Color(TerminalColor::Blue), width_percentage: 42, min_width: 45, }, snippet: Default::default(), } } } impl Default for Finder { fn default() -> Self { Self { command: FinderChoice::Fzf, overrides: None, overrides_var: None, delimiter_var: None, } } } impl Default for Shell { fn default() -> Self { Self { command: "bash".to_string(), finder_command: None, } } } impl Default for YamlConfig { fn default() -> Self { Self { style: Default::default(), finder: Default::default(), cheats: Default::default(), search: Default::default(), shell: Default::default(), client: Default::default(), source: "BUILT-IN".to_string(), } } } ================================================ FILE: src/deser/mod.rs ================================================ use crate::prelude::*; use unicode_width::UnicodeWidthStr; pub mod raycast; pub mod terminal; const NEWLINE_ESCAPE_CHAR: char = '\x15'; pub const LINE_SEPARATOR: &str = " \x15 "; lazy_static! { pub static ref NEWLINE_REGEX: Regex = Regex::new(r"\\\s+").expect("Invalid regex"); pub static ref VAR_REGEX: Regex = Regex::new(r"\\?<(\w[\w\d\-_]*)>").expect("Invalid regex"); } pub fn with_new_lines(txt: String) -> String { txt.replace(LINE_SEPARATOR, "\n") } pub fn fix_newlines(txt: &str) -> String { if txt.contains(NEWLINE_ESCAPE_CHAR) { (*NEWLINE_REGEX) .replace_all(txt.replace(LINE_SEPARATOR, " ").as_str(), "") .to_string() } else { txt.to_string() } } fn limit_str(text: &str, length: usize) -> String { let len = UnicodeWidthStr::width(text); if len <= length { format!("{}{}", text, " ".repeat(length - len)) } else { let mut new_length = length; let mut actual_length = 9999; let mut txt = text.to_owned(); while actual_length >= length { txt = txt.chars().take(new_length - 1).collect::(); actual_length = UnicodeWidthStr::width(txt.as_str()); new_length -= 1; } format!("{}…{}", txt, " ".repeat(length - actual_length - 1)) } } ================================================ FILE: src/deser/raycast.rs ================================================ use super::*; use crate::structures::item::Item; const FIELD_SEP_ESCAPE_CHAR: char = '\x16'; pub fn write(item: &Item) -> String { format!( "{hash}{delimiter}{tags}{delimiter}{comment}{delimiter}{icon}{delimiter}{snippet}\n", hash = item.hash(), tags = item.tags, comment = item.comment, delimiter = FIELD_SEP_ESCAPE_CHAR, icon = item.icon.clone().unwrap_or_default(), snippet = &item.snippet.trim_end_matches(LINE_SEPARATOR), ) } pub fn read(line: &str) -> Result { let mut parts = line.split(FIELD_SEP_ESCAPE_CHAR); let hash: u64 = parts .next() .context("no hash")? .parse() .context("hash not a u64")?; let tags = parts.next().context("no tags")?.into(); let comment = parts.next().context("no comment")?.into(); let icon_str = parts.next().context("no icon")?; let snippet = parts.next().context("no snippet")?.into(); let icon = if icon_str.is_empty() { None } else { Some(icon_str.into()) }; let item = Item { tags, comment, icon, snippet, ..Default::default() }; if item.hash() != hash { dbg!(&item.hash()); dbg!(hash); Err(anyhow!("Incorrect hash")) } else { Ok(item) } } ================================================ FILE: src/deser/terminal.rs ================================================ use super::*; use crate::common::terminal; use crate::structures::item::Item; use crossterm::style::{style, Stylize}; use std::cmp::max; pub fn get_widths() -> (usize, usize, usize) { let width = terminal::width(); let tag_width_percentage = max( CONFIG.tag_min_width(), width * CONFIG.tag_width_percentage() / 100, ); let comment_width_percentage = max( CONFIG.comment_min_width(), width * CONFIG.comment_width_percentage() / 100, ); let snippet_width_percentage = max( CONFIG.snippet_min_width(), width * CONFIG.snippet_width_percentage() / 100, ); ( usize::from(tag_width_percentage), usize::from(comment_width_percentage), usize::from(snippet_width_percentage), ) } pub const DELIMITER: &str = r" ⠀"; lazy_static! { pub static ref COLUMN_WIDTHS: (usize, usize, usize) = get_widths(); } pub fn write(item: &Item) -> String { let (tag_width_percentage, comment_width_percentage, snippet_width_percentage) = *COLUMN_WIDTHS; format!( "{tags_short}{delimiter}{comment_short}{delimiter}{snippet_short}{delimiter}{tags}{delimiter}{comment}{delimiter}{snippet}{delimiter}{file_index}{delimiter}\n", tags_short = style(limit_str(&item.tags, tag_width_percentage)).with(CONFIG.tag_color()), comment_short = style(limit_str(&item.comment, comment_width_percentage)).with(CONFIG.comment_color()), snippet_short = style(limit_str(&fix_newlines(&item.snippet), snippet_width_percentage)).with(CONFIG.snippet_color()), tags = item.tags, comment = item.comment, delimiter = DELIMITER, snippet = &item.snippet.trim_end_matches(LINE_SEPARATOR), file_index = item.file_index.unwrap_or(0), ) } pub fn read(raw_snippet: &str, is_single: bool) -> Result<(&str, Item)> { let mut lines = raw_snippet.split('\n'); let key = if is_single { "enter" } else { lines .next() .context("Key was promised but not present in `selections`")? }; let mut parts = lines .next() .context("No more parts in `selections`")? .split(DELIMITER) .skip(3); let tags = parts.next().unwrap_or("").into(); let comment = parts.next().unwrap_or("").into(); let snippet = parts.next().unwrap_or("").into(); let file_index = parts.next().unwrap_or("").parse().ok(); let item = Item { tags, comment, snippet, file_index, ..Default::default() }; Ok((key, item)) } ================================================ FILE: src/env_var.rs ================================================ use crate::prelude::*; pub use env::remove_var as remove; pub use env::set_var as set; pub use env::var as get; use std::env; pub const PREVIEW_INITIAL_SNIPPET: &str = "NAVI_PREVIEW_INITIAL_SNIPPET"; pub const PREVIEW_TAGS: &str = "NAVI_PREVIEW_TAGS"; pub const PREVIEW_COMMENT: &str = "NAVI_PREVIEW_COMMENT"; pub const PREVIEW_COLUMN: &str = "NAVI_PREVIEW_COLUMN"; pub const PREVIEW_DELIMITER: &str = "NAVI_PREVIEW_DELIMITER"; pub const PREVIEW_MAP: &str = "NAVI_PREVIEW_MAP"; pub const PATH: &str = "NAVI_PATH"; pub const FZF_OVERRIDES: &str = "NAVI_FZF_OVERRIDES"; pub const FZF_OVERRIDES_VAR: &str = "NAVI_FZF_OVERRIDES_VAR"; pub const FINDER: &str = "NAVI_FINDER"; pub const CONFIG: &str = "NAVI_CONFIG"; pub const CONFIG_YAML: &str = "NAVI_CONFIG_YAML"; pub fn parse(varname: &str) -> Option { if let Ok(x) = env::var(varname) { x.parse::().ok() } else { None } } pub fn must_get(name: &str) -> String { if let Ok(v) = env::var(name) { v } else { panic!("{name} not set") } } pub fn escape(name: &str) -> String { name.replace('-', "_") } ================================================ FILE: src/filesystem.rs ================================================ pub use crate::common::fs::{create_dir, exe_string, read_lines, remove_dir}; use crate::env_var; use crate::parser::Parser; use crate::prelude::*; use crate::structures::fetcher; use etcetera::BaseStrategy; use regex::Regex; use std::cell::RefCell; use std::path::MAIN_SEPARATOR; use walkdir::WalkDir; /// Multiple paths are joint by a platform-specific separator. /// FIXME: it's actually incorrect to assume a path doesn't containing this separator #[cfg(target_family = "windows")] pub const JOIN_SEPARATOR: &str = ";"; #[cfg(not(target_family = "windows"))] pub const JOIN_SEPARATOR: &str = ":"; pub fn all_cheat_files(path: &Path) -> Vec { WalkDir::new(path) .follow_links(true) .into_iter() .filter_map(|e| e.ok()) .map(|e| e.path().to_str().unwrap_or("").to_string()) .filter(|e| e.ends_with(".cheat") || e.ends_with(".cheat.md")) .collect::>() } fn paths_from_path_param(env_var: &str) -> impl Iterator { env_var.split(JOIN_SEPARATOR).filter(|folder| folder != &"") } fn compiled_default_path(path: Option<&str>) -> Option { match path { Some(path) => { let path = if path.contains(MAIN_SEPARATOR) { path.split(MAIN_SEPARATOR).next().unwrap() } else { path }; let path = Path::new(path); if path.exists() { Some(path.to_path_buf()) } else { None } } None => None, } } pub fn default_cheat_pathbuf() -> Result { let mut pathbuf = get_data_dir_by_platform()?; pathbuf.push("navi"); pathbuf.push("cheats"); if pathbuf.exists() { if let Some(path) = compiled_default_path(option_env!("NAVI_PATH")) { pathbuf = path; } } Ok(pathbuf) } pub fn default_config_pathbuf() -> Result { let mut pathbuf = get_config_dir_by_platform()?; pathbuf.push("navi"); pathbuf.push("config.yaml"); if !pathbuf.exists() { if let Some(path) = compiled_default_path(option_env!("NAVI_CONFIG")) { pathbuf = path; } } Ok(pathbuf) } pub fn cheat_paths(path: Option) -> Result { if let Some(p) = path { Ok(p) } else { Ok(default_cheat_pathbuf()?.to_string()) } } //////////////////////////////////////////////////////////////////////////////////////////////////// // // Here are other functions, unrelated to CLI commands (or at least not directly related) // //////////////////////////////////////////////////////////////////////////////////////////////////// /// Returns the data dir computed for each platform. /// /// We are currently handling two cases: When the platform is `macOS` and when the platform isn't (including `Windows` and `Linux/Unix` platforms) fn get_data_dir_by_platform() -> Result { if cfg!(target_os = "macos") { let base_dirs = etcetera::base_strategy::Apple::new()?; Ok(base_dirs.data_dir()) } else { let base_dirs = etcetera::choose_base_strategy()?; Ok(base_dirs.data_dir()) } } /// Returns the config dir computed for each platform. /// /// We are currently handling two cases: When the platform is `macOS` and when the platform isn't (including `Windows` and `Linux/Unix` platforms) fn get_config_dir_by_platform() -> Result { if cfg!(target_os = "macos") { let base_dirs = etcetera::base_strategy::Apple::new()?; Ok(base_dirs.config_dir()) } else { let base_dirs = etcetera::choose_base_strategy()?; Ok(base_dirs.config_dir()) } } pub fn tmp_pathbuf() -> Result { let mut root = default_cheat_pathbuf()?; root.push("tmp"); Ok(root) } fn interpolate_paths(paths: String) -> String { let re = Regex::new(r#"\$\{?[a-zA-Z_][a-zA-Z_0-9]*"#).unwrap(); let mut newtext = paths.to_string(); for capture in re.captures_iter(&paths) { if let Some(c) = capture.get(0) { let varname = c.as_str().replace(['$', '{', '}'], ""); if let Ok(replacement) = &env_var::get(&varname) { newtext = newtext .replace(&format!("${varname}"), replacement) .replace(&format!("${{{varname}}}"), replacement); } } } newtext } #[derive(Debug)] pub struct Fetcher { path: Option, files: RefCell>, } impl Fetcher { pub fn new(path: Option) -> Self { Self { path, files: Default::default(), } } } impl fetcher::Fetcher for Fetcher { fn fetch(&self, parser: &mut Parser) -> Result { let mut found_something = false; let path = self.path.clone(); let paths = cheat_paths(path); if paths.is_err() { return Ok(false); }; let paths = paths.expect("Unable to get paths"); let interpolated_paths = interpolate_paths(paths); let folders = paths_from_path_param(&interpolated_paths); let home_regex = Regex::new(r"^~").unwrap(); let home = etcetera::home_dir().ok(); // parser.filter = self.tag_rules.as_ref().map(|r| gen_lists(r.as_str())); for folder in folders { let interpolated_folder = match &home { Some(h) => home_regex.replace(folder, h.to_string_lossy()).to_string(), None => folder.to_string(), }; let folder_pathbuf = PathBuf::from(interpolated_folder); let cheat_files = all_cheat_files(&folder_pathbuf); debug!("read cheat files in `{folder_pathbuf:?}`: {cheat_files:#?}"); for file in cheat_files { self.files.borrow_mut().push(file.clone()); let index = self.files.borrow().len() - 1; let read_file_result = { let path = PathBuf::from(&file); let lines = read_lines(&path)?; parser.read_lines(lines, &file, Some(index)) }; if read_file_result.is_ok() && !found_something { found_something = true } } } debug!("FilesystemFetcher = {self:#?}"); Ok(found_something) } fn files(&self) -> Vec { self.files.borrow().clone() } } #[cfg(test)] mod tests { use super::*; /* TODO use crate::finder::structures::{Opts as FinderOpts, SuggestionType}; use crate::writer; use std::process::{Command, Stdio}; #[test] fn test_read_file() { let path = "tests/cheats/ssh.cheat"; let mut variables = VariableMap::new(); let mut child = Command::new("cat") .stdin(Stdio::piped()) .stdout(Stdio::null()) .spawn() .unwrap(); let child_stdin = child.stdin.as_mut().unwrap(); let mut visited_lines: HashSet = HashSet::new(); let mut writer: Box = Box::new(writer::terminal::Writer::new()); read_file( path, 0, &mut variables, &mut visited_lines, &mut *writer, child_stdin, ) .unwrap(); let expected_suggestion = ( r#" echo -e "$(whoami)\nroot" "#.to_string(), Some(FinderOpts { header_lines: 0, column: None, delimiter: None, suggestion_type: SuggestionType::SingleSelection, ..Default::default() }), ); let actual_suggestion = variables.get_suggestion("ssh", "user"); assert_eq!(Some(&expected_suggestion), actual_suggestion); } */ #[test] fn splitting_of_dirs_param_may_not_contain_empty_items() { // 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. let given_path_config = "SOME_PATH:ANOTHER_PATH:"; let found_paths = paths_from_path_param(given_path_config); let mut expected_paths = vec!["SOME_PATH", "ANOTHER_PATH"].into_iter(); for found in found_paths { let expected = expected_paths.next().unwrap(); assert_eq!(found, expected) } } #[test] fn test_default_config_pathbuf() { let base_dirs = etcetera::choose_base_strategy().expect("could not determine base directories"); let expected = { let mut e = base_dirs.config_dir(); e.push("navi"); e.push("config.yaml"); e.to_string_lossy().to_string() }; let config = default_config_pathbuf().expect("could not find default config path"); assert_eq!(expected, config.to_string_lossy().to_string()) } #[test] fn test_default_cheat_pathbuf() { let base_dirs = etcetera::choose_base_strategy().expect("could not determine base directories"); let expected = { let mut e = base_dirs.data_dir(); e.push("navi"); e.push("cheats"); e.to_string_lossy().to_string() }; let cheats = default_cheat_pathbuf().expect("could not find default config path"); assert_eq!(expected, cheats.to_string_lossy().to_string()) } #[test] #[cfg(target_family = "windows")] fn multiple_paths() { let p = r#"C:\Users\Administrator\AppData\Roaming\navi\config.yaml"#; let paths = &[p; 2].join(JOIN_SEPARATOR); assert_eq!(paths_from_path_param(paths).collect::>(), [p; 2]); } } ================================================ FILE: src/finder/mod.rs ================================================ use crate::deser; use crate::prelude::*; use std::io::Write; use std::process::{self, Output}; use std::process::{Command, Stdio}; pub mod structures; use clap::ValueEnum; pub use post::process; use structures::Opts; use structures::SuggestionType; const MIN_FZF_VERSION_MAJOR: u32 = 0; const MIN_FZF_VERSION_MINOR: u32 = 23; const MIN_FZF_VERSION_PATCH: u32 = 1; mod post; #[derive(Debug, Clone, Copy, Deserialize, ValueEnum)] pub enum FinderChoice { Fzf, Skim, } impl FromStr for FinderChoice { type Err = &'static str; fn from_str(s: &str) -> Result { match s { "fzf" => Ok(FinderChoice::Fzf), "skim" => Ok(FinderChoice::Skim), _ => Err("no match"), } } } fn parse(out: Output, opts: Opts) -> Result { let text = match out.status.code() { Some(0) | Some(1) | Some(2) => { String::from_utf8(out.stdout).context("Invalid utf8 received from finder")? } Some(130) => process::exit(130), _ => { let err = String::from_utf8(out.stderr) .unwrap_or_else(|_| "".to_owned()); panic!("External command failed:\n {err}") } }; let output = post::parse_output_single(text, opts.suggestion_type)?; post::process(output, opts.column, opts.delimiter.as_deref(), opts.map) } impl FinderChoice { fn check_fzf_version() -> Option<(u32, u32, u32)> { let output = Command::new("fzf").arg("--version").output().ok()?.stdout; let version_string = String::from_utf8(output).ok()?; let version_parts: Vec<_> = version_string.split('.').collect(); if version_parts.len() == 3 { let major = version_parts[0].parse().ok()?; let minor = version_parts[1].parse().ok()?; let patch = version_parts[2].split_whitespace().next()?.parse().ok()?; Some((major, minor, patch)) } else { None } } pub fn call(&self, finder_opts: Opts, stdin_fn: F) -> Result<(String, R)> where F: Fn(&mut dyn Write) -> Result, { let finder_str = match self { Self::Fzf => "fzf", Self::Skim => "sk", }; if let Self::Fzf = self { if let Some((major, minor, patch)) = Self::check_fzf_version() { if major == MIN_FZF_VERSION_MAJOR && minor < MIN_FZF_VERSION_MINOR && patch < MIN_FZF_VERSION_PATCH { eprintln!( "Warning: Fzf version {major}.{minor} does not support the preview window layout used by navi.", ); eprintln!( "Consider updating Fzf to a version >= {MIN_FZF_VERSION_MAJOR}.{MIN_FZF_VERSION_MINOR}.{MIN_FZF_VERSION_PATCH} or use a compatible layout.", ); process::exit(1); } } } let mut command = Command::new(finder_str); let opts = finder_opts.clone(); let preview_height = match self { FinderChoice::Skim => 3, _ => 2, }; let bindings = if opts.suggestion_type == SuggestionType::MultipleSelections { ",ctrl-r:toggle-all" } else { "" }; command.args([ "--preview", "", "--preview-window", format!("up:{preview_height}:nohidden").as_str(), "--delimiter", deser::terminal::DELIMITER.to_string().as_str(), "--ansi", "--bind", format!("ctrl-j:down,ctrl-k:up{bindings}").as_str(), "--exact", ]); if !opts.show_all_columns { command.args(["--with-nth", "1,2,3"]); } if !opts.prevent_select1 { if let Self::Fzf = self { command.arg("--select-1"); } } match opts.suggestion_type { SuggestionType::MultipleSelections => { command.arg("--multi"); } SuggestionType::Disabled => { if let Self::Fzf = self { command.args(["--print-query", "--no-select-1"]); }; } SuggestionType::SnippetSelection => { command.args(["--expect", "ctrl-y,ctrl-o,enter"]); } SuggestionType::SingleRecommendation => { command.args(["--print-query", "--expect", "tab,enter"]); } _ => {} } if let Some(p) = opts.preview { command.args(["--preview", &p]); } if let Some(q) = opts.query { command.args(["--query", &q]); } if let Some(f) = opts.filter { command.args(["--filter", &f]); } if let Some(d) = opts.delimiter { command.args(["--delimiter", &d]); } if let Some(h) = opts.header { command.args(["--header", &h]); } if let Some(p) = opts.prompt { command.args(["--prompt", &p]); } if let Some(pw) = opts.preview_window { command.args(["--preview-window", &pw]); } if opts.header_lines > 0 { command.args(["--header-lines", format!("{}", opts.header_lines).as_str()]); } if let Some(o) = opts.overrides { shellwords::split(&o)? .into_iter() .filter(|s| !s.is_empty()) .for_each(|s| { command.arg(s); }); } command .env("SHELL", CONFIG.finder_shell()) .stdin(Stdio::piped()) .stdout(Stdio::piped()); debug!(cmd = ?command); let child = command.spawn(); let mut child = match child { Ok(x) => x, Err(_) => { let repo = match self { Self::Fzf => "https://github.com/junegunn/fzf", Self::Skim => "https://github.com/lotabout/skim", }; eprintln!( "navi was unable to call {cmd}. Please make sure it's correctly installed. Refer to {repo} for more info.", cmd = &finder_str, repo = repo ); process::exit(33) } }; let stdin = child .stdin .as_mut() .ok_or_else(|| anyhow!("Unable to acquire stdin of finder"))?; let mut writer: Box<&mut dyn Write> = Box::new(stdin); let return_value = stdin_fn(&mut writer).context("Failed to pass data to finder")?; let out = child.wait_with_output().context("Failed to wait for finder")?; let output = parse(out, finder_opts).context("Unable to get output")?; Ok((output, return_value)) } } ================================================ FILE: src/finder/post.rs ================================================ use crate::common::shell; use crate::finder::structures::SuggestionType; use crate::prelude::*; use shell::EOF; use std::process::Stdio; fn apply_map(text: String, map_fn: Option) -> Result { if let Some(m) = map_fn { let cmd = if CONFIG.shell().contains("fish") { format!(r#"printf "%s" "{text}" | {m}"#) } else { format!( r#"_navi_input() {{ cat <<'{EOF}' {text} {EOF} }} _navi_map_fn() {{ {m} }} _navi_nonewline() {{ printf "%s" "$(cat)" }} _navi_input | _navi_map_fn | _navi_nonewline"# ) }; let output = shell::out() .arg(cmd.as_str()) .stderr(Stdio::inherit()) .output() .context("Failed to execute map function")?; String::from_utf8(output.stdout).context("Invalid utf8 output for map function") } else { Ok(text) } } fn get_column(text: String, column: Option, delimiter: Option<&str>) -> String { if let Some(c) = column { let mut result = String::from(""); let re = regex::Regex::new(delimiter.unwrap_or(r"\s\s+")).expect("Invalid regex"); for line in text.split('\n') { if (line).is_empty() { continue; } let mut parts = re.split(line).skip((c - 1) as usize); if !result.is_empty() { result.push('\n'); } result.push_str(parts.next().unwrap_or("")); } result } else { text } } pub fn process( text: String, column: Option, delimiter: Option<&str>, map_fn: Option, ) -> Result { apply_map(get_column(text, column, delimiter), map_fn) } pub(super) fn parse_output_single(mut text: String, suggestion_type: SuggestionType) -> Result { Ok(match suggestion_type { SuggestionType::SingleSelection => text .lines() .next() .context("No sufficient data for single selection")? .to_string(), SuggestionType::MultipleSelections | SuggestionType::Disabled | SuggestionType::SnippetSelection => { let len = text.len(); if len > 1 { text.truncate(len - 1); } text } SuggestionType::SingleRecommendation => { let lines: Vec<&str> = text.lines().collect(); match (lines.first(), lines.get(1), lines.get(2)) { (Some(one), Some(termination), Some(two)) if *termination == "enter" || termination.is_empty() => { if two.is_empty() { (*one).to_string() } else { (*two).to_string() } } (Some(one), Some(termination), None) if *termination == "enter" || termination.is_empty() => { (*one).to_string() } (Some(one), Some(termination), _) if *termination == "tab" => (*one).to_string(), _ => "".to_string(), } } }) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_output1() { let text = "palo\n".to_string(); let output = parse_output_single(text, SuggestionType::SingleSelection).unwrap(); assert_eq!(output, "palo"); } #[test] fn test_parse_output2() { let text = "\nenter\npalo".to_string(); let output = parse_output_single(text, SuggestionType::SingleRecommendation).unwrap(); assert_eq!(output, "palo"); } #[test] fn test_parse_recommendation_output_1() { let text = "\nenter\npalo".to_string(); let output = parse_output_single(text, SuggestionType::SingleRecommendation).unwrap(); assert_eq!(output, "palo"); } #[test] fn test_parse_recommendation_output_2() { let text = "p\nenter\npalo".to_string(); let output = parse_output_single(text, SuggestionType::SingleRecommendation).unwrap(); assert_eq!(output, "palo"); } #[test] fn test_parse_recommendation_output_3() { let text = "peter\nenter\n".to_string(); let output = parse_output_single(text, SuggestionType::SingleRecommendation).unwrap(); assert_eq!(output, "peter"); } #[test] fn test_parse_output3() { let text = "p\ntab\npalo".to_string(); let output = parse_output_single(text, SuggestionType::SingleRecommendation).unwrap(); assert_eq!(output, "p"); } #[test] fn test_parse_snippet_request() { 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(); let output = parse_output_single(text, SuggestionType::SnippetSelection).unwrap(); 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 @ ⠀"); } } ================================================ FILE: src/finder/structures.rs ================================================ use crate::filesystem; use crate::prelude::*; #[derive(Debug, PartialEq, Clone)] pub struct Opts { pub query: Option, pub filter: Option, pub prompt: Option, pub preview: Option, pub preview_window: Option, pub overrides: Option, pub header_lines: u8, pub header: Option, pub suggestion_type: SuggestionType, pub delimiter: Option, pub column: Option, pub map: Option, pub prevent_select1: bool, pub show_all_columns: bool, } impl Default for Opts { fn default() -> Self { Self { query: None, filter: None, preview: None, preview_window: None, overrides: None, header_lines: 0, header: None, prompt: None, suggestion_type: SuggestionType::SingleSelection, column: None, delimiter: None, map: None, prevent_select1: true, show_all_columns: false, } } } impl Opts { pub fn snippet_default() -> Self { Self { suggestion_type: SuggestionType::SnippetSelection, overrides: CONFIG.fzf_overrides(), preview: Some(format!("{} preview {{}}", filesystem::exe_string())), prevent_select1: !CONFIG.best_match(), query: if CONFIG.best_match() { None } else { CONFIG.get_query() }, filter: if CONFIG.best_match() { CONFIG.get_query() } else { None }, ..Default::default() } } pub fn var_default() -> Self { Self { overrides: CONFIG.fzf_overrides_var(), suggestion_type: SuggestionType::SingleRecommendation, prevent_select1: false, delimiter: CONFIG.delimiter_var(), ..Default::default() } } } #[derive(Clone, Copy, Debug, PartialEq)] pub enum SuggestionType { /// finder will not print any suggestions Disabled, /// finder will only select one of the suggestions SingleSelection, /// finder will select multiple suggestions MultipleSelections, /// finder will select one of the suggestions or use the query SingleRecommendation, /// initial snippet selection SnippetSelection, } ================================================ FILE: src/lib.rs ================================================ #[macro_use] extern crate lazy_static; mod clients; mod commands; mod common; mod config; mod deser; mod env_var; mod filesystem; mod finder; mod parser; pub mod prelude; mod structures; mod welcome; mod libs { pub mod dns_common; } pub use {commands::handle, filesystem::default_config_pathbuf}; ================================================ FILE: src/libs/dns_common/component.rs ================================================ use crate::prelude::*; pub trait Component: Any + AsAny + Send + Sync {} pub trait AsAny: Any { fn as_any(&self) -> &dyn Any; fn as_mut_any(&mut self) -> &mut dyn Any; } ================================================ FILE: src/libs/dns_common/mod.rs ================================================ pub mod component; mod tracing; ================================================ FILE: src/libs/dns_common/tracing.rs ================================================ use crate::prelude::*; #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(deny_unknown_fields)] pub struct TracingConfig { pub time: bool, pub level: String, } ================================================ FILE: src/parser.rs ================================================ use crate::common::fs; use crate::deser; use crate::finder::structures::{Opts as FinderOpts, SuggestionType}; use crate::prelude::*; use crate::structures::cheat::VariableMap; use crate::structures::item::Item; use std::io::Write; lazy_static! { pub static ref VAR_LINE_REGEX: Regex = Regex::new(r"^\$\s*([^:]+):(.*)").expect("Invalid regex"); } fn parse_opts(text: &str) -> Result { let mut multi = false; let mut prevent_extra = false; let mut opts = FinderOpts::var_default(); let parts = shellwords::split(text).map_err(|_| anyhow!("Given options are missing a closing quote"))?; parts .into_iter() .filter(|part| { // We'll take parts in pairs of 2: (argument, value). Flags don't have a value tho, so we filter and handle them beforehand. match part.as_str() { "--multi" => { multi = true; false } "--prevent-extra" => { prevent_extra = true; false } "--expand" => { opts.map = Some(format!("{} fn map::expand", fs::exe_string())); false } _ => true, } }) .collect::>() .chunks(2) .try_for_each(|flag_and_value| { if let [flag, value] = flag_and_value { match flag.as_str() { "--headers" | "--header-lines" => { opts.header_lines = value .parse::() .context("Value for `--headers` is invalid u8")? } "--column" => { opts.column = Some( value .parse::() .context("Value for `--column` is invalid u8")?, ) } "--map" => opts.map = Some(value.to_string()), "--delimiter" => opts.delimiter = Some(value.to_string()), "--query" => opts.query = Some(value.to_string()), "--filter" => opts.filter = Some(value.to_string()), "--preview" => opts.preview = Some(value.to_string()), "--preview-window" => opts.preview_window = Some(value.to_string()), "--header" => opts.header = Some(value.to_string()), "--fzf-overrides" => opts.overrides = Some(value.to_string()), _ => (), } Ok(()) } else if let [flag] = flag_and_value { Err(anyhow!("No value provided for the flag `{}`", flag)) } else { unreachable!() // Chunking by 2 allows only for tuples of 1 or 2 items... } }) .context("Failed to parse finder options")?; let suggestion_type = match (multi, prevent_extra) { (true, _) => SuggestionType::MultipleSelections, // multi wins over prevent-extra (false, false) => SuggestionType::SingleRecommendation, (false, true) => SuggestionType::SingleSelection, }; opts.suggestion_type = suggestion_type; Ok(opts) } fn parse_variable_line(line: &str) -> Result<(&str, &str, Option)> { let caps = VAR_LINE_REGEX .captures(line) .ok_or_else(|| anyhow!("No variables, command, and options found in the line `{}`", line))?; let variable = caps .get(1) .ok_or_else(|| anyhow!("No variable captured in the line `{}`", line))? .as_str() .trim(); let mut command_plus_opts = caps .get(2) .ok_or_else(|| anyhow!("No command and options captured in the line `{}`", line))? .as_str() .split("---"); let command = command_plus_opts .next() .ok_or_else(|| anyhow!("No command captured in the line `{}`", line))?; let command_options = command_plus_opts.next().map(parse_opts).transpose()?; Ok((variable, command, command_options)) } fn without_prefix(line: &str) -> String { if line.len() > 2 { String::from(line[2..].trim()) } else { String::from("") } } #[derive(Clone, Default)] pub struct FilterOpts { pub allowlist: Vec, pub denylist: Vec, pub hash: Option, } pub struct Parser<'a> { pub variables: VariableMap, visited_lines: HashSet, filter: FilterOpts, writer: &'a mut dyn Write, write_fn: fn(&Item) -> String, } fn without_first(string: &str) -> String { string .char_indices() .next() .and_then(|(i, _)| string.get(i + 1..)) .expect("Should have at least one char") .to_string() } fn gen_lists(tag_rules: &str) -> FilterOpts { let words: Vec<_> = tag_rules.split(',').collect(); let allowlist = words .iter() .filter(|w| !w.starts_with('!')) .map(|w| w.to_string()) .collect(); let denylist = words .iter() .filter(|w| w.starts_with('!')) .map(|w| without_first(w)) .collect(); FilterOpts { allowlist, denylist, ..Default::default() } } impl<'a> Parser<'a> { pub fn new(writer: &'a mut dyn Write, is_terminal: bool) -> Self { let write_fn = if is_terminal { deser::terminal::write } else { deser::raycast::write }; let filter = match CONFIG.tag_rules() { Some(tr) => gen_lists(&tr), None => Default::default(), }; Self { variables: Default::default(), visited_lines: Default::default(), filter, write_fn, writer, } } pub fn set_hash(&mut self, hash: u64) { self.filter.hash = Some(hash) } fn write_cmd(&mut self, item: &Item) -> Result<()> { if item.comment.is_empty() || item.snippet.trim().is_empty() { return Ok(()); } let hash = item.hash(); if self.visited_lines.contains(&hash) { return Ok(()); } self.visited_lines.insert(hash); if !self.filter.denylist.is_empty() { for v in &self.filter.denylist { if item.tags.contains(v) { return Ok(()); } } } if !self.filter.allowlist.is_empty() { let mut should_allow = false; for v in &self.filter.allowlist { if item.tags.contains(v) { should_allow = true; break; } } if !should_allow { return Ok(()); } } if let Some(h) = self.filter.hash { if h != hash { return Ok(()); } } let write_fn = self.write_fn; self.writer .write_all(write_fn(item).as_bytes()) .context("Failed to write command to finder's stdin") } pub fn read_lines( &mut self, lines: impl Iterator>, id: &str, file_index: Option, ) -> Result<()> { let mut item = Item::new(file_index); let mut should_break = false; let mut variable_cmd = String::from(""); let mut inside_snippet: bool = false; for (line_nr, line_result) in lines.enumerate() { let line = line_result .with_context(|| format!("Failed to read line number {line_nr} in cheatsheet `{id}`"))?; if should_break { break; } // duplicate // if !item.tags.is_empty() && !item.comment.is_empty() {} // blank if line.is_empty() { if !item.snippet.is_empty() { item.snippet.push_str(deser::LINE_SEPARATOR); } } // tag else if line.starts_with('%') { should_break = self.write_cmd(&item).is_err(); item.snippet = String::from(""); item.tags = without_prefix(&line); } // dependency else if line.starts_with('@') { let tags_dependency = without_prefix(&line); self.variables.insert_dependency(&item.tags, &tags_dependency); } // raycast icon else if let Some(icon) = line.strip_prefix("; raycast.icon:") { item.icon = Some(icon.trim().into()); } // metacomment else if line.starts_with(';') { } // comment else if line.starts_with('#') { should_break = self.write_cmd(&item).is_err(); item.snippet = String::from(""); item.comment = without_prefix(&line); } // variable else if !variable_cmd.is_empty() || (line.starts_with('$') && line.contains(':')) && !inside_snippet { should_break = self.write_cmd(&item).is_err(); item.snippet = String::from(""); variable_cmd.push_str(line.trim_end_matches('\\')); if !line.ends_with('\\') { let full_variable_cmd = variable_cmd.clone(); let (variable, command, opts) = parse_variable_line(&full_variable_cmd).with_context(|| { format!( "Failed to parse variable line. See line number {} in cheatsheet `{}`", line_nr + 1, id ) })?; variable_cmd = String::from(""); self.variables .insert_suggestion(&item.tags, variable, (String::from(command), opts)); } } // markdown snippet else if line.starts_with("```") { inside_snippet = !inside_snippet; } // snippet else { if !item.snippet.is_empty() { item.snippet.push_str(deser::LINE_SEPARATOR); } item.snippet.push_str(&line); } } if !should_break { let _ = self.write_cmd(&item); } Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_variable_line() { let (variable, command, command_options) = parse_variable_line("$ user : echo -e \"$(whoami)\\nroot\" --- --prevent-extra").unwrap(); assert_eq!(command, " echo -e \"$(whoami)\\nroot\" "); assert_eq!(variable, "user"); let opts = command_options.unwrap(); assert_eq!(opts.header_lines, 0); assert_eq!(opts.column, None); assert_eq!(opts.delimiter, None); assert_eq!(opts.suggestion_type, SuggestionType::SingleSelection); } } ================================================ FILE: src/prelude.rs ================================================ pub use crate::common::deps::HasDeps; pub use crate::common::fs::ToStringExt; pub use crate::config::CONFIG; // TODO pub use crate::libs::dns_common; pub use anyhow::{anyhow, Context, Error, Result}; pub use regex::Regex; pub use serde::de::Deserializer; pub use serde::ser::Serializer; pub use serde::{Deserialize, Serialize}; pub use std::any::{Any, TypeId}; pub use std::collections::{HashMap, HashSet}; pub use std::convert::{TryFrom, TryInto}; pub use std::fs::File; pub use std::io::{BufRead, BufReader}; pub use std::path::{Path, PathBuf}; pub use std::process::Stdio; pub use std::str::FromStr; pub use std::sync::{Arc, Mutex, RwLock}; pub use tracing::{self, debug, error, event, info, instrument, span, subscriber, trace, warn}; pub trait Component: Any + AsAny + Send + Sync {} pub trait AsAny: Any { fn as_any(&self) -> &dyn Any; fn as_mut_any(&mut self) -> &mut dyn Any; } impl AsAny for T where T: Any, { fn as_any(&self) -> &dyn Any { self } fn as_mut_any(&mut self) -> &mut dyn Any { self } } pub trait Runnable { fn run(&self) -> Result<()>; } ================================================ FILE: src/structures/cheat.rs ================================================ use crate::common::hash::fnv; use crate::finder::structures::Opts; use crate::prelude::*; pub type Suggestion = (String, Option); #[derive(Clone, Default)] pub struct VariableMap { variables: HashMap>, dependencies: HashMap>, } impl VariableMap { pub fn insert_dependency(&mut self, tags: &str, tags_dependency: &str) { let k = fnv(&tags); if let Some(v) = self.dependencies.get_mut(&k) { v.push(fnv(&tags_dependency)); } else { let v: Vec = vec![fnv(&tags_dependency)]; self.dependencies.insert(k, v); } } pub fn insert_suggestion(&mut self, tags: &str, variable: &str, value: Suggestion) { let k1 = fnv(&tags); let k2 = String::from(variable); if let Some(m) = self.variables.get_mut(&k1) { m.insert(k2, value); } else { let mut m = HashMap::new(); m.insert(k2, value); self.variables.insert(k1, m); } } pub fn get_suggestion(&self, tags: &str, variable: &str) -> Option<&Suggestion> { let k = fnv(&tags); if let Some(vm) = self.variables.get(&k) { let res = vm.get(variable); if res.is_some() { return res; } } if let Some(dependency_keys) = self.dependencies.get(&k) { for dependency_key in dependency_keys { if let Some(vm) = self.variables.get(dependency_key) { let res = vm.get(variable); if res.is_some() { return res; } } } } None } } ================================================ FILE: src/structures/fetcher.rs ================================================ use crate::parser::Parser; use crate::prelude::*; pub trait Fetcher { fn fetch(&self, parser: &mut Parser) -> Result; fn files(&self) -> Vec { vec![] } } pub struct StaticFetcher { lines: Vec, } impl StaticFetcher { pub fn new(lines: Vec) -> Self { Self { lines } } } impl Fetcher for StaticFetcher { fn fetch(&self, parser: &mut Parser) -> Result { parser.read_lines(self.lines.clone().into_iter().map(Ok), "static", None)?; Ok(true) } } ================================================ FILE: src/structures/item.rs ================================================ use crate::common::hash::fnv; #[derive(Default, Debug)] pub struct Item { pub tags: String, pub comment: String, pub snippet: String, pub file_index: Option, pub icon: Option, } impl Item { pub fn new(file_index: Option) -> Self { Self { file_index, ..Default::default() } } pub fn hash(&self) -> u64 { fnv(&format!( "{}{}{}", &self.tags.trim(), &self.comment.trim(), &self.snippet.trim() )) } } ================================================ FILE: src/structures/mod.rs ================================================ pub mod cheat; pub mod fetcher; pub mod item; ================================================ FILE: src/welcome.rs ================================================ use crate::parser::Parser; use crate::prelude::*; use crate::structures::fetcher; pub fn populate_cheatsheet(parser: &mut Parser) -> Result<()> { let cheatsheet = include_str!("../docs/examples/cheatsheet/navi.cheat"); let lines = cheatsheet.split('\n').map(|s| Ok(s.to_string())); parser.read_lines(lines, "welcome", None)?; Ok(()) } pub struct Fetcher {} impl Fetcher { pub fn new() -> Self { Self {} } } impl fetcher::Fetcher for Fetcher { fn fetch(&self, parser: &mut Parser) -> Result { populate_cheatsheet(parser)?; Ok(true) } } ================================================ FILE: tests/cheats/more_cases.cheat ================================================ ; author: CI/CD % test, ci/cd # escape code + subshell echo -ne "\033]0;$(hostname)\007" # env var echo "$HOME" # multi + column myfn() { for i in $@; do echo -e "arg: $i\n" done } folders=($(echo "")) myfn "${folders[@]}" # second column: default delimiter echo " is cool" # second column: custom delimiter echo " is cool" # return multiple results: single words echo "I like these languages: "$(printf '%s' "" | tr '\n' ',' | sed 's/,/, /g')"" # return multiple results: multiple words echo "I like these examples: "$(printf '%s' "" | sed 's/^..*$/"&"/' | awk 1 ORS=', ' | sed 's/, $//')"" # multiple replacements -> "foo" echo " " # with preview cat "" # with map echo "" # empty echo "http://google.com?q=" # fzf ls / | fzf # echo description space # echo description blank # x echo description one character # map can be used to expand into multiple arguments for l in ; do echo "line: $l"; done # x echo # Concatenate pdf files files=($(echo "")) echo pdftk "${files[@]:-}" cat output $ files: echo 'file1.pdf file2.pdf file3.pdf' | tr ' ' '\n' --- --multi --fzf-overrides '--tac' $ x: echo '1 2 3' | tr ' ' '\n' $ y: echo 'a b c' | tr ' ' '\n' $ z: echo 'foo bar' | tr ' ' '\n' $ table_elem: echo -e '0 rust rust-lang.org\n1 clojure clojure.org' --- --column 2 $ table_elem2: echo -e '0;rust;rust-lang.org\n1;clojure;clojure.org' --- --column 2 --delimiter ';' $ multi_col: ls -la | awk '{print $1, $9}' --- --column 2 --delimiter '\s' --multi $ langs: echo 'clojure rust javascript' | tr ' ' '\n' --- --multi $ mapped: echo 'true false' | tr ' ' '\n' --- --map "grep -q t && echo 1 || echo 0" $ examples: echo -e 'foo bar\nlorem ipsum\ndolor sit' --- --multi $ multiword: echo -e 'foo bar\nlorem ipsum\ndolor sit\nbaz'i $ file: ls . --- --preview 'cat {}' --preview-window 'right:50%' $ phrases: echo -e "foo bar\nlorem ipsum\ndolor sit" --- --multi --map "navi fn map::expand" $ with_overrides: echo -e "foo bar\nlorem ipsum\ndolor sit" --- --fzf-overrides "--margin=15% --bind=ctrl-u:replace-query" # this should be displayed echo hi % # Without tag echo hi 1 2 3 ================================================ FILE: tests/cheats/ssh.cheat ================================================ % ssh # login to a server with a key and port ssh -i -p @ $ user : echo -e "$(whoami)\nroot" --- --prevent-extra ================================================ FILE: tests/config.yaml ================================================ style: tag: color: cyan width_percentage: 26 min_width: 20 comment: color: yellow width_percentage: 42 min_width: 45 snippet: color: white finder: command: fzf shell: finder_command: bash command: env BASH_ENV="${NAVI_HOME}/tests/helpers.sh" bash --norc --noprofile ================================================ FILE: tests/core.bash ================================================ #!/usr/bin/env bash # vim: filetype=sh source "${NAVI_HOME}/scripts/install" NEWLINE_CHAR="\036" PASSED=0 FAILED=0 SKIPPED=0 SUITE="" test::set_suite() { SUITE="$*" } test::success() { PASSED=$((PASSED+1)) log::success "Test passed!" } test::fail() { FAILED=$((FAILED+1)) log::error "Test failed..." return } test::skip() { echo log::note "${SUITE:-unknown} - ${1:-unknown}" SKIPPED=$((SKIPPED+1)) log::warning "Test skipped..." return } test::run() { echo log::note "${SUITE:-unknown} - ${1:-unknown}" shift "$@" && test::success || test::fail } test::_escape() { tr '\n' "$NEWLINE_CHAR" | sed -E "s/[\s$(printf "$NEWLINE_CHAR") ]+$//g" } test::equals() { local -r actual="$(cat)" local -r expected="${1:-}" local -r actual2="$(echo "$actual" | test::_escape)" local -r expected2="$(echo "$expected" | test::_escape)" if [[ "$actual2" != "$expected2" ]]; then log::error "Expected '${expected}' but got '${actual}'" return 2 fi } test::contains() { local -r haystack="$(cat)" local -r needle="${1:-}" local -r haystack2="$(echo "$haystack" | test::_escape)" local -r needle2="$(echo "$needle" | test::_escape)" if [[ "$haystack2" != *"$needle2"* ]]; then log::error "Expected '${haystack}' to include '${needle2}'" return 2 fi } test::finish() { echo if [ $SKIPPED -gt 0 ]; then log::warning "${SKIPPED} tests skipped!" fi if [ $FAILED -gt 0 ]; then log::error "${PASSED} tests passed but ${FAILED} failed... :(" exit "${FAILED}" else log::success "All ${PASSED} tests passed! :)" exit 0 fi } ================================================ FILE: tests/helpers.sh ================================================ #!/usr/local/bin/env bash myhelperfn() { echo "inside helper: $*" } ================================================ FILE: tests/no_prompt_cheats/cases.cheat ================================================ ; author: CI/CD % test, first # trivial case -> "foo" echo "foo" # map with underscores -> "_foo_" echo "" # expand -> "foo" echo "" # duplicated lines -> "foo\nlorem ipsum\nlorem ipsum\nbaz" echo foo echo lorem ipsum echo lorem ipsum echo baz # empty line -> "foo\n\n\nbar" echo "$(cat < "172.17.0.2" 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 # 2nd column with default delimiter -> "rust is cool" echo " is cool" # 2nd column with custom delimiter -> "clojure is cool" echo " is cool" # multiple words -> "lorem foo bar ipsum" echo "lorem ipsum" # variable dependency, full -> "2 12 a 2" echo " " ; # variable dependency, we can ignore intermediate values -> "foo 12" ; printf "foo "; : ; echo "" # nested unused value -> "path: /my/pictures" echo "path: " # multiline command: no backslash -> "foo\nbar" echo "foo" echo "bar" # multiline command: with backslash -> "lorem ipsum\nno match" echo 'lorem ipsum' echo "foo" \ | grep -q "bar" \ && echo "match" \ || echo "no match" # multiline variable -> "foo bar" echo "" # helper -> "inside helper: 42" myhelperfn 42 $ x: echo '2' $ x2: echo "$((x+10))" $ y: echo 'a' $ language: echo '0 rust rust-lang.org' --- --column 2 $ language2: echo '1;clojure;clojure.org' --- --column 2 --delimiter ';' $ multiword: echo 'foo bar' $ pictures_folder: echo "/my/pictures" $ map1: echo "foo" --- --map 'echo _$(cat)_' $ multilinevar: echo "xoo yar" \ | tr 'x' 'f' \ | tr 'y' 'b' $ expand1: echo "foo" --- --expand # this should be displayed -> "hi" echo hi % test, second @ test, first @ test, third # nested used value -> "path: /my/pictures/wallpapers" echo "path: " # same command as before -> "12" : ; echo "" # the order isn't relevant -> "br" echo "" $ wallpaper_folder: echo "/wallpapers" % test, third ; this cheathsheet doesn't have any commands $ country: echo "br" ================================================ FILE: tests/no_prompt_cheats/one.cheat ================================================ ; author: CI/CD % test, first # trivial case -> "foo" echo "foo" ================================================ FILE: tests/run ================================================ #!/usr/bin/env bash set -euo pipefail export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)" source "${NAVI_HOME}/tests/core.bash" export TEST_CHEAT_PATH="${NAVI_HOME}/tests/no_prompt_cheats" export NAVI_EXE="${NAVI_HOME}/target/debug/navi" if ! command_exists navi; then navi() { "$NAVI_EXE" "$@" } export -f navi fi _navi() { stty sane || true local path="${NAVI_TEST_PATH:-$TEST_CHEAT_PATH}" path="${path//$HOME/~}" export NAVI_ENV_VAR_PATH="$path" RUST_BACKTRACE=1 NAVI_PATH='$NAVI_ENV_VAR_PATH' NAVI_CONFIG="${NAVI_HOME}/tests/config.yaml" "$NAVI_EXE" "$@" } _navi_cases() { local -r filter="${1::-2}" _navi --query "$filter" --best-match } _navi_cases_test() { _navi_cases "$1" \ | test::equals "$2" } _get_all_tests() { cat "${TEST_CHEAT_PATH}/cases.cheat" \ | grep '^#' \ | grep ' ->' \ | sed 's/\\n/'"$(printf "$NEWLINE_CHAR")"'/g' \ | sed -E 's/# (.*) -> "(.*)"/\1|\2/g' } _get_tests() { local -r filter="$1" if [ -n "$filter" ]; then _get_all_tests \ | grep "$filter" else _get_all_tests fi } _navi_tldr() { _navi --tldr docker --query ps --print --best-match \ | test::contains "docker ps" } _navi_cheatsh() { _navi --cheatsh docker --query remove --print --best-match \ | test::contains "docker container prune" } _navi_widget() { local -r out="$(_navi widget "$1")" if ! echo "$out" | grep -q "navi "; then echo "$out" return 1 fi } _navi_cheatspath() { _navi info cheats-path \ | grep -q "/cheats" } _kill_tmux() { pkill -f tmux 2>/dev/null || true } _assert_tmux() { local -r log_file="$1" local -r sessions="$(tmux list-sessions)" if [ -z "$sessions" ]; then _kill_tmux cat "$log_file" return 1 fi } _integration() { _kill_tmux local -r log_file="${NAVI_HOME}/target/ci.log" local -r cheats_path="$($NAVI_EXE info cheats-path)" rm -rf "$cheats_path" 2>/dev/null || true mkdir -p "$cheats_path" 2>/dev/null || true local -r bak_cheats_path="$(mktemp -d "${cheats_path}_XXXXX")" rm "$log_file" 2>/dev/null || true mv "$cheats_path" "$bak_cheats_path" 2>/dev/null || true log::note "Starting sessions..." tmux new-session -d -s ci "export NAVI_TEST_PATH='${cheats_path}'; ${NAVI_HOME}/tests/run _navi |& tee '${log_file}'" sleep 5 _assert_tmux "$log_file" log::note "Downloading default cheatsheets..." tmux send-key -t ci "download default"; tmux send-key -t ci "Enter" sleep 1 _assert_tmux "$log_file" log::note "Confirming import..." tmux send-key -t ci "y" sleep 1 tmux send-key -t ci "Enter" sleep 6 _assert_tmux "$log_file" log::note "Running snippet..." tmux send-key -t ci "pwd" sleep 1 tmux send-key -t ci "Enter" log::note "Checking paths..." sleep 2 local -r downloaded_path="$(cat "$log_file" | grep 'They are now located at' | sed 's/They are now located at //')" ls "$downloaded_path" | grep -q '^pkg_mgr__brew.cheat$' } if ! command_exists fzf; then export PATH="$PATH:$HOME/.fzf/bin" fi cd "$NAVI_HOME" filter="${1:-}" # TODO: remove this if [[ $filter == "_navi" ]]; then shift _navi "$@" exit 0 fi test::set_suite "cases" ifs="$IFS" IFS=$'\n' for i in $(_get_tests "$filter"); do IFS="$ifs" query="$(echo "$i" | cut -d'|' -f1)" expected="$(echo "$i" | tr "$NEWLINE_CHAR" '\n' | cut -d'|' -f2)" test::run "$query" _navi_cases_test "$query" "$expected" done test::set_suite "info" test::run "cheats_path" _navi_cheatspath test::set_suite "widget" test::run "bash" _navi_widget "bash" test::run "zsh" _navi_widget "zsh" test::run "fish" _navi_widget "fish" test::run "elvish" _navi_widget "elvish" test::run "nu" _navi_widget "nushell" test::set_suite "3rd party" test::run "tldr" _navi_tldr test::run "cheatsh" _navi_cheatsh test::set_suite "integration" test::run "welcome->pwd" _integration test::finish ================================================ FILE: tests/tests.rs ================================================ #[cfg(test)] mod tests { #[test] fn it_works() { //let _x = navi::handle_config(navi::config_from_iter( //"navi best trivial".split(' ').collect(), //)); // assert_eq!(x, 3); } }