├── .github └── workflows │ ├── cargo-build-and-test.yml │ └── manual.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── manual ├── book.toml └── src │ ├── README.md │ ├── SUMMARY.md │ ├── bat.md │ ├── current_limitations.md │ ├── customization │ ├── customizables.md │ ├── customization.md │ ├── edit.md │ └── preview.md │ ├── edit.md │ ├── filetypes │ ├── filetypes.md │ ├── match.md │ ├── negate.md │ └── options.md │ ├── git │ ├── add.md │ ├── blame.md │ ├── branch.md │ ├── commit.md │ ├── diff.md │ ├── git.md │ ├── restore.md │ ├── status.md │ └── status_markers.md │ ├── labels.md │ ├── offerings.md │ ├── prerequisite_setup.md │ ├── prerequisites │ ├── lames.md │ ├── linux.md │ ├── macos.md │ ├── prerequisites.md │ └── windows.md │ ├── releases │ ├── all.md │ ├── releases.md │ └── version_number.md │ ├── rootless.md │ ├── standard_usage.md │ ├── tokei.md │ ├── unlocked_functionality.md │ └── upgrading.md ├── nomad.toml └── src ├── cli ├── config.rs ├── filetype.rs ├── git.rs ├── global.rs ├── mod.rs └── releases.rs ├── config ├── mod.rs ├── models.rs ├── preview.rs └── toml.rs ├── errors.rs ├── git ├── blame.rs ├── branch.rs ├── commit.rs ├── diff.rs ├── markers.rs ├── mod.rs ├── status.rs ├── trees.rs └── utils.rs ├── loc ├── format.rs ├── mod.rs └── utils.rs ├── main.rs ├── models └── mod.rs ├── releases └── mod.rs ├── style ├── mod.rs ├── models.rs ├── paint.rs └── settings.rs ├── switches ├── config.rs ├── filetype.rs ├── git.rs ├── mod.rs └── release.rs ├── traverse ├── format.rs ├── mod.rs ├── models.rs ├── modes.rs ├── traits.rs └── utils.rs ├── ui ├── app.rs ├── interface.rs ├── layouts.rs ├── mod.rs ├── models.rs ├── stateful_widgets.rs ├── text.rs ├── utils.rs └── widgets.rs └── utils ├── bat.rs ├── cache.rs ├── export.rs ├── icons.rs ├── meta.rs ├── mod.rs ├── open.rs ├── paint.rs ├── paths.rs ├── search.rs └── table.rs /.github/workflows/cargo-build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Cargo build and test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Build nomad 20 | run: cargo build --verbose 21 | 22 | - name: Run tests 23 | run: cargo test --verbose 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/manual.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Manual 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Setup mdBook 17 | uses: peaceiris/actions-mdbook@v1 18 | with: 19 | mdbook-version: 'latest' 20 | 21 | - name: Build manual 22 | run: mdbook build 23 | working-directory: manual 24 | 25 | - name: Deploy to GitHub Pages 26 | uses: JamesIves/github-pages-deploy-action@v4.2.5 27 | with: 28 | branch: gh-pages 29 | folder: manual/book 30 | 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | manual/book/* 2 | /target/ 3 | 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Joseph Lai "] 3 | categories = ["command-line-utilities", "development-tools", "filesystem", "visualization"] 4 | description = "The customizable next gen tree command with Git integration and TUI" 5 | documentation = "https://josephlai241.github.io/nomad/" 6 | edition = "2021" 7 | homepage = "https://github.com/JosephLai241/nomad" 8 | keywords = ["command-line-tool", "developer-tool", "git", "regex", "tree"] 9 | license = "MIT" 10 | name = "nomad-tree" 11 | readme = "README.md" 12 | repository = "https://github.com/JosephLai241/nomad" 13 | version = "1.0.0" 14 | 15 | [[bin]] 16 | name = "nd" 17 | path = "src/main.rs" 18 | 19 | [dependencies] 20 | ansi_term = "0.12.1" 21 | anyhow = "1.0.53" 22 | bat = "0.18.3" 23 | chrono = "0.4.19" 24 | crossterm = "0.23.0" 25 | directories = "4.0.1" 26 | git2 = "0.13.25" 27 | ignore = "0.4.18" 28 | indicatif = "0.16.2" 29 | itertools = "0.10.3" 30 | lazy_static = "1.4.0" 31 | ptree = "0.4.0" 32 | rand = "0.8.5" 33 | regex = "1.5.4" 34 | self_update = "0.28.0" 35 | serde = { version = "1.0.132", features = ["derive"] } 36 | serde_json = "1.0.73" 37 | structopt = "0.3.25" 38 | syntect = "4.6.0" 39 | term-table = "1.3.2" 40 | thiserror = "1.0.30" 41 | tokei = "12.1.2" 42 | toml = "0.5.8" 43 | tui = { version = "0.17.0", default-features = false, features = ["crossterm"] } 44 | unix_mode = "0.1.3" 45 | users = "0.11.0" 46 | 47 | [dev-dependencies] 48 | assert_cmd = "2.0.2" 49 | 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Joseph Lai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /manual/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Joseph Lai"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "nomad" 7 | 8 | [output.html] 9 | default-theme = "Ayu" 10 | git-repository-url = "https://github.com/JosephLai241/nomad" 11 | preferred-dark-theme = "Ayu" 12 | -------------------------------------------------------------------------------- /manual/src/README.md: -------------------------------------------------------------------------------- 1 | ________ ________ ________ ________ _______ 2 | ╱ ╱ ╲╱ ╲╱ ╲╱ ╲_╱ ╲ 3 | ╱ ╱ ╱ ╱ ╱ ╱ 4 | ╱ ╱ ╱ ╱ ╱ ╱ 5 | ╲__╱_____╱╲________╱╲__╱__╱__╱╲___╱____╱╲________╱ 6 | 7 | > The customizable next gen `tree` command with Git integration and TUI. 8 | 9 | ![Rust](https://img.shields.io/badge/Rust-black?style=flat-square&logo=rust) 10 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/JosephLai241/nomad/Rust?style=flat-square&logo=github) 11 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/JosephLai241/nomad?style=flat-square) 12 | ![Lines of code](https://img.shields.io/tokei/lines/github/JosephLai241/nomad?style=flat-square) 13 | ![License](https://img.shields.io/github/license/JosephLai241/nomad?style=flat-square) 14 | 15 | `nomad` is a modern, customizable [`tree`][tree] alternative that aims to expand upon the concept by simplifying the way you interact with files and directories within your terminal. 16 | 17 | [tree]: https://linux.die.net/man/1/tree 18 | -------------------------------------------------------------------------------- /manual/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./README.md) 4 | * [What `nomad` Has to Offer](./offerings.md) 5 | * [Current Limitations](./current_limitations.md) 6 | * [Prerequisite Setup](./prerequisites/prerequisites.md) 7 | + [Installing a NerdFont on MacOS](./prerequisites/macos.md) 8 | + [Installing a NerdFont on Linux](./prerequisites/linux.md) 9 | + [Installing a NerdFont on Windows](./prerequisites/windows.md) 10 | + ["I don't want to install a NerdFont"](./prerequisites/lames.md) 11 | * [Standard Usage](./standard_usage.md) 12 | * [Running `nomad` With Item Labels](./labels.md) 13 | + [Unlocked Functionality via Labels](./unlocked_functionality.md) 14 | * [`bat` - `bat` Files](./bat.md) 15 | * [`edit` - Edit Files](./edit.md) 16 | * [`tokei` - Display Code Statistics](./tokei.md) 17 | * [`ft` - Filtering Items by Filetype or Glob](./filetypes/filetypes.md) 18 | + [Matching Filetypes or Globs](./filetypes/match.md) 19 | + [Negating Fiietypes or Globs](./filetypes/negate.md) 20 | + [Listing Filetype Definitions](./filetypes/options.md) 21 | * [`git` - Run `Git` Commands](./git/git.md) 22 | + [Git Status Markers](./git/status_markers.md) 23 | + [`git status` - `git status` in Tree Form](./git/status.md) 24 | + [`git add`](./git/add.md) 25 | + [`git blame`](./git/blame.md) 26 | + [`git branch`](./git/branch.md) 27 | + [`git commit`](./git/commit.md) 28 | + [`git diff`](./git/diff.md) 29 | + [`git restore`](./git/restore.md) 30 | * [Rootless Mode](./rootless.md) 31 | * [Customizing `nomad`](./customization/customization.md) 32 | + ["What Can I Customize?"](./customization/customizables.md) 33 | + [`config edit` - Edit the Configuration File](./customization/edit.md) 34 | + [`config preview` - Preview Your Settings](./customization/preview.md) 35 | * [`releases` - View `nomad` Releases](./releases/releases.md) 36 | + [`releases all` - View All Releases](./releases/all.md) 37 | + [`releases VERSION_NUMBER` - View Release Data for a Specific Version](./releases/version_number.md) 38 | * [Upgrading `nomad`](./upgrading.md) 39 | 40 | -------------------------------------------------------------------------------- /manual/src/bat.md: -------------------------------------------------------------------------------- 1 | # `bat` - `bat` Files 2 | 3 | > **NOTE**: Requires a preceeding run in a [labeled mode](./labels.md). 4 | 5 | You can quickly `bat` files by using the `bat` subcommand. As mentioned before, `bat` is a modern `cat` alternative that supports syntax highlighting and displays Git change markers like you would see in your text editor, to name two of its features. 6 | 7 | For example, if you wanted to `bat` the 2nd and 5th file as well as all files in the directory labeled "b", you would run the following command: 8 | 9 | ``` 10 | nd bat 2 5 b 11 | ``` 12 | 13 | Files will be `bat`ed in the order of the labels you provide. 14 | -------------------------------------------------------------------------------- /manual/src/current_limitations.md: -------------------------------------------------------------------------------- 1 | # Current Limitations 2 | 3 | As of v1.0.0, `nomad` unfortunately experiences a performance hit when running it in very large directories. This is due to the way the directory tree is built. 4 | 5 | This project uses the [`ptree`][ptree] crate to build the tree via its `TreeBuilder`. The `TreeBuilder` stores all tree items before building and displaying the tree, so the tree will only be displayed after all items (depending on your traversal settings) have been visited. 6 | 7 | I have plans to replace this crate with a tree implementation that will display items in real time when they are visited, but for now I would recommend against running it in very large directories. 8 | 9 | [ptree]: https://docs.rs/ptree/latest/ptree/ 10 | -------------------------------------------------------------------------------- /manual/src/customization/customizables.md: -------------------------------------------------------------------------------- 1 | # "What Can I Customize?" 2 | 3 | The following settings are customizable for the normal tree: 4 | 5 | * Indentation 6 | * Indentation characters (the tree's branches) 7 | * Padding 8 | * Directory color 9 | * Label colors (for labeled modes) 10 | * Git markers and its colors 11 | * Regex match color 12 | 13 | The following settings are customizable for [Rootless mode](../rootless.md): 14 | 15 | * Widget border color 16 | * Standard item highlight color (the color for items that do not contain a Git status) 17 | * Git status colors (for items) 18 | * Regex match color 19 | 20 | -------------------------------------------------------------------------------- /manual/src/customization/customization.md: -------------------------------------------------------------------------------- 1 | # Customizing `nomad` 2 | 3 | Great news - you can customize `nomad` to your liking if you do not like the default configuration. And you do not have to create this configuration file yourself! 4 | -------------------------------------------------------------------------------- /manual/src/customization/edit.md: -------------------------------------------------------------------------------- 1 | # `config edit` - Edit the Configuration File 2 | 3 | To simplify customization, **you do *not* have to make your own configuration file** -- `nomad` will write one to your disk if it does not already exist. 4 | 5 | To access the configuration file, simply use the `config edit` subcommand: 6 | 7 | ``` 8 | nd config edit 9 | ``` 10 | 11 | I have written a walkthrough of sorts into the TOML file detailing how you can choose built-in colors or pick a custom color, so I will not elaborate how to do so here. You can also take a look at [`nomad.toml`][nomad.toml] in this repository; this is the same configuration file that is written to your disk. 12 | 13 | [nomad.toml]: https://github.com/JosephLai241/nomad/blob/main/nomad.toml 14 | -------------------------------------------------------------------------------- /manual/src/customization/preview.md: -------------------------------------------------------------------------------- 1 | # `config preview` - Preview Your Settings 2 | 3 | You can quickly see a preview of your settings by using the `config preview` subcommand: 4 | 5 | ``` 6 | nd config preview 7 | ``` 8 | 9 | This will display a dummy tree with all your settings applied. 10 | -------------------------------------------------------------------------------- /manual/src/edit.md: -------------------------------------------------------------------------------- 1 | # `edit` - Edit Files 2 | 3 | > **NOTE**: Requires a preceeding run in a [labeled mode](./labels.md). 4 | 5 | You can quickly open files in a text editor by using the `edit` subcommand. 6 | 7 | `nomad` will try to find your default `$EDITOR` setting and open files with that editor if you have specified one. If your `$EDITOR` is not set, it will try to open files with the following text editor in this order: 8 | 9 | 1. [Neovim][Neovim] 10 | 2. [Vim][Vim] 11 | 3. [Vi][Vi] 12 | 4. [Nano][Nano] 13 | 14 | The first editor that is found on your machine will be used to edit your files. 15 | 16 | For example, if you wanted to open the 2nd and 5th file as well as all files in the directory labeled "b", you would run the following command: 17 | 18 | ``` 19 | nd edit 2 5 b 20 | ``` 21 | 22 | [Nano]: https://www.nano-editor.org/ 23 | [Neovim]: https://github.com/neovim/neovim 24 | [Vi]: http://ex-vi.sourceforge.net/ 25 | [Vim]: https://www.vim.org/ 26 | -------------------------------------------------------------------------------- /manual/src/filetypes/filetypes.md: -------------------------------------------------------------------------------- 1 | # `ft` - Filtering Items by Filetype or Glob 2 | 3 | The `ft` subcommand allows you to filter results by filetypes or globs. This subcommand contains additional subcommands that allow you to control whether to match (include) or negate (ignore) filetypes or globs. 4 | 5 | ## Usage 6 | 7 | ``` 8 | Filter directory items by filetype 9 | 10 | USAGE: 11 | nd ft 12 | 13 | FLAGS: 14 | -h, --help Prints help information 15 | -V, --version Prints version information 16 | 17 | SUBCOMMANDS: 18 | help Prints this message or the help of the given subcommand(s) 19 | match Only display files matching the specified filetypes and/or globs 20 | negate Do not display files that match the specified filetypes and/or globs 21 | options List the current set of filetype definitions. Optionally search for a filetype. ie. `nd filetype options rust` 22 | ``` 23 | -------------------------------------------------------------------------------- /manual/src/filetypes/match.md: -------------------------------------------------------------------------------- 1 | # Matching Filetypes or Globs 2 | 3 | You can include filetypes or globs by using the `match` subcommand. Use the `-f` flag to include filetypes and the `-g` flag to match globs. 4 | 5 | For example, if you only wanted to include Rust files and files that match the glob `*.asdf`, you would run: 6 | 7 | ``` 8 | nd ft match -f rust -g "*.asdf" 9 | ``` 10 | 11 | Both the `-f` and `-g` flags accept a list of filetypes or globs. An example: 12 | 13 | ``` 14 | nd ft match -f rust toml py -g "*.asdf" "*.qwerty" 15 | ``` 16 | 17 | -------------------------------------------------------------------------------- /manual/src/filetypes/negate.md: -------------------------------------------------------------------------------- 1 | # Negating Filetypes or Globs 2 | 3 | You can exclude filetypes or globs by using the `negate` subcommand. Use the `-f` flag to exclude filetypes and the `-g` flag to exclude globs. 4 | 5 | For example, if you only wanted to negate C++ files and files that match the glob `*.asdf`, you would run: 6 | 7 | ``` 8 | nd ft negate -f cpp -g "*.asdf" 9 | ``` 10 | 11 | Both the `-f` and `-g` flags accept a list of filetypes or globs. An example: 12 | 13 | ``` 14 | nd ft negate -f c cpp java -g "*.asdf" "*.qwerty" 15 | ``` 16 | 17 | -------------------------------------------------------------------------------- /manual/src/filetypes/options.md: -------------------------------------------------------------------------------- 1 | # Listing Filetype Definitions 2 | 3 | If you want to see the built-in filetype globs, use the `options` subcommand to see a table containing the filetype and the globs for that particular filetype: 4 | 5 | ``` 6 | nd ft options 7 | ``` 8 | 9 | You can also search for a filetype to only display those matching globs. For example, if you wanted to see the globs for Rust files, you would run: 10 | 11 | ``` 12 | nd ft options rust 13 | ``` 14 | 15 | -------------------------------------------------------------------------------- /manual/src/git/add.md: -------------------------------------------------------------------------------- 1 | # `git add` 2 | 3 | > **NOTE**: Requires a preceeding run in a [labeled mode](../labels.md). 4 | 5 | You can use the `git add` subcommand to quickly stage files. For example, if you wanted to stage the 2nd and 5th file as well as all files containing a Git status in the directory labeled "b", you would run the following command: 6 | 7 | ``` 8 | nd git add 2 5 b 9 | ``` 10 | 11 | If you pass a directory label, only items containing a Git status will be staged, just like the original `git add` command. 12 | -------------------------------------------------------------------------------- /manual/src/git/blame.md: -------------------------------------------------------------------------------- 1 | # `git blame` 2 | 3 | > **NOTE**: Requires a preceeding run in a [labeled mode](../labels.md). 4 | 5 | You can use the `git blame` subcommand to quickly blame a file. **This subcommand only accepts one file, so directory labels will not work!** 6 | 7 | `nomad`'s `git blame` offers some visual improvements over the original `git blame` command: 8 | 9 | * Commit hashes, authors, and timestamps are colorized differently to provide contrast among the columns 10 | * Lines are colorized based on a unique color assigned to each author 11 | + These colors are randomly chosen and will be different each time you run `git blame`. Commits made by you are plain white whereas commits by other authors are assigned a color. 12 | 13 | ### Usage 14 | 15 | ``` 16 | USAGE: 17 | nd git blame [FLAGS] [OPTIONS] 18 | 19 | FLAGS: 20 | --emails Display emails for each blame line instead of timestamps 21 | -h, -help Prints help information 22 | -V, --version Prints version information 23 | 24 | OPTIONS: 25 | -l, --lines ... Restrict a range of lines to display in the blame 26 | 27 | ARGS: 28 | Display a blame for this file 29 | ``` 30 | -------------------------------------------------------------------------------- /manual/src/git/branch.md: -------------------------------------------------------------------------------- 1 | # `git branch` 2 | 3 | You can use the `git branch` subcommand to display all your branches in a tree form (this may be disabled, see the [Usage](#usage) section). 4 | 5 | `nomad`'s `git branch` displays additional data by default, such as: 6 | 7 | * Whether a branch is `HEAD` 8 | * Whether an upstream branch is set 9 | 10 | > Unfortunately I have not implemented `git checkout` for v1.0.0, but I plan on implementing it in the next version for quick branch switching. 11 | 12 | ### Usage 13 | 14 | ``` 15 | USAGE: 16 | nd git branch [FLAGS] [OPTIONS] 17 | 18 | FLAGS: 19 | -f, --flat Display branches in a normal list 20 | -h, --help Prints help information 21 | --no-icons Do not display icons 22 | -n, --numbered Label branches with numbers 23 | -s, --statistics Display the total number of branches 24 | -V, --version Prints version information 25 | 26 | OPTIONS: 27 | --export Export the tree to a file. Optionally include a target filename 28 | -p, --pattern Only display branches matching this pattern. Supports regex expressions 29 | ``` 30 | -------------------------------------------------------------------------------- /manual/src/git/commit.md: -------------------------------------------------------------------------------- 1 | # `git commit` 2 | 3 | `git commit` has also been implemented and offers some visual improvements over the original `git commit` command. 4 | -------------------------------------------------------------------------------- /manual/src/git/diff.md: -------------------------------------------------------------------------------- 1 | # `git diff` 2 | 3 | > **NOTE**: Requires a preceeding run in a [labeled mode](../labels.md). 4 | 5 | You can use the `git diff` command to quickly display the diff for files. For example, if you wanted to `diff` the 2nd and 5th file as well as all files containing a Git status in the directory labeled "b", you would run the following command: 6 | 7 | ``` 8 | nd git diff 2 5 b 9 | ``` 10 | 11 | `nomad`'s `git diff` offers visual improvements over the original `git diff` command by making it much easier to read, in my opinion. See for yourself: 12 | 13 | 14 | 15 | If you pass a directory label, only items containing a Git status will be `diff`ed, just like the original `git diff` command. 16 | -------------------------------------------------------------------------------- /manual/src/git/git.md: -------------------------------------------------------------------------------- 1 | # `git` - Run Git Commands 2 | 3 | > **NOTE**: Most Git commands require a preceeding run in a [labeled mode](../labels.md). 4 | 5 | In addition to respecting ignore-type files such as `.gitignore`s and displaying [Git status markers](./status_markers.md), `nomad` implements the following Git commands: 6 | 7 | * [`git add`](./add.md) 8 | * [`git blame`](./blame.md) 9 | * [`git branch`](./branch.md) 10 | * [`git commit`](./commit.md) 11 | * [`git diff`](./diff.md) 12 | * [`git restore`](./restore.md) 13 | * [`git status`](./status.md) 14 | 15 | > **TIP:** I recommend taking a look at [`git status`](./status.md) before looking at the other sections. 16 | 17 | **These commands are not meant to be a full replacement for the original Git commands yet!** Git is a *massive* ecosystem, so I have not yet implemented these commands to support everything the original `git` counterpart is capable of. `nomad`'s `git` commands work well if you need to do something basic with those Git commands, however. 18 | 19 | All Git-related functionality is implemented via the [`git2`][git2] crate, which provides an API to Git. 20 | 21 | [git2]: https://docs.rs/git2/latest/git2/ 22 | -------------------------------------------------------------------------------- /manual/src/git/restore.md: -------------------------------------------------------------------------------- 1 | # `git restore` 2 | 3 | > **NOTE**: Requires a preceeding run in a [labeled mode](../labels.md). 4 | 5 | You can use the `git restore` subcommand to restore files back to its clean state. For example, if you wanted to restore the 2nd and 5th file as well as all files containing a Git status in the directory labeled "b", you would run the following command: 6 | 7 | ``` 8 | nd git restore 2 5 b 9 | ``` 10 | 11 | -------------------------------------------------------------------------------- /manual/src/git/status.md: -------------------------------------------------------------------------------- 1 | # `git status` - `git status` in Tree Form 2 | 3 | Use `git status` to see the Git status in tree form. You can run this in lieu of running `nomad` in normal mode to only see tracked files that contain a Git status. 4 | 5 | This command pairs well with the other Git commands described below this section. 6 | 7 | ### Usage 8 | 9 | ``` 10 | USAGE: 11 | nd git status [FLAGS] [OPTIONS] 12 | 13 | FLAGS: 14 | -L, --all-labels Label both files and directories. Alias for `-n -l` 15 | -h, --help Prints help information 16 | -l, --label-directories Label directories with characters 17 | --loc Display code statistics (lines of code, blanks, and comments) for each item 18 | -m, --metadata Show item metadata such as file permissions, owner, group, file size, and last modified time 19 | --no-colors Do not display any colors 20 | --no-git Do not display Git status markers 21 | --no-icons Do not display icons 22 | -n, --numbered Label directory items with numbers 23 | --plain Mute icons, Git markers, and colors to display a plain tree 24 | -s, --stats Display traversal statistics after the tree is displayed 25 | --summary Display `tokei` (lines of code counter) statistics. This only applies if `--loc` is provided 26 | -V, --version Prints version information 27 | 28 | OPTIONS: 29 | --export Export the tree to a file. Optionally include a target filename 30 | -p, --pattern Only display items matching this pattern. Supports regex expressions 31 | ``` 32 | 33 | -------------------------------------------------------------------------------- /manual/src/git/status_markers.md: -------------------------------------------------------------------------------- 1 | # Git Status Markers 2 | 3 | Here is a table that contains the default Git status markers, the marker's color, and what it represents: 4 | 5 | | Marker | Color | Status | 6 | |--------|----------|---------------------| 7 | | `!` | Red | Conflicting | 8 | | `D` | Red | Deleted | 9 | | `M` | Orange | Modified | 10 | | `R` | Orange | Renamed | 11 | | `TC` | Purple | Type change | 12 | | `SA` | \*Green | Staged, Added | 13 | | `SD` | \*Red | Staged, Deleted | 14 | | `SM` | \*Orange | Staged, Modified | 15 | | `SR` | \*Orange | Staged, Renamed | 16 | | `STC` | \*Purple | Staged, type change | 17 | | `U` | Gray | Untracked | 18 | 19 | > \* The filename will also be painted the same color. 20 | 21 | I you do not like the default marker or color configuration, you can [customize it to your liking](../customization/customization.md). 22 | -------------------------------------------------------------------------------- /manual/src/labels.md: -------------------------------------------------------------------------------- 1 | # Labeled Modes 2 | 3 | Running `nomad` in a labeled mode unlocks its full potential and allows you to perform actions on items in the tree very quickly. 4 | 5 | The flags that enable labels are: 6 | 7 | * `-l` - labels directories 8 | * `-n` - labels items 9 | * `-L` - labels both directories and items. This is an alias for `-l -n` 10 | 11 | Directories are labeled with a letter and items are labeled by numbers. Numbers may be appended to directory labels if there are more than 26 directories in the tree. 12 | -------------------------------------------------------------------------------- /manual/src/offerings.md: -------------------------------------------------------------------------------- 1 | # What `nomad` Has to Offer 2 | 3 | When used without extra options, `nomad` will display a stylized tree for a directory while respecting any rules that are specified in any ignore-type files such as `.gitignore`s (this can be disabled). 4 | 5 | There are a ton of features baked into `nomad`. Here is a list of all it has to offer (so far): 6 | 7 | * Display tree items with NerdFont-supported icons and Git status markers regardless of whether you run `nomad` in a directory containing a Git repository. 8 | * Filter (including or excluding) directory items by filetype or glob patterns. 9 | * Interact with directory items without typing paths. 10 | + [`bat`][bat], an improved `cat` alternative written in Rust, or open file(s) in the tree. 11 | + Quickly `git diff/add/restore/blame` files. 12 | * Integrated Git 13 | + In addition to `git diff/add/restore/blame`, `git status` and `git branch` are implemented in tree form. 14 | * Visually improved `git diff` and `git blame`. 15 | * [Rootless mode](./rootless.md) -- `nomad` in a TUI (terminal UI) with file preview and pattern searching capability. 16 | * Integrated [`Tokei`][Tokei], a program that displays statistics about your code such as the number of files, total lines, comments, and blank lines. 17 | * Convenient configuration editing and preview. 18 | * It's self-upgrading and you can see the available releases all from your terminal! 19 | 20 | [bat]: https://github.com/sharkdp/bat 21 | [tokei]: https://github.com/XAMPPRocky/tokei 22 | -------------------------------------------------------------------------------- /manual/src/prerequisite_setup.md: -------------------------------------------------------------------------------- 1 | # Prerequisite Setup 2 | -------------------------------------------------------------------------------- /manual/src/prerequisites/lames.md: -------------------------------------------------------------------------------- 1 | # "I don't want to install a Nerdfont" 2 | 3 | If you do not want a crispy looking tree, you can include the `--mute-icons` flag to disable icons. 4 | 5 | Including this flag every time you run `nomad` can become cumbersome, so it may be helpful to create an alias for it in your `.bashrc` or equivalent: 6 | 7 | ```bash 8 | # In `.bashrc` 9 | 10 | alias nd="nd --mute-icons" 11 | ``` 12 | 13 | Also, you are lame as **fuck**. 14 | -------------------------------------------------------------------------------- /manual/src/prerequisites/linux.md: -------------------------------------------------------------------------------- 1 | # Installing a NerdFont on Linux 2 | -------------------------------------------------------------------------------- /manual/src/prerequisites/macos.md: -------------------------------------------------------------------------------- 1 | # Installing a NerdFont on MacOS 2 | 3 | Installing a NerdFont on **MacOS** is particularly easy because NerdFonts are available via [Homebrew][Homebrew]. 4 | 5 | To install the Hack NerdFont, for example, run these commands: 6 | 7 | ``` 8 | brew tap homebrew/cask-fonts 9 | brew install --cask font-hack-nerd-font 10 | ``` 11 | 12 | Then go to your terminal's preferences and set the font to the newly installed NerdFont. If you're using [iTerm2][iTerm2], this is located at: 13 | 14 | ``` 15 | iTerm2 -> Preferences -> Profiles tab -> Text tab -> Font dropdown -> Hack Nerd Font Mono 16 | ``` 17 | 18 | [Homebrew]: https://brew.sh/ 19 | [iTerm2]: https://iterm2.com/ 20 | -------------------------------------------------------------------------------- /manual/src/prerequisites/prerequisites.md: -------------------------------------------------------------------------------- 1 | # Prerequisite Setup 2 | 3 | **`nomad` requires a [NerdFont][NerdFont] to correctly display the icons.** Install a NerdFont before installing `nomad`, otherwise you will be very sad when you see what the tree looks like without a NerdFont. 4 | 5 | I will provide instructions to install the Hack NerdFont on your system. If you want to use another font, follow the [NerdFont installation guide][NerdFont Installation] for instructions on how to do so for your system. 6 | 7 | [NerdFont]: https://www.nerdfonts.com/ 8 | [NerdFont Installation]: https://github.com/ryanoasis/nerd-fonts#font-installation 9 | -------------------------------------------------------------------------------- /manual/src/prerequisites/windows.md: -------------------------------------------------------------------------------- 1 | # Installing a NerdFont on Windows 2 | -------------------------------------------------------------------------------- /manual/src/releases/all.md: -------------------------------------------------------------------------------- 1 | # `releases all` - View All Releases 2 | 3 | Use the `releases all` subcommand to view all `nomad` releases in a table containing: 4 | 5 | * The name of the release 6 | * Version number 7 | * Release Date 8 | * Description 9 | * Attached assets 10 | 11 | ``` 12 | nd releases all 13 | ``` 14 | -------------------------------------------------------------------------------- /manual/src/releases/releases.md: -------------------------------------------------------------------------------- 1 | # `releases` - View `nomad` Releases 2 | 3 | You can view `nomad` releases directly from `nomad` itself. Releases are retrieved from GitHub. 4 | -------------------------------------------------------------------------------- /manual/src/releases/version_number.md: -------------------------------------------------------------------------------- 1 | # `releases VERSION_NUMBER` - View Release Data for a Specific Version 2 | 3 | You can view release data for a specific version by passing in the version number. For example, if you wanted to view release data for v1.0.0, you would run the following command: 4 | 5 | ``` 6 | nd releases v1.0.0 7 | ``` 8 | 9 | -------------------------------------------------------------------------------- /manual/src/rootless.md: -------------------------------------------------------------------------------- 1 | # Rootless Mode 2 | 3 | Rootless mode is `nomad` in TUI form. You can interactively traverse directories while in this mode, preview files, and even run regex searches within the file. 4 | 5 | 6 | 7 | Press `?` in Rootless mode to bring up the Help widget. This widget details how Rootless mode works, navigation, and keybindings. 8 | 9 | > **NOTE:** As of v1.0.0, Rootless mode does not support mouse interaction and operates solely on keyboard bindings. 10 | 11 | Git status colors are respected in this mode and can also be customized if you do not like the default configuration. See the [Customizing `nomad`](#customizing-nomad) section to learn how to do so. 12 | 13 | -------------------------------------------------------------------------------- /manual/src/standard_usage.md: -------------------------------------------------------------------------------- 1 | # Standard Usage 2 | 3 | You can display a tree for your current directory just by running `nd` after `cd`ing into that directory. 4 | 5 | ``` 6 | cd some_directory/ 7 | nd 8 | ``` 9 | 10 | Alternatively, pass in the name of the directory you wish to target: 11 | 12 | ``` 13 | nd some_directory/ 14 | ``` 15 | 16 | `nomad` will assign an icon to each file within the tree based on its respective filetype and search for Git repositories in the target directory. If Git repositories are detected, Git markers will show next to the item indicating its Git status. 17 | 18 | Git markers are shown regardless of whether you are running `nomad` in a directory containing a Git repository. For example, let's say you have a directory named `rust/` containing multiple Rust projects that are tracked by Git and are stored in sub-directories. Many of these projects contain changes that you have not committed. Running `nomad` on the `rust/` directory will display the Git changes for all of these projects. This provides a convenient overview for which projects have changes that you need to commit/revisit. 19 | 20 | See the [Git Status Markers](./git/status_markers.md) section for more details. 21 | 22 | ## Flags 23 | 24 | Here are the flags you can use in standard mode: 25 | 26 | ``` 27 | FLAGS: 28 | -L, --all-labels Label both files and directories. Alias for `-n -l` 29 | --banner Display the banner 30 | --dirs Only display directories 31 | --disrespect Disrespect all ignore rules 32 | -h, --help Prints help information 33 | --hidden Display hidden files 34 | -l, --label-directories Label directories with characters 35 | --loc Display code statistics (lines of code, blanks, and comments) for each item 36 | -m, --metadata Show item metadata such as file permissions, owner, group, file size, and last modified time 37 | --no-colors Do not display any colors 38 | --no-git Do not display Git status markers 39 | --no-icons Do not display icons 40 | -n, --numbered Label directory items with numbers 41 | --plain Mute icons, Git markers, and colors to display a plain tree 42 | -s, --stats Display traversal statistics after the tree is displayed 43 | -V, --version Prints version information 44 | 45 | OPTIONS: 46 | --export Export the tree to a file. Optionally include a target filename 47 | --max-depth Set the maximum depth to recurse 48 | --max-filesize Set the maximum filesize (in bytes) to include in the tree 49 | -p, --pattern Only display items matching this pattern. Supports regex expressions 50 | ``` 51 | -------------------------------------------------------------------------------- /manual/src/tokei.md: -------------------------------------------------------------------------------- 1 | # `tokei` - Display Code Statistics 2 | 3 | [`Tokei`][tokei] is a code statistics tool written in Rust. This tool displays data such as the number of files, total lines, comments, and blank lines present in your current directory. 4 | 5 | `Tokei` is available as its own stand-alone CLI tool, but you can also access its default behavior by using the `tokei` subcommand: 6 | 7 | ``` 8 | nd tokei 9 | ``` 10 | 11 | [tokei]: https://github.com/XAMPPRocky/tokei 12 | -------------------------------------------------------------------------------- /manual/src/unlocked_functionality.md: -------------------------------------------------------------------------------- 1 | # Unlocked Functionality via Labeled Modes 2 | 3 | By using a labeled mode, you gain access to the following subcommands: 4 | 5 | * [`bat` - `bat` file(s)](./bat.md) 6 | * [`edit` - open file(s) in a text editor](./edit.md) 7 | * `git GIT_SUBCOMMAND` - execute `git` [`add`](./git/add.md), [`blame`](./git/blame.md), [`diff`](./git/diff.md), and [`restore`](./git/restore.md) for file(s) 8 | 9 | **All of these subcommands will accept item and/or directory labels.** See their respective sections for more details. 10 | -------------------------------------------------------------------------------- /manual/src/upgrading.md: -------------------------------------------------------------------------------- 1 | # Upgrading `nomad` 2 | 3 | `nomad` can upgrade itself! 4 | 5 | Simply run this command to upgrade to the latest version: 6 | 7 | ``` 8 | nd upgrade 9 | ``` 10 | 11 | You can also run `upgrade` with the `--check` flag to just check if there is an update available. 12 | 13 | ``` 14 | nd upgrade --check 15 | ``` 16 | -------------------------------------------------------------------------------- /nomad.toml: -------------------------------------------------------------------------------- 1 | # =============== 2 | # Configure nomad 3 | # =============== 4 | 5 | 6 | # ================================= INFO START ================================= 7 | 8 | # Refer to the repository's README to learn more about how to use nomad: 9 | # 10 | # https://github.com/JosephLai241/nomad 11 | 12 | # If you are unfamiliar with TOML, refer to the official site to learn more about 13 | # its syntax: 14 | # 15 | # https://toml.io/en/ 16 | 17 | # ------------------------------- CUSTOM COLORS -------------------------------- 18 | 19 | # If you do not want to use the default colors, you can set the color to an Xterm 20 | # 256 color's hex code located here: 21 | # 22 | # https://upload.wikimedia.org/wikipedia/commons/1/15/Xterm_256color_chart.svg 23 | # 24 | # For example, if you want to set something to the color d78700 (a shade of orange), 25 | # wrap the value in quotes, ie. "d78700". 26 | 27 | # ------------------------------------------------------------------------------ 28 | 29 | # ================================== INFO END ================================== 30 | 31 | # NOTE 32 | # ---- 33 | # 34 | # The default values for each item are displayed below. 35 | # The default value will be used if an invalid value is provided. 36 | 37 | 38 | # 39 | # Uncomment the items below this table to set the tree's style. 40 | # 41 | [tree] 42 | #indent = 4 43 | #padding = 1 44 | 45 | 46 | # 47 | # Uncomment the item below this table to set tree item colors. 48 | # 49 | # Default colors 50 | # -------------- 51 | # * "black" 52 | # * "blue" 53 | # * "cyan" 54 | # * "green" 55 | # * "purple" 56 | # * "red" 57 | # * "white" 58 | # * "yellow" 59 | # 60 | [tree.items.colors] 61 | #directory_color = "blue" 62 | 63 | 64 | # 65 | # Uncomment the items below this table to set the tree's indent characters. 66 | # 67 | [tree.indent_chars] 68 | #down = "|" 69 | #down_and_right = "├" 70 | #empty = " " 71 | #right = "─" 72 | #turn_right = "└" 73 | 74 | 75 | # 76 | # Uncomment the items below this table to set label styles. 77 | # 78 | # Default colors 79 | # -------------- 80 | # * "black" 81 | # * "blue" 82 | # * "cyan" 83 | # * "green" 84 | # * "purple" 85 | # * "red" 86 | # * "white" 87 | # * "yellow" 88 | # 89 | [tree.labels] 90 | #item_labels = "d78700" 91 | #directory_labels = "d78700" 92 | 93 | 94 | # 95 | # Uncomment the items below this table to set the text displayed for each Git 96 | # item's status. 97 | # 98 | [tree.git.markers] 99 | #conflicted_marker = "!" 100 | #deleted_marker = "D" 101 | #modified_marker = "M" 102 | #renamed_marker = "R" 103 | #typechanged_marker = "TC" 104 | #untracked_marker = "U" 105 | 106 | #staged_added_marker = "SA" 107 | #staged_deleted_marker = "SD" 108 | #staged_modified_marker = "SM" 109 | #staged_renamed_marker = "SR" 110 | #staged_typechanged_marker = "STC" 111 | 112 | 113 | # 114 | # Uncomment the items below this table to set the colors of the Git markers. 115 | # 116 | # Default colors 117 | # -------------- 118 | # * "black" 119 | # * "blue" 120 | # * "cyan" 121 | # * "green" 122 | # * "purple" 123 | # * "red" 124 | # * "white" 125 | # * "yellow" 126 | # 127 | [tree.git.colors] 128 | #conflicted_color = "red" 129 | #deleted_color = "red" 130 | #modified_color = "d78700" # A shade of orange. 131 | #renamed_color = "red" 132 | #typechanged_color = "purple" 133 | #untracked_color = "767676" # A shade of gray. 134 | 135 | #staged_added_color = "green" 136 | #staged_deleted_color = "red" 137 | #staged_modified_color = "d78700" # A shade of orange. 138 | #staged_renamed_color = "red" 139 | #staged_typechanged_color = "purple" 140 | 141 | 142 | # 143 | # Uncomment the item below this table to set the color of the text that is 144 | # matched by the pattern flag (`-p/--pattern`). 145 | # 146 | # Default colors 147 | # -------------- 148 | # * "black" 149 | # * "blue" 150 | # * "cyan" 151 | # * "green" 152 | # * "purple" 153 | # * "red" 154 | # * "white" 155 | # * "yellow" 156 | # 157 | [tree.regex] 158 | #match_color = "0087ff" # A shade of blue. 159 | 160 | 161 | # 162 | # Uncomment the items below this table to set the TUI's colors. 163 | # 164 | # Default colors 165 | # -------------- 166 | # * "black" 167 | # * "blue" 168 | # * "cyan" 169 | # * "darkgray" 170 | # * "gray" 171 | # * "green" 172 | # * "lightblue" 173 | # * "lightcyan" 174 | # * "lightgreen" 175 | # * "lightmagenta" 176 | # * "lightred" 177 | # * "lightyellow" 178 | # * "magenta" 179 | # * "red" 180 | # * "white" 181 | # * "yellow" 182 | # 183 | [tui.style] 184 | #border_color = "0087ff" # A shade of blue. 185 | 186 | # The color of the tree item if it does not contain any Git changes. 187 | #standard_item_highlight_color = "0087ff" # A shade of blue. 188 | 189 | 190 | # 191 | # Uncomment the items below this table to set the Git colors in the TUI. 192 | # 193 | # Default colors 194 | # -------------- 195 | # * "black" 196 | # * "blue" 197 | # * "cyan" 198 | # * "darkgray" 199 | # * "gray" 200 | # * "green" 201 | # * "lightblue" 202 | # * "lightcyan" 203 | # * "lightgreen" 204 | # * "lightmagenta" 205 | # * "lightred" 206 | # * "lightyellow" 207 | # * "magenta" 208 | # * "red" 209 | # * "white" 210 | # * "yellow" 211 | # 212 | [tui.git.colors] 213 | #conflicted_color = "red" 214 | #deleted_color = "red" 215 | #modified_color = "d78700" # A shade of orange. 216 | #renamed_color = "red" 217 | #untracked_color = "767676" # A shade of gray. 218 | 219 | #staged_added_color = "green" 220 | #staged_deleted_color = "red" 221 | #staged_modified_color = "d78700" # A shade of orange. 222 | #staged_renamed_color = "red" 223 | 224 | 225 | # 226 | # Uncomment the item below this table to set the color of the text that is 227 | # matched when searching for a pattern in the text view. 228 | # 229 | # Default colors 230 | # -------------- 231 | # * "black" 232 | # * "blue" 233 | # * "cyan" 234 | # * "darkgray" 235 | # * "gray" 236 | # * "green" 237 | # * "lightblue" 238 | # * "lightcyan" 239 | # * "lightgreen" 240 | # * "lightmagenta" 241 | # * "lightred" 242 | # * "lightyellow" 243 | # * "magenta" 244 | # * "red" 245 | # * "white" 246 | # * "yellow" 247 | # 248 | [tui.regex] 249 | #match_color = "0087ff" # A shade of blue. 250 | 251 | 252 | -------------------------------------------------------------------------------- /src/cli/config.rs: -------------------------------------------------------------------------------- 1 | //! Providing configuration read/write CLI options. 2 | 3 | use structopt::StructOpt; 4 | 5 | #[derive(Debug, PartialEq, StructOpt)] 6 | pub enum ConfigOptions { 7 | /// Edit the configuration file. 8 | Edit, 9 | /// Preview the configuration settings in a tree. 10 | Preview, 11 | } 12 | -------------------------------------------------------------------------------- /src/cli/filetype.rs: -------------------------------------------------------------------------------- 1 | //! Providing pattern matching CLI options. 2 | 3 | use structopt::StructOpt; 4 | 5 | use super::global::GlobalArgs; 6 | 7 | #[derive(Debug, PartialEq, StructOpt)] 8 | pub enum FileTypeOptions { 9 | /// Only display files matching the specified filetypes and/or globs. 10 | Match(MatchOptions), 11 | /// Do not display files that match the specified filetypes and/or globs. 12 | Negate(NegateOptions), 13 | /// List the current set of filetype definitions. Optionally search for a filetype. 14 | /// ie. `nd filetype options rust`. 15 | Options { filetype: Option }, 16 | } 17 | 18 | #[derive(Debug, PartialEq, StructOpt)] 19 | pub struct MatchOptions { 20 | #[structopt( 21 | short, 22 | long, 23 | help = "Enter a single filetype or a list of filetypes delimited by a space. ie. `nd filetype match -f rust py go vim`" 24 | )] 25 | pub filetypes: Vec, 26 | 27 | #[structopt(flatten)] 28 | pub general: GlobalArgs, 29 | 30 | #[structopt( 31 | short, 32 | long, 33 | help = "Enter a single glob or a list of globs delimited by a space. ie. `nd filetype match -g *.something *.anotherthing`. You may have to put quotes around globs that include '*'" 34 | )] 35 | pub globs: Vec, 36 | } 37 | 38 | #[derive(Debug, PartialEq, StructOpt)] 39 | pub struct NegateOptions { 40 | #[structopt( 41 | short, 42 | long, 43 | help = "Enter a single filetype or a list of filetypes delimited by a space. ie. `nd filetype match -f rust py go vim`" 44 | )] 45 | pub filetypes: Vec, 46 | 47 | #[structopt(flatten)] 48 | pub general: GlobalArgs, 49 | 50 | #[structopt( 51 | short, 52 | long, 53 | help = "Enter a single glob or a list of globs delimited by a space. ie. `nd filetype match -g *.something *.anotherthing`. You may have to put quotes around globs that include '*'" 54 | )] 55 | pub globs: Vec, 56 | } 57 | -------------------------------------------------------------------------------- /src/cli/git.rs: -------------------------------------------------------------------------------- 1 | //! Providing Git CLI options. 2 | 3 | use structopt::StructOpt; 4 | 5 | use super::global::{LabelArgs, MetaArgs, RegexArgs, StyleArgs}; 6 | 7 | #[derive(Debug, PartialEq, StructOpt)] 8 | pub enum GitOptions { 9 | /// The `git add` command. 10 | /// This may be used after running nomad in a labeled mode. 11 | Add(AddOptions), 12 | /// The `git blame` command. 13 | /// This may be used after running nomad in a labeled mode. 14 | /// You can only call `git blame` on a single file. 15 | Blame(BlameOptions), 16 | /// The `git branch` command. Displays branches in tree form by default (this behavior may be 17 | /// disabled). 18 | Branch(BranchOptions), 19 | /// The `git commit` command. 20 | /// Optionally include a message after the command, ie. `git commit "YOUR MESSAGE HERE"` 21 | /// The default commit message is "Updating" if no message is included. 22 | Commit { message: Option }, 23 | /// The `git diff` command. 24 | /// This may be used after running nomad in a labeled mode. 25 | Diff { item_labels: Vec }, 26 | /// The `git restore` command. This may be used after running nomad in a labeled mode. 27 | Restore(RestoreOptions), 28 | /// The `git status` command. Only display changed/unstaged files in the tree. 29 | Status(StatusOptions), 30 | } 31 | 32 | #[derive(Debug, PartialEq, StructOpt)] 33 | pub struct AddOptions { 34 | #[structopt(help = "The item labels to add")] 35 | pub item_labels: Vec, 36 | 37 | #[structopt( 38 | short = "A", 39 | long, 40 | help = "Add changes from all tracked and untracked files" 41 | )] 42 | pub all: bool, 43 | } 44 | 45 | #[derive(Debug, PartialEq, StructOpt)] 46 | pub struct BlameOptions { 47 | #[structopt( 48 | long, 49 | help = "Display emails for each blame line instead of timestamps" 50 | )] 51 | pub emails: bool, 52 | 53 | #[structopt(help = "Display a blame for this file")] 54 | pub file_number: String, 55 | 56 | #[structopt( 57 | short, 58 | long, 59 | help = "Restrict a range of lines to display in the blame" 60 | )] 61 | pub lines: Vec, 62 | } 63 | 64 | #[derive(Debug, PartialEq, StructOpt)] 65 | pub struct BranchOptions { 66 | #[structopt( 67 | long = "export", 68 | help = "Export the tree to a file. Optionally include a target filename" 69 | )] 70 | pub export: Option>, 71 | 72 | #[structopt(short, long, help = "Display branches in a normal list")] 73 | pub flat: bool, 74 | 75 | #[structopt(short = "n", long = "numbered", help = "Label branches with numbers")] 76 | pub numbers: bool, 77 | 78 | #[structopt( 79 | short = "p", 80 | long = "pattern", 81 | help = "Only display branches matching this pattern. Supports regex expressions" 82 | )] 83 | pub pattern: Option, 84 | 85 | #[structopt(short, long, help = "Display the total number of branches")] 86 | pub statistics: bool, 87 | 88 | #[structopt(long = "no-icons", help = "Do not display icons")] 89 | pub no_icons: bool, 90 | } 91 | 92 | #[derive(Debug, PartialEq, StructOpt)] 93 | pub struct RestoreOptions { 94 | #[structopt( 95 | help = "Restore these items to its clean Git state. Restores in the working tree by default" 96 | )] 97 | pub item_labels: Vec, 98 | } 99 | 100 | #[derive(Debug, PartialEq, StructOpt)] 101 | pub struct StatusOptions { 102 | #[structopt( 103 | long = "export", 104 | help = "Export the tree to a file. Optionally include a target filename" 105 | )] 106 | pub export: Option>, 107 | 108 | #[structopt(flatten)] 109 | pub labels: LabelArgs, 110 | 111 | #[structopt(flatten)] 112 | pub meta: MetaArgs, 113 | 114 | #[structopt(flatten)] 115 | pub regex: RegexArgs, 116 | 117 | #[structopt( 118 | short = "s", 119 | long = "stats", 120 | help = "Display traversal statistics after the tree is displayed" 121 | )] 122 | pub statistics: bool, 123 | 124 | #[structopt(flatten)] 125 | pub style: StyleArgs, 126 | } 127 | -------------------------------------------------------------------------------- /src/cli/global.rs: -------------------------------------------------------------------------------- 1 | //! Providing arguments that are used throughout `nomad`. 2 | 3 | use structopt::StructOpt; 4 | 5 | #[derive(Debug, PartialEq, StructOpt)] 6 | pub struct GlobalArgs { 7 | #[structopt( 8 | long = "export", 9 | help = "Export the tree to a file. Optionally include a target filename" 10 | )] 11 | pub export: Option>, 12 | 13 | #[structopt(flatten)] 14 | pub labels: LabelArgs, 15 | 16 | #[structopt(flatten)] 17 | pub meta: MetaArgs, 18 | 19 | #[structopt(flatten)] 20 | pub modifiers: ModifierArgs, 21 | 22 | #[structopt(flatten)] 23 | pub regex: RegexArgs, 24 | 25 | #[structopt(flatten)] 26 | pub style: StyleArgs, 27 | 28 | #[structopt( 29 | short = "s", 30 | long = "stats", 31 | help = "Display traversal statistics after the tree is displayed" 32 | )] 33 | pub statistics: bool, 34 | } 35 | 36 | #[derive(Debug, PartialEq, StructOpt)] 37 | pub struct LabelArgs { 38 | #[structopt( 39 | short = "L", 40 | long = "all-labels", 41 | help = "Label both files and directories. Alias for `-n -l`" 42 | )] 43 | pub all_labels: bool, 44 | 45 | #[structopt( 46 | short = "l", 47 | long = "label-directories", 48 | help = "Label directories with characters" 49 | )] 50 | pub label_directories: bool, 51 | 52 | #[structopt( 53 | short = "n", 54 | long = "numbered", 55 | help = "Label directory items with numbers" 56 | )] 57 | pub numbers: bool, 58 | } 59 | 60 | #[derive(Debug, PartialEq, StructOpt)] 61 | pub struct MetaArgs { 62 | #[structopt( 63 | short = "m", 64 | long = "metadata", 65 | help = "Show item metadata such as file permissions, owner, group, file size, and last modified time" 66 | )] 67 | pub metadata: bool, 68 | 69 | #[structopt( 70 | long = "tokei", 71 | help = "Display code statistics (lines of code, blanks, and comments) for each item" 72 | )] 73 | pub tokei: bool, 74 | } 75 | 76 | #[derive(Debug, PartialEq, StructOpt)] 77 | pub struct ModifierArgs { 78 | #[structopt(long = "dirs", help = "Only display directories")] 79 | pub dirs: bool, 80 | 81 | #[structopt(long = "disrespect", help = "Disrespect all ignore rules")] 82 | pub disrespect: bool, 83 | 84 | #[structopt(long = "hidden", help = "Display hidden files")] 85 | pub hidden: bool, 86 | 87 | #[structopt(long = "max-depth", help = "Set the maximum depth to recurse")] 88 | pub max_depth: Option, 89 | 90 | #[structopt( 91 | long = "max-filesize", 92 | help = "Set the maximum filesize (in bytes) to include in the tree" 93 | )] 94 | pub max_filesize: Option, 95 | } 96 | 97 | #[derive(Debug, PartialEq, StructOpt)] 98 | pub struct RegexArgs { 99 | #[structopt( 100 | short = "p", 101 | long = "pattern", 102 | help = "Only display items matching this pattern. Supports regex expressions" 103 | )] 104 | pub pattern: Option, 105 | } 106 | 107 | #[derive(Debug, PartialEq, StructOpt)] 108 | pub struct StyleArgs { 109 | #[structopt(long = "no-colors", help = "Do not display any colors")] 110 | pub no_colors: bool, 111 | 112 | #[structopt(long = "no-git", help = "Do not display Git status markers")] 113 | pub no_git: bool, 114 | 115 | #[structopt(long = "no-icons", help = "Do not display icons")] 116 | pub no_icons: bool, 117 | 118 | #[structopt( 119 | long = "plain", 120 | help = "Mute icons, Git markers, and colors to display a plain tree" 121 | )] 122 | pub plain: bool, 123 | } 124 | -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | //! Defining command-line interface flags. 2 | 3 | pub mod config; 4 | pub mod filetype; 5 | pub mod git; 6 | pub mod global; 7 | pub mod releases; 8 | 9 | use structopt::StructOpt; 10 | 11 | use self::{ 12 | config::ConfigOptions, 13 | filetype::FileTypeOptions, 14 | git::GitOptions, 15 | global::GlobalArgs, 16 | releases::{ReleaseOptions, UpgradeOptions}, 17 | }; 18 | 19 | /// This struct contains all flags that are used in this program. 20 | #[derive(Debug, PartialEq, StructOpt)] 21 | #[structopt( 22 | name = "nomad", 23 | about = "The customizable next gen tree command with Git integration and TUI", 24 | author = "Joseph Lai" 25 | )] 26 | pub struct Args { 27 | #[structopt(long, help = "Display the banner")] 28 | pub banner: bool, 29 | 30 | #[structopt(help = "Display a tree for this directory")] 31 | pub directory: Option, 32 | 33 | #[structopt(flatten)] 34 | pub global: GlobalArgs, 35 | 36 | #[structopt(subcommand)] 37 | pub sub_commands: Option, 38 | } 39 | 40 | #[derive(Debug, PartialEq, StructOpt)] 41 | pub enum SubCommands { 42 | ///`bat` (the Rust alternative to the `cat` command) a file. 43 | /// This may be used after running nomad in a labeled mode. 44 | Bat { item_labels: Vec }, 45 | /// Customize/configure nomad or view your current configuration. 46 | /// 47 | /// Edit or view your settings defined in the self-instantiated configuration 48 | /// file `nomad.toml`. 49 | /// 50 | /// === NOTE === 51 | /// 52 | /// You DO NOT have to create this file yourself. nomad will create 53 | /// it for you if it does not already exist on your system. 54 | Config(ConfigOptions), 55 | /// Edit a file with your default $EDITOR or with Neovim, Vim, Vi, or Nano. 56 | /// This may be used after running nomad in a labeled mode. 57 | Edit { item_labels: Vec }, 58 | /// Filter directory items by filetype. 59 | Ft(FileTypeOptions), 60 | /// Run commonly used Git commands. 61 | /// Some commands may be used after running nomad in a labeled mode. 62 | /// 63 | /// Use the `-h`/`--help` flags to see the available options for each command. 64 | Git(GitOptions), 65 | /// Retrieve releases for this program (retrieved from GitHub). 66 | Releases(ReleaseOptions), 67 | /// Enter rootless (interactive) mode. 68 | Rootless, 69 | /// Run `tokei` (lines of code counter). 70 | Tokei, 71 | /// Upgrade nomad or just check if there is an upgrade available. 72 | Upgrade(UpgradeOptions), 73 | } 74 | 75 | /// Return the `Args` struct. 76 | pub fn get_args() -> Args { 77 | Args::from_args() 78 | } 79 | 80 | #[cfg(test)] 81 | mod test_cli { 82 | use assert_cmd::Command; 83 | 84 | #[test] 85 | fn test_invalid_arg() { 86 | Command::cargo_bin("nd") 87 | .unwrap() 88 | .arg("-q") 89 | .assert() 90 | .failure(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/cli/releases.rs: -------------------------------------------------------------------------------- 1 | //! Providing CLI options for `nomad` releases. 2 | 3 | use structopt::StructOpt; 4 | 5 | #[derive(Debug, PartialEq, StructOpt)] 6 | pub enum ReleaseOptions { 7 | /// List all releases. 8 | All, 9 | /// Display information for a release version. Optionally search for a release version. 10 | Info { release_version: Option }, 11 | } 12 | 13 | #[derive(Debug, PartialEq, StructOpt)] 14 | pub struct UpgradeOptions { 15 | /// Check if there is an upgrade available. Does not actually upgrade nomad. 16 | #[structopt( 17 | long, 18 | help = "Check if there is an upgrade available. Does not actually upgrade nomad" 19 | )] 20 | pub check: bool, 21 | } 22 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | //! Exposing functionality to customize `nomad`. 2 | 3 | pub mod models; 4 | pub mod preview; 5 | pub mod toml; 6 | -------------------------------------------------------------------------------- /src/config/models.rs: -------------------------------------------------------------------------------- 1 | //! Structs used when serializing/deserializing configuration settings from `nomad.toml`. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Contains all settings specified in `nomad.toml`. 6 | #[derive(Debug, Deserialize, Serialize)] 7 | pub struct NomadConfig { 8 | /// Contains settings for the standard tree. 9 | pub tree: Option, 10 | /// Contains settings for the TUI. 11 | pub tui: Option, 12 | } 13 | 14 | /// Contains settings for the standard tree. 15 | #[derive(Debug, Deserialize, Serialize)] 16 | pub struct TreeSettings { 17 | /// Contains settings for all things related to Git in the standard tree. 18 | pub git: Option, 19 | /// Contains settings for the color of tree labels (items and directories). 20 | pub labels: Option, 21 | /// Contains the indentation setting. 22 | pub indent: Option, 23 | /// Contains settings for the tree items' appearance. 24 | pub items: Option, 25 | /// Contains indent characters for the tree itself. 26 | pub indent_chars: Option, 27 | /// Contains the padding setting. 28 | pub padding: Option, 29 | /// Contains the setting for the color of the regex match. 30 | pub regex: Option, 31 | } 32 | 33 | /// Contains settings for the TUI. 34 | #[derive(Debug, Deserialize, Serialize)] 35 | pub struct TUISettings { 36 | /// Contains settings for all things related to Git in the TUI. 37 | pub git: Option, 38 | /// Contains settings for the TUI's style. 39 | pub style: Option, 40 | /// Contains the setting for the color of the regex match in the text view. 41 | pub regex: Option, 42 | } 43 | 44 | /// Contains settings for the tree items' appearance. 45 | #[derive(Debug, Deserialize, Serialize)] 46 | pub struct TreeItems { 47 | /// The colors for items in the tree. 48 | pub colors: Option, 49 | } 50 | 51 | /// Contains settings for tree items' appearance. 52 | #[derive(Debug, Deserialize, Serialize)] 53 | pub struct TreeItemColor { 54 | /// The color for directories. 55 | pub directory_color: Option, 56 | } 57 | 58 | /// Contains settings for the color of tree labels (items and directories). 59 | #[derive(Debug, Deserialize, Serialize)] 60 | pub struct LabelColors { 61 | /// The color for item labels. 62 | pub item_labels: Option, 63 | /// The color for directory labels. 64 | pub directory_labels: Option, 65 | } 66 | 67 | /// Contains indent characters for the tree itself. 68 | #[derive(Debug, Deserialize, Serialize)] 69 | pub struct IndentCharacters { 70 | /// The character used for pointing straight down. 71 | pub down: Option, 72 | /// The character used for pointing down and to the right. 73 | pub down_and_right: Option, 74 | /// The character used for empty sections. 75 | pub empty: Option, 76 | /// The character used for pointing right. 77 | pub right: Option, 78 | /// The character used for turning from down to right. 79 | pub turn_right: Option, 80 | } 81 | 82 | /// Contains settings for all things related to Git in the standard tree. 83 | #[derive(Debug, Deserialize, Serialize)] 84 | pub struct TreeGit { 85 | /// Contains settings for the color of each Git marker. 86 | pub colors: Option, 87 | /// Contains settings for each Git marker. 88 | pub markers: Option, 89 | } 90 | 91 | /// Contains the setting for the color of the regex match. 92 | #[derive(Debug, Deserialize, Serialize)] 93 | pub struct Regex { 94 | /// The color the matched substring. 95 | pub match_color: Option, 96 | } 97 | 98 | /// Contains settings for the TUI's style. 99 | #[derive(Debug, Deserialize, Serialize)] 100 | pub struct TUIStyle { 101 | /// The color of the borders. 102 | pub border_color: Option, 103 | /// The color of the tree item if it does not contain any Git changes. 104 | pub standard_item_highlight_color: Option, 105 | } 106 | 107 | /// Contains settings for all things related to Git in the TUI. 108 | #[derive(Debug, Deserialize, Serialize)] 109 | pub struct TUIGit { 110 | /// Contains settings for the color of each Git marker. 111 | pub colors: Option, 112 | } 113 | 114 | /// Contains settings for each Git marker. 115 | #[derive(Debug, Deserialize, Serialize)] 116 | pub struct Markers { 117 | /// The string that marks a conflicting file. 118 | pub conflicted_marker: Option, 119 | /// The string that marks a deleted file. 120 | pub deleted_marker: Option, 121 | /// The string that marks a modified file. 122 | pub modified_marker: Option, 123 | /// The string that marks a renamed file. 124 | pub renamed_marker: Option, 125 | /// The string that marks a staged added file. 126 | pub staged_added_marker: Option, 127 | /// The string that marks a staged deleted file. 128 | pub staged_deleted_marker: Option, 129 | /// The string that marks a staged modified file. 130 | pub staged_modified_marker: Option, 131 | /// The string that marks a staged renamed file. 132 | pub staged_renamed_marker: Option, 133 | /// The string that marks a staged typechanged file. 134 | pub staged_typechanged_marker: Option, 135 | /// The string that marks a typechanged file. 136 | pub typechanged_marker: Option, 137 | /// The string that marks an untracked file. 138 | pub untracked_marker: Option, 139 | } 140 | 141 | /// Contains settings for the color of each Git marker. 142 | #[derive(Debug, Deserialize, Serialize)] 143 | pub struct Colors { 144 | /// The color associated with conflicting files. 145 | pub conflicted_color: Option, 146 | /// The color associated with deleted files. 147 | pub deleted_color: Option, 148 | /// The color associated with modified files. 149 | pub modified_color: Option, 150 | /// The color associated with renamed files. 151 | pub renamed_color: Option, 152 | /// The color associated with staged added files. 153 | pub staged_added_color: Option, 154 | /// The color associated with staged deleted files. 155 | pub staged_deleted_color: Option, 156 | /// The color associated with staged modified files. 157 | pub staged_modified_color: Option, 158 | /// The color associated with staged renamed files. 159 | pub staged_renamed_color: Option, 160 | /// The color associated with staged typechanged files. 161 | pub staged_typechanged_color: Option, 162 | /// The color associated with typechanged files. 163 | pub typechanged_color: Option, 164 | /// The color associated with untracked files. 165 | pub untracked_color: Option, 166 | } 167 | -------------------------------------------------------------------------------- /src/config/preview.rs: -------------------------------------------------------------------------------- 1 | //! Create a dummy tree to preview the current settings. 2 | 3 | use ansi_term::{Colour, Style}; 4 | use anyhow::Result; 5 | use ptree::{print_tree_with, TreeBuilder}; 6 | 7 | use crate::{ 8 | errors::NomadError, 9 | style::models::NomadStyle, 10 | traverse::{format::highlight_matched, utils::build_tree_style}, 11 | }; 12 | 13 | /// Build a dummy tree with the current tree settings. 14 | pub fn display_preview_tree(nomad_style: &NomadStyle) -> Result<(), NomadError> { 15 | let mut tree = TreeBuilder::new(format!( 16 | "\u{e615} {}{}{}", // "" 17 | Style::new().bold().paint("["), 18 | Colour::Fixed(172).bold().paint("PREVIEW"), 19 | Style::new().bold().paint("]"), 20 | )); 21 | let config = build_tree_style(nomad_style); 22 | 23 | // Begin Git configuration branch. Doing these in alphabetical order. 24 | tree.begin_child(format!( 25 | "\u{f1d3} {}{}{}", // "" 26 | Style::new().bold().paint("["), 27 | Colour::Fixed(172).bold().paint("GIT"), 28 | Style::new().bold().paint("]"), 29 | )); 30 | 31 | // Working directory Git changes. 32 | tree.add_empty_child(format!( 33 | "{} \u{e204} conflicting file", // "" 34 | nomad_style 35 | .git 36 | .conflicted_color 37 | .paint(nomad_style.git.conflicted_marker.to_string()) 38 | )); 39 | tree.add_empty_child(format!( 40 | "{} \u{e61d} deleted file", // "" 41 | nomad_style 42 | .git 43 | .deleted_color 44 | .paint(nomad_style.git.deleted_marker.to_string()) 45 | )); 46 | tree.add_empty_child(format!( 47 | "{} \u{e7a8} modified file", // "" 48 | nomad_style 49 | .git 50 | .modified_color 51 | .paint(nomad_style.git.modified_marker.to_string()) 52 | )); 53 | tree.add_empty_child(format!( 54 | "{} \u{f48a} renamed file", // "" 55 | nomad_style 56 | .git 57 | .renamed_color 58 | .paint(nomad_style.git.renamed_marker.to_string()) 59 | )); 60 | tree.add_empty_child(format!( 61 | "{} \u{f17a} typechanged file", // "" 62 | nomad_style 63 | .git 64 | .typechanged_color 65 | .paint(nomad_style.git.typechanged_marker.to_string()) 66 | )); 67 | 68 | // Staged (index) Git changes. 69 | tree.add_empty_child(format!( 70 | "{} \u{e606} {}", // "" 71 | nomad_style 72 | .git 73 | .staged_added_color 74 | .paint(nomad_style.git.staged_added_marker.to_string()), 75 | nomad_style 76 | .git 77 | .staged_added_color 78 | .paint("staged added file") 79 | )); 80 | tree.add_empty_child(format!( 81 | "{} \u{e61d} {}", // "" 82 | nomad_style 83 | .git 84 | .staged_deleted_color 85 | .paint(nomad_style.git.staged_deleted_marker.to_string()), 86 | nomad_style 87 | .git 88 | .staged_deleted_color 89 | .strikethrough() 90 | .paint("staged deleted file") 91 | )); 92 | tree.add_empty_child(format!( 93 | "{} \u{e7a8} {}", // "" 94 | nomad_style 95 | .git 96 | .staged_modified_color 97 | .paint(nomad_style.git.staged_modified_marker.to_string()), 98 | nomad_style 99 | .git 100 | .staged_modified_color 101 | .paint("staged modified file") 102 | )); 103 | tree.add_empty_child(format!( 104 | "{} \u{f48a} {}", // "" 105 | nomad_style 106 | .git 107 | .staged_renamed_color 108 | .paint(nomad_style.git.staged_renamed_marker.to_string()), 109 | nomad_style 110 | .git 111 | .staged_renamed_color 112 | .paint("staged renamed file") 113 | )); 114 | tree.add_empty_child(format!( 115 | "{} \u{f17a} {}", // "" 116 | nomad_style 117 | .git 118 | .staged_typechanged_color 119 | .paint(nomad_style.git.staged_typechanged_marker.to_string()), 120 | nomad_style 121 | .git 122 | .staged_typechanged_color 123 | .paint("staged typechanged file") 124 | )); 125 | 126 | // Last working directory Git change. 127 | tree.add_empty_child(format!( 128 | "{} \u{e74e} untracked file", // "" 129 | nomad_style 130 | .git 131 | .untracked_color 132 | .paint(nomad_style.git.untracked_marker.to_string()) 133 | )); 134 | 135 | tree.end_child(); 136 | 137 | // Begin regex match branch. 138 | tree.begin_child(format!( 139 | "\u{e60b} {}{}{}", // "" 140 | Style::new().bold().paint("["), 141 | Colour::Fixed(172).bold().paint("REGEX"), 142 | Style::new().bold().paint("]"), 143 | )); 144 | tree.begin_child(format!( 145 | "\u{f115} {}", //  146 | highlight_matched(true, nomad_style, "directory match".to_string(), (5, 8),) 147 | )); 148 | tree.add_empty_child(format!( 149 | "\u{e7a8} {}", // "" 150 | highlight_matched(false, nomad_style, "item match".to_string(), (5, 8)) 151 | )); 152 | 153 | tree.end_child(); 154 | 155 | println!(); 156 | print_tree_with(&tree.build(), &config)?; 157 | 158 | Ok(()) 159 | } 160 | -------------------------------------------------------------------------------- /src/config/toml.rs: -------------------------------------------------------------------------------- 1 | //! Parse `nomad.toml`, the configuration file. 2 | 3 | use crate::errors::NomadError; 4 | 5 | use super::models::NomadConfig; 6 | 7 | use anyhow::Result; 8 | use directories::ProjectDirs; 9 | use toml::from_slice; 10 | 11 | use std::fs::{create_dir_all, read, write}; 12 | 13 | /// Parse the settings that are specified in `nomad.toml` into the `NomadConfig` struct. 14 | pub fn parse_config() -> Result<(NomadConfig, Option), NomadError> { 15 | if let Some(ref project_directory) = ProjectDirs::from("", "", "nomad") { 16 | let config_path = project_directory.config_dir().join("nomad.toml"); 17 | 18 | if !config_path.exists() { 19 | match &config_path.parent() { 20 | Some(parent) => create_dir_all(parent)?, 21 | None => { 22 | return Err(NomadError::PathError( 23 | "Could not get the path to nomad's application directory!".to_string(), 24 | )) 25 | } 26 | } 27 | 28 | write(&config_path, include_bytes!("../../nomad.toml"))?; 29 | } 30 | 31 | let config_contents = read(&config_path)?; 32 | 33 | Ok(( 34 | from_slice(&config_contents)?, 35 | config_path.to_str().map(|path| path.to_string()), 36 | )) 37 | } else { 38 | Err(NomadError::ApplicationError) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | //! Normalizing errors for `nomad`. 2 | 3 | use thiserror::Error; 4 | 5 | use std::io; 6 | 7 | /// Contains options for errors that may be raised throughout this program. 8 | #[derive(Debug, Error)] 9 | pub enum NomadError { 10 | /// Something went wrong when trying to `bat` a file. 11 | #[error("Bat error: {0}")] 12 | BatError(#[from] bat::error::Error), 13 | 14 | /// Something went wrong when trying to get the application-specific directories. 15 | #[error("Could not retrieve system application directories!")] 16 | ApplicationError, 17 | 18 | /// Something went wrong when opening a file with an editor. 19 | #[error("Unable to open the file with {editor}: {reason}")] 20 | EditorError { 21 | editor: String, 22 | #[source] 23 | reason: io::Error, 24 | }, 25 | 26 | /// A generic formatted error. 27 | #[error(transparent)] 28 | Error(#[from] anyhow::Error), 29 | 30 | /// An invalid target was entered when attempting to run the Git blame subcommand. 31 | #[error("`git blame` can only be called on a single file")] 32 | GitBlameError, 33 | 34 | /// Something went wrong when running Git subcommands. 35 | #[error("{context:?}: {source:?}")] 36 | GitError { 37 | context: String, 38 | #[source] 39 | source: git2::Error, 40 | }, 41 | 42 | /// Error for the `ignore` crate. 43 | #[error("Ignore error: {0}")] 44 | IgnoreError(#[from] ignore::Error), 45 | 46 | /// IO errors raised from the standard library (std::io). 47 | #[error("IO error: {0}")] 48 | IOError(#[from] io::Error), 49 | 50 | /// Something went wrong with the MPSC receiver. 51 | #[error("MPSC error: {0}")] 52 | MPSCError(#[from] std::sync::mpsc::RecvError), 53 | 54 | /// No items are available in Rootless mode. 55 | #[error("There are no items in this directory!")] 56 | NoItems, 57 | 58 | /// Nothing was found. 59 | #[error("No items were found!")] 60 | NothingFound, 61 | 62 | /// Nothing is currently selected in Rootless mode. 63 | #[error("Nothing is selected!")] 64 | NothingSelected, 65 | 66 | /// An invalid directory path is provided. 67 | #[error("{0} is not a directory!")] 68 | NotADirectory(String), 69 | 70 | /// Unable to edit the selected item because it is a directory. 71 | /// This is only used in Rootless mode. 72 | #[error("Unable to edit: The selected item is not a file!")] 73 | NotAFile, 74 | 75 | /// Raised when any path-related errors arise. 76 | #[error("Path error: {0}")] 77 | PathError(String), 78 | 79 | /// Plain Git error. 80 | #[error("{0}")] 81 | PlainGitError(#[from] git2::Error), 82 | 83 | /// Something went wrong while displaying a `ptree`. 84 | #[error("{context}: {source}")] 85 | PTreeError { 86 | context: String, 87 | #[source] 88 | source: io::Error, 89 | }, 90 | 91 | /// Something went wrong when compiling a regex expression. 92 | #[error("{0}")] 93 | RegexError(#[from] regex::Error), 94 | 95 | /// Something went wrong when self-updating. 96 | #[error("Self-upgrade error: {0}")] 97 | SelfUpgradeError(#[from] self_update::errors::Error), 98 | 99 | /// Something went wrong when doing something with Serde JSON. 100 | #[error("Serde JSON error: {0}")] 101 | SerdeJSONError(#[from] serde_json::Error), 102 | 103 | /// Something went wrong when deserializing/serializing the TOML config file. 104 | #[error("TOML error: {0}")] 105 | TOMLError(#[from] toml::de::Error), 106 | 107 | /// Something went wrong when decoding to UTF-8. 108 | #[error("UTF-8 error: {0}")] 109 | UTF8Error(#[from] std::str::Utf8Error), 110 | } 111 | -------------------------------------------------------------------------------- /src/git/commit.rs: -------------------------------------------------------------------------------- 1 | //! Commit staged changes in the Git repository. 2 | 3 | use ansi_term::Colour; 4 | use git2::{ObjectType, Repository}; 5 | 6 | use crate::{ 7 | errors::NomadError, 8 | git::{ 9 | diff::get_diff_stats, 10 | utils::{get_last_commit, get_repo_branch}, 11 | }, 12 | }; 13 | 14 | /// Commit the staged changes with an accompanying message if applicable. 15 | pub fn commit_changes(message: &Option, repo: &Repository) -> Result<(), NomadError> { 16 | match repo.signature() { 17 | Ok(signature) => { 18 | let checked_message = if let Some(message) = message { 19 | message.to_string() 20 | } else { 21 | "Updating".to_string() 22 | }; 23 | 24 | let mut index = repo.index()?; 25 | let staged_tree = repo.find_tree(index.write_tree()?)?; 26 | 27 | let previous_head = repo.head()?.peel(ObjectType::Tree)?.id(); 28 | 29 | let parent_commit = get_last_commit(repo)?; 30 | let commit_oid = repo 31 | .commit( 32 | Some("HEAD"), 33 | &signature, 34 | &signature, 35 | &checked_message, 36 | &staged_tree, 37 | &[&parent_commit], 38 | )? 39 | .to_string(); 40 | 41 | let branch_name = get_repo_branch(repo).unwrap_or_else(|| "?".to_string()); 42 | let branch = Colour::Green.bold().paint(branch_name).to_string(); 43 | 44 | let sliced_oid = &commit_oid[..7]; 45 | 46 | println!("\n[{branch} {sliced_oid}] {checked_message}\n"); 47 | 48 | let old_tree = repo.find_tree(previous_head)?; 49 | if let (Some(files_changed), Some(insertions), Some(deletions)) = 50 | get_diff_stats(&mut index, &old_tree, repo) 51 | { 52 | println!( 53 | "| {colored_changed} {changed_label} changed | {colored_insertions} {insertions_label} | {colored_deletions} {deletions_label} |\n", 54 | colored_changed = Colour::Fixed(172).bold().paint(format!("{files_changed}")), 55 | changed_label = if files_changed == 1 { "file" } else { "files" }, 56 | colored_insertions = Colour::Green.bold().paint(format!("+{insertions}")), 57 | insertions_label = if insertions == 1 { "insertion" } else { "insertions" }, 58 | colored_deletions = Colour::Red.bold().paint(format!("-{deletions}")), 59 | deletions_label = if deletions == 1 { "deletion" } else { "deletions" }, 60 | ); 61 | } 62 | 63 | Ok(()) 64 | } 65 | Err(error) => Err(NomadError::GitError { 66 | context: "Unable to commit changes without a Git signature".into(), 67 | source: error, 68 | }), 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/git/markers.rs: -------------------------------------------------------------------------------- 1 | //! Set Git status markers for items within the tree. 2 | 3 | use super::utils::get_repo; 4 | use crate::{cli::global::StyleArgs, errors::NomadError, style::models::NomadStyle}; 5 | 6 | use anyhow::Result; 7 | use git2::{Repository, Status, StatusOptions, StatusShow}; 8 | 9 | use std::{collections::HashMap, path::Path}; 10 | 11 | /// Try to extend the `HashMap` containing status markers and their corresponding 12 | /// filenames with new Git repository items. 13 | pub fn extend_marker_map( 14 | args: &StyleArgs, 15 | git_markers: &mut HashMap, 16 | nomad_style: &NomadStyle, 17 | target_directory: &str, 18 | ) { 19 | if let Some(repo) = get_repo(target_directory) { 20 | if let Ok(top_level_map) = get_status_markers(args, nomad_style, &repo, target_directory) { 21 | git_markers.extend(top_level_map); 22 | } 23 | } 24 | } 25 | 26 | /// Get the status markers (colored initials) that correspond with the Git status 27 | /// of tracked files in the repository. 28 | pub fn get_status_markers( 29 | args: &StyleArgs, 30 | nomad_style: &NomadStyle, 31 | repo: &Repository, 32 | target_directory: &str, 33 | ) -> Result, NomadError> { 34 | let mut status_options = StatusOptions::new(); 35 | status_options 36 | .show(StatusShow::IndexAndWorkdir) 37 | .include_untracked(true) 38 | .recurse_untracked_dirs(true); 39 | 40 | let mut formatted_items = HashMap::new(); 41 | 42 | for repo_item in repo.statuses(Some(&mut status_options))?.iter() { 43 | let item_name = repo 44 | .path() 45 | .parent() 46 | .unwrap_or_else(|| Path::new(target_directory)) 47 | .join(repo_item.path().unwrap_or("?")) 48 | .to_str() 49 | .unwrap_or("?") 50 | .to_string(); 51 | 52 | let items = { 53 | let marker = match repo_item.status() { 54 | s if s.contains(Status::INDEX_DELETED) => { 55 | if args.no_colors { 56 | nomad_style.git.staged_deleted_marker.clone() 57 | } else { 58 | nomad_style 59 | .git 60 | .staged_deleted_color 61 | .paint(&nomad_style.git.staged_deleted_marker) 62 | .to_string() 63 | } 64 | } 65 | s if s.contains(Status::INDEX_MODIFIED) => { 66 | if args.no_colors { 67 | nomad_style.git.staged_modified_marker.clone() 68 | } else { 69 | nomad_style 70 | .git 71 | .staged_modified_color 72 | .paint(&nomad_style.git.staged_modified_marker) 73 | .to_string() 74 | } 75 | } 76 | s if s.contains(Status::INDEX_NEW) => { 77 | if args.no_colors { 78 | nomad_style.git.staged_added_marker.clone() 79 | } else { 80 | nomad_style 81 | .git 82 | .staged_added_color 83 | .paint(&nomad_style.git.staged_added_marker) 84 | .to_string() 85 | } 86 | } 87 | s if s.contains(Status::INDEX_RENAMED) => { 88 | if args.no_colors { 89 | nomad_style.git.staged_renamed_marker.clone() 90 | } else { 91 | nomad_style 92 | .git 93 | .staged_renamed_color 94 | .paint(&nomad_style.git.staged_renamed_marker) 95 | .to_string() 96 | } 97 | } 98 | s if s.contains(Status::INDEX_TYPECHANGE) => { 99 | if args.no_colors { 100 | nomad_style.git.staged_typechanged_marker.clone() 101 | } else { 102 | nomad_style 103 | .git 104 | .staged_typechanged_color 105 | .paint(&nomad_style.git.staged_typechanged_marker) 106 | .to_string() 107 | } 108 | } 109 | s if s.contains(Status::WT_DELETED) => { 110 | if args.no_colors { 111 | nomad_style.git.deleted_marker.clone() 112 | } else { 113 | nomad_style 114 | .git 115 | .deleted_color 116 | .paint(&nomad_style.git.deleted_marker) 117 | .to_string() 118 | } 119 | } 120 | s if s.contains(Status::WT_MODIFIED) => { 121 | if args.no_colors { 122 | nomad_style.git.modified_marker.clone() 123 | } else { 124 | nomad_style 125 | .git 126 | .modified_color 127 | .paint(&nomad_style.git.modified_marker) 128 | .to_string() 129 | } 130 | } 131 | s if s.contains(Status::WT_NEW) => { 132 | if args.no_colors { 133 | nomad_style.git.untracked_marker.clone() 134 | } else { 135 | nomad_style 136 | .git 137 | .untracked_color 138 | .paint(&nomad_style.git.untracked_marker) 139 | .to_string() 140 | } 141 | } 142 | s if s.contains(Status::WT_RENAMED) => { 143 | if args.no_colors { 144 | nomad_style.git.renamed_marker.clone() 145 | } else { 146 | nomad_style 147 | .git 148 | .renamed_color 149 | .paint(&nomad_style.git.renamed_marker) 150 | .to_string() 151 | } 152 | } 153 | s if s.contains(Status::WT_TYPECHANGE) => { 154 | if args.no_colors { 155 | nomad_style.git.typechanged_marker.clone() 156 | } else { 157 | nomad_style 158 | .git 159 | .typechanged_color 160 | .paint(&nomad_style.git.typechanged_marker) 161 | .to_string() 162 | } 163 | } 164 | s if s.contains(Status::CONFLICTED) => { 165 | if args.no_colors { 166 | nomad_style.git.conflicted_marker.clone() 167 | } else { 168 | nomad_style 169 | .git 170 | .conflicted_color 171 | .paint(&nomad_style.git.conflicted_marker) 172 | .to_string() 173 | } 174 | } 175 | _ => "".to_string(), 176 | }; 177 | 178 | (item_name, marker) 179 | }; 180 | 181 | formatted_items.insert(items.0, items.1); 182 | } 183 | 184 | Ok(formatted_items) 185 | } 186 | -------------------------------------------------------------------------------- /src/git/mod.rs: -------------------------------------------------------------------------------- 1 | //! Exposing Git functionality. 2 | 3 | pub mod blame; 4 | pub mod branch; 5 | pub mod commit; 6 | pub mod diff; 7 | pub mod markers; 8 | pub mod status; 9 | pub mod trees; 10 | pub mod utils; 11 | -------------------------------------------------------------------------------- /src/git/status.rs: -------------------------------------------------------------------------------- 1 | //! Display the Git status command in tree form. 2 | 3 | use super::markers::get_status_markers; 4 | use crate::{ 5 | cli::{ 6 | git, 7 | global::{GlobalArgs, LabelArgs, MetaArgs, ModifierArgs, RegexArgs, StyleArgs}, 8 | }, 9 | errors::NomadError, 10 | style::models::NomadStyle, 11 | traverse::{ 12 | models::FoundItem, 13 | modes::NomadMode, 14 | traits::{ToTree, TransformFound}, 15 | }, 16 | }; 17 | 18 | use ansi_term::{Colour, Style}; 19 | use anyhow::{Result, __private}; 20 | use git2::{ObjectType, Repository}; 21 | use itertools::Itertools; 22 | use ptree::{item::StringItem, PrintConfig}; 23 | use regex::Regex; 24 | 25 | use std::{collections::HashMap, path::Path}; 26 | 27 | /// Build a tree that only contains items that are tracked in Git. 28 | pub fn display_status_tree( 29 | args: &git::StatusOptions, 30 | nomad_style: &NomadStyle, 31 | repo: &Repository, 32 | target_directory: &str, 33 | ) -> Result, NomadError> { 34 | // Hm... There is probably a better solution, but fuck it. Leaving it for now. 35 | let global_args = GlobalArgs { 36 | export: args.export.clone(), 37 | labels: LabelArgs { 38 | all_labels: args.labels.all_labels, 39 | label_directories: args.labels.label_directories, 40 | numbers: args.labels.numbers, 41 | }, 42 | meta: MetaArgs { 43 | metadata: args.meta.metadata, 44 | tokei: args.meta.tokei, 45 | }, 46 | modifiers: ModifierArgs { 47 | dirs: false, 48 | disrespect: false, 49 | hidden: false, 50 | max_depth: None, 51 | max_filesize: None, 52 | }, 53 | regex: RegexArgs { 54 | pattern: args.regex.pattern.clone(), 55 | }, 56 | style: StyleArgs { 57 | no_colors: args.style.no_colors, 58 | no_git: args.style.no_git, 59 | no_icons: args.style.no_icons, 60 | plain: args.style.plain, 61 | }, 62 | statistics: args.statistics, 63 | }; 64 | 65 | get_status_markers(&args.style, nomad_style, repo, target_directory).map_or_else( 66 | Err, 67 | |marker_map| { 68 | if marker_map.is_empty() { 69 | println!( 70 | "\n{}\n", 71 | Colour::Green 72 | .bold() 73 | .paint("Nothing to commit. Working tree clean.".to_string()) 74 | ); 75 | 76 | Ok(None) 77 | } else { 78 | Ok(Some(build_status_tree( 79 | &global_args, 80 | marker_map, 81 | nomad_style, 82 | target_directory, 83 | )?)) 84 | } 85 | }, 86 | ) 87 | } 88 | 89 | /// Get the number of commits ahead of `origin`. 90 | pub fn display_commits_ahead(branch_name: &str, repo: &Repository) -> Result<(), NomadError> { 91 | let head_oid = repo.head()?.peel(ObjectType::Commit)?.id(); 92 | 93 | let origin_branch = format!("origin/{branch_name}"); 94 | 95 | if let Ok(git_object) = repo.revparse_single(&origin_branch) { 96 | let last_commit_oid = git_object.id(); 97 | 98 | let (ahead, _behind) = repo.graph_ahead_behind(head_oid, last_commit_oid)?; 99 | 100 | if ahead > 0 { 101 | println!( 102 | "{} of {} by {} commit{plurality}.\n └── Run `{}` to publish your local changes.", 103 | Style::new().underline().paint("Ahead"), 104 | Colour::Blue.bold().paint(origin_branch), 105 | Colour::Green.bold().paint(format!("{}", ahead)), 106 | Style::new().bold().paint("git push"), 107 | plurality = if ahead > 1 { "s" } else { "" } 108 | ); 109 | } else { 110 | println!( 111 | "Up to date with {}.", 112 | Colour::Blue.bold().paint(origin_branch) 113 | ); 114 | } 115 | } else { 116 | println!( 117 | "{}", 118 | Colour::Fixed(172).bold().paint("No upstream branch found.") 119 | ); 120 | } 121 | 122 | Ok(()) 123 | } 124 | 125 | /// Traverse the repo and build the status tree. 126 | fn build_status_tree( 127 | args: &GlobalArgs, 128 | marker_map: HashMap, 129 | nomad_style: &NomadStyle, 130 | target_directory: &str, 131 | ) -> Result<(StringItem, PrintConfig), NomadError> { 132 | let regex_expression = if let Some(ref pattern) = args.regex.pattern { 133 | match Regex::new(&pattern.clone()) { 134 | Ok(regex) => Some(regex), 135 | Err(error) => return __private::Err(NomadError::RegexError(error)), 136 | } 137 | } else { 138 | None 139 | }; 140 | 141 | let (tree, config, _) = marker_map 142 | .iter() 143 | .filter_map(|(absolute_path, marker)| { 144 | if absolute_path.contains(target_directory) { 145 | match regex_expression { 146 | Some(ref regex) => match regex.find( 147 | Path::new(&absolute_path) 148 | .strip_prefix(target_directory) 149 | .unwrap_or_else(|_| Path::new("?")) 150 | .to_str() 151 | .unwrap_or("?"), 152 | ) { 153 | Some(matched) => Some(FoundItem { 154 | marker: Some(marker.to_string()), 155 | matched: Some((matched.start(), matched.end())), 156 | path: absolute_path.clone(), 157 | }), 158 | None => None, 159 | }, 160 | None => Some(FoundItem { 161 | marker: Some(marker.to_string()), 162 | matched: None, 163 | path: absolute_path.to_string(), 164 | }), 165 | } 166 | } else { 167 | None 168 | } 169 | }) 170 | .sorted_by_key(|found_item| found_item.path.to_string()) 171 | .collect::>() 172 | .transform(target_directory)? 173 | .to_tree(args, NomadMode::GitStatus, nomad_style, target_directory)?; 174 | 175 | Ok((tree, config)) 176 | } 177 | -------------------------------------------------------------------------------- /src/git/trees.rs: -------------------------------------------------------------------------------- 1 | //! Modify the Git trees - stages or restores files. 2 | 3 | use std::path::Path; 4 | 5 | use ansi_term::Colour; 6 | use git2::{build::CheckoutBuilder, Error, Index, IndexAddOption, Repository, Tree}; 7 | 8 | use crate::{ 9 | cli::Args, 10 | style::models::NomadStyle, 11 | utils::search::{indiscriminate_search, SearchMode}, 12 | }; 13 | 14 | /// Contains variants for stage/unstage/restore modes. 15 | pub enum TreeMode { 16 | /// Stage specified files from the working directory into the index. 17 | Stage, 18 | /// Stage all modified, deleted, or untracked files from the working directory 19 | /// into the index. 20 | StageAll, 21 | /// Restore files in the working directory back to their clean Git state. 22 | RestoreWorkingDirectory, 23 | } 24 | 25 | /// Modify the Git trees to stage/unstage/restore files. 26 | /// 27 | /// This function may do any of the following: 28 | /// * Adds new or modified files to the current index. 29 | /// * Restores staged files from the staging area to the index (unstage a file). 30 | /// * Restores modified files from the working directory to its clean state. 31 | /// 32 | pub fn modify_trees( 33 | args: &Args, 34 | item_labels: &[String], 35 | nomad_style: &NomadStyle, 36 | repo: &Repository, 37 | stage_mode: TreeMode, 38 | target_directory: &str, 39 | ) -> Result<(), Error> { 40 | let head_tree = repo.head()?.peel_to_tree()?; 41 | let mut index = repo.index()?; 42 | 43 | let mut staged_files = 0; 44 | match stage_mode { 45 | TreeMode::StageAll => { 46 | index.add_all(["*"].iter(), IndexAddOption::DEFAULT, None)?; 47 | index.write()?; 48 | 49 | println!("\n{}\n", Colour::Green.bold().paint("Staged all files")); 50 | } 51 | _ => { 52 | let found_items = indiscriminate_search( 53 | args, 54 | item_labels, 55 | nomad_style, 56 | Some(repo), 57 | SearchMode::Git, 58 | target_directory, 59 | ); 60 | 61 | if let Some(found_items) = found_items { 62 | for item in found_items { 63 | let target_file = Path::new(&item); 64 | let relative_path = match Path::new(&item).strip_prefix(target_directory) { 65 | Ok(prefix_stripped) => prefix_stripped, 66 | Err(_) => target_file, 67 | }; 68 | 69 | match stage_mode { 70 | TreeMode::Stage => { 71 | if index.add_path(relative_path).is_err() { 72 | index.remove_path(relative_path)?; 73 | } 74 | 75 | staged_files += 1; 76 | } 77 | TreeMode::RestoreWorkingDirectory => { 78 | restore_file( 79 | &head_tree, 80 | &mut index, 81 | relative_path, 82 | repo, 83 | &mut staged_files, 84 | )?; 85 | } 86 | _ => {} 87 | } 88 | } 89 | } 90 | 91 | if staged_files > 0 { 92 | index.write()?; 93 | 94 | let info = match stage_mode { 95 | TreeMode::Stage => "Staged", 96 | TreeMode::RestoreWorkingDirectory => "Restored", 97 | _ => "", 98 | }; 99 | 100 | println!( 101 | "\n{} {} {}\n", 102 | info, 103 | Colour::Green.bold().paint(format!("{staged_files}")), 104 | if staged_files == 1 { "item" } else { "items" } 105 | ); 106 | } else { 107 | println!("{}\n", Colour::Red.bold().paint("No items were staged!")); 108 | } 109 | } 110 | } 111 | 112 | Ok(()) 113 | } 114 | 115 | /// Restore a file to its working directory or clean state. 116 | fn restore_file( 117 | head_tree: &Tree, 118 | index: &mut Index, 119 | relative_path: &Path, 120 | repo: &Repository, 121 | staged_files: &mut i32, 122 | ) -> Result<(), Error> { 123 | if head_tree 124 | .get_name(relative_path.to_str().unwrap_or("?")) 125 | .is_some() 126 | { 127 | let mut checkout_options = CheckoutBuilder::new(); 128 | checkout_options.force(); 129 | checkout_options.path(relative_path); 130 | 131 | repo.checkout_head(Some(&mut checkout_options))?; 132 | *staged_files += 1; 133 | } else { 134 | // Indicates this file was untracked prior to adding it to the index. 135 | index.remove_path(relative_path)?; 136 | *staged_files += 1; 137 | } 138 | 139 | Ok(()) 140 | } 141 | -------------------------------------------------------------------------------- /src/git/utils.rs: -------------------------------------------------------------------------------- 1 | //! Contains useful utilities that support Git functionality. 2 | 3 | use std::{ffi::OsStr, path::Path}; 4 | 5 | use crate::{errors::NomadError, style::models::NomadStyle}; 6 | 7 | use ansi_term::{Colour, Style}; 8 | use anyhow::{anyhow, Result}; 9 | use git2::{Branch, Commit, ObjectType, Repository}; 10 | 11 | /// Try to discover a Git repository at or above the current path. 12 | fn discover_repo(target_directory: &str) -> Option { 13 | if let Ok(repo) = Repository::discover(target_directory) { 14 | if repo.is_bare() { 15 | println!("\n{}", Colour::Fixed(172).paint("Git repository is bare!")); 16 | None 17 | } else { 18 | Some(repo) 19 | } 20 | } else { 21 | None 22 | } 23 | } 24 | 25 | /// Try to get Git metadata from the target directory. 26 | pub fn get_repo(target_directory: &str) -> Option { 27 | if let Ok(repo) = Repository::open(target_directory) { 28 | if repo.is_bare() { 29 | println!("\n{}", Colour::Fixed(172).paint("Git repository is bare!")); 30 | None 31 | } else { 32 | Some(repo) 33 | } 34 | } else { 35 | discover_repo(target_directory) 36 | } 37 | } 38 | 39 | /// Try to get the current Git branch's name. 40 | pub fn get_repo_branch(repo: &Repository) -> Option { 41 | if let Ok(reference) = repo.head() { 42 | if let Ok(Some(name)) = Branch::wrap(reference).name() { 43 | let branch_name = name.to_string(); 44 | Some(branch_name) 45 | } else { 46 | println!( 47 | "\n{}\n", 48 | Colour::Red 49 | .bold() 50 | .paint("Could not get the current Git branch name!") 51 | ); 52 | None 53 | } 54 | } else { 55 | println!( 56 | "\n{}\n", 57 | Colour::Red.bold().paint("Could not get repository HEAD!") 58 | ); 59 | None 60 | } 61 | } 62 | 63 | /// Get the last commit in the Git repository. 64 | pub fn get_last_commit(repo: &Repository) -> Result { 65 | let object = repo.head()?.resolve()?.peel(ObjectType::Commit)?; 66 | object.into_commit().map_err(|_| { 67 | NomadError::Error(anyhow!( 68 | "Could not find the last commit in this Git repository!" 69 | )) 70 | }) 71 | } 72 | 73 | /// Add color/style to the filename depending on its Git status. 74 | pub fn paint_git_item( 75 | filename: &str, 76 | marker: &str, 77 | nomad_style: &NomadStyle, 78 | matched: Option<(usize, usize)>, 79 | ) -> String { 80 | let staged_deleted = &nomad_style 81 | .git 82 | .staged_deleted_color 83 | .paint(&nomad_style.git.staged_deleted_marker) 84 | .to_string(); 85 | let staged_modified = &nomad_style 86 | .git 87 | .staged_modified_color 88 | .paint(&nomad_style.git.staged_modified_marker) 89 | .to_string(); 90 | let staged_added = &nomad_style 91 | .git 92 | .staged_added_color 93 | .paint(&nomad_style.git.staged_added_marker) 94 | .to_string(); 95 | let staged_renamed = &nomad_style 96 | .git 97 | .staged_renamed_color 98 | .paint(&nomad_style.git.staged_renamed_marker) 99 | .to_string(); 100 | let conflicted = &nomad_style 101 | .git 102 | .conflicted_color 103 | .paint(&nomad_style.git.conflicted_marker) 104 | .to_string(); 105 | 106 | let style = match marker.to_string() { 107 | _ if marker == staged_added => nomad_style.git.staged_added_color, 108 | _ if marker == staged_deleted => nomad_style.git.staged_deleted_color.strikethrough(), 109 | _ if marker == staged_modified => nomad_style.git.staged_modified_color, 110 | _ if marker == staged_renamed => nomad_style.git.staged_renamed_color, 111 | _ if marker == conflicted => nomad_style.git.conflicted_color, 112 | _ => Style::new(), 113 | }; 114 | 115 | paint_with_highlight(filename, matched, nomad_style, style) 116 | } 117 | 118 | /// Highlight the filename in the Git status color. Also paint the pattern if a 119 | /// pattern is matched. 120 | fn paint_with_highlight( 121 | filename: &str, 122 | matched: Option<(usize, usize)>, 123 | nomad_style: &NomadStyle, 124 | style: Style, 125 | ) -> String { 126 | let painted_file = match matched { 127 | Some(ranges) => { 128 | if (0..filename.len()).contains(&ranges.0) 129 | && (0..filename.len() + 1).contains(&ranges.1) 130 | { 131 | let mut painted_prefix = filename[..ranges.0] 132 | .chars() 133 | .into_iter() 134 | .map(|character| style.paint(format!("{character}")).to_string()) 135 | .collect::>(); 136 | let mut painted_matched = filename[ranges.0..ranges.1] 137 | .chars() 138 | .into_iter() 139 | .map(|character| { 140 | nomad_style 141 | .tree 142 | .regex 143 | .match_color 144 | .paint(format!("{character}")) 145 | .to_string() 146 | }) 147 | .collect::>(); 148 | let mut painted_suffix = filename[ranges.1..] 149 | .chars() 150 | .into_iter() 151 | .map(|character| style.paint(format!("{character}")).to_string()) 152 | .collect::>(); 153 | 154 | painted_prefix.append(&mut painted_matched); 155 | painted_prefix.append(&mut painted_suffix); 156 | 157 | painted_prefix.join("") 158 | } else { 159 | filename 160 | .chars() 161 | .into_iter() 162 | .map(|character| style.paint(format!("{character}")).to_string()) 163 | .collect::>() 164 | .join("") 165 | } 166 | } 167 | None => filename 168 | .chars() 169 | .into_iter() 170 | .map(|character| style.paint(format!("{character}")).to_string()) 171 | .collect::>() 172 | .join(""), 173 | }; 174 | 175 | let filename = Path::new(&painted_file) 176 | .file_name() 177 | .unwrap_or_else(|| OsStr::new("?")) 178 | .to_str() 179 | .unwrap_or("?") 180 | .to_string(); 181 | 182 | filename 183 | } 184 | -------------------------------------------------------------------------------- /src/loc/format.rs: -------------------------------------------------------------------------------- 1 | //! Format `tokei` file metadata for tree or table views. 2 | 3 | use ansi_term::{Colour, Style}; 4 | use tokei::Report; 5 | 6 | use crate::cli::global::GlobalArgs; 7 | 8 | /// Contains formatted strings for a file's individual tokei metadata. 9 | pub struct TokeiTreeStats { 10 | /// The formatted string indicating the number of blank lines in this file. 11 | pub blanks: String, 12 | /// The formatted string indicating the lines of code in this file. 13 | pub code: String, 14 | /// The formatted string indicating the number of comments in this file. 15 | pub comments: String, 16 | /// The total number of lines in this file. 17 | pub lines: String, 18 | } 19 | 20 | /// Format the file's complimentary `Report` for normal/tree view. 21 | pub fn tree_stats_from_report( 22 | args: &GlobalArgs, 23 | report: Option<&'_ Report>, 24 | ) -> Option { 25 | report.map(|metadata| TokeiTreeStats { 26 | blanks: if args.style.no_colors || args.style.plain { 27 | format!("| Blanks {}", metadata.stats.blanks) 28 | } else { 29 | format!( 30 | "{} Blanks {}", 31 | Style::new().bold().paint("|"), 32 | Colour::Fixed(030) 33 | .bold() 34 | .paint(format!("{}", metadata.stats.blanks)) 35 | ) 36 | }, 37 | code: if args.style.no_colors || args.style.plain { 38 | format!("| Code {}", metadata.stats.code) 39 | } else { 40 | format!( 41 | "{} Code {}", 42 | Style::new().bold().paint("|"), 43 | Colour::Fixed(030) 44 | .bold() 45 | .paint(format!("{}", metadata.stats.code)) 46 | ) 47 | }, 48 | comments: if args.style.no_colors || args.style.plain { 49 | format!("| Comments {}", metadata.stats.comments) 50 | } else { 51 | format!( 52 | "{} Comments {}", 53 | Style::new().bold().paint("|"), 54 | Colour::Fixed(030) 55 | .bold() 56 | .paint(format!("{}", metadata.stats.comments)) 57 | ) 58 | }, 59 | lines: if args.style.no_colors || args.style.plain { 60 | format!("| Lines {}", metadata.stats.lines()) 61 | } else { 62 | format!( 63 | "{} Lines {}", 64 | Style::new().bold().paint("|"), 65 | Colour::Fixed(030) 66 | .bold() 67 | .paint(format!("{}", metadata.stats.lines())) 68 | ) 69 | }, 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /src/loc/mod.rs: -------------------------------------------------------------------------------- 1 | //! Exposing functionality to `Tokei` - counts lines of code and other file metadata. 2 | 3 | pub mod format; 4 | pub mod utils; 5 | 6 | use std::path::PathBuf; 7 | 8 | use ansi_term::{Colour, Style}; 9 | use term_table::{ 10 | row::Row, 11 | table_cell::{Alignment, TableCell}, 12 | Table, TableStyle, 13 | }; 14 | use tokei::{Config, Language, Languages, Sort}; 15 | 16 | use crate::cli::global::GlobalArgs; 17 | 18 | use self::{format::tree_stats_from_report, utils::get_file_report}; 19 | 20 | // FUTURE: Add a table in `nomad.toml` called `[tokei]` to set the `Config` 21 | // and ignored paths. 22 | 23 | /// Get the `Language` struct for a directory. 24 | pub fn loc_in_dir(target_directory: &str) -> Language { 25 | let mut languages = Languages::new(); 26 | languages.get_statistics(&[target_directory], &[], &Config::default()); 27 | 28 | languages.total() 29 | } 30 | 31 | /// Get the `CodeStats` for a single file from the `Language` struct. 32 | pub fn loc_in_file(args: &GlobalArgs, file_path: &str, tokei: &Language) -> Vec { 33 | let report = get_file_report(&tokei.children, PathBuf::from(file_path)); 34 | 35 | let mut formatted_stats = Vec::new(); 36 | 37 | match tree_stats_from_report(args, report) { 38 | Some(stats) => { 39 | formatted_stats.push(stats.blanks); 40 | formatted_stats.push(stats.code); 41 | formatted_stats.push(stats.comments); 42 | formatted_stats.push(stats.lines); 43 | } 44 | None => formatted_stats.push(if args.style.no_colors || args.style.plain { 45 | "| No tokei data available".to_string() 46 | } else { 47 | format!( 48 | "{} {}", 49 | Style::new().bold().paint("|"), 50 | Colour::Fixed(172).bold().paint("No tokei data available") 51 | ) 52 | }), 53 | } 54 | 55 | formatted_stats 56 | } 57 | 58 | /// Summarize the `Tokei` stats for this directory. 59 | /// Sort summary by lines of code, descending. 60 | pub fn run_tokei(target_directory: &str) { 61 | let mut languages = Languages::new(); 62 | let config = Config { 63 | sort: Some(Sort::Code), // Why doesn't this fucking work? 64 | ..Config::default() 65 | }; 66 | 67 | languages.get_statistics(&[target_directory], &[], &config); 68 | 69 | let mut summary = languages.total(); 70 | 71 | if summary.is_empty() { 72 | println!( 73 | "\n{}\n", 74 | Colour::Red 75 | .bold() 76 | .paint("No tokei data available for this directory.") 77 | ); 78 | } else { 79 | if summary.inaccurate { 80 | println!( 81 | "{}", 82 | Colour::Fixed(172).bold().paint( 83 | "Tokei encountered issues during parsing.\nThis data may not be accurate.\n" 84 | ) 85 | ); 86 | } 87 | 88 | summary.sort_by(Sort::Code); // This doesn't work either?? I'm triggered it won't sort by code. 89 | 90 | display_summary_table(summary); 91 | } 92 | } 93 | 94 | /// Create a table containing `Tokei` data. 95 | fn display_summary_table(summary: Language) { 96 | let mut table = Table::new(); 97 | 98 | table.max_column_width = 300; 99 | table.separate_rows = false; 100 | table.style = TableStyle::empty(); 101 | 102 | let mut headers = vec![TableCell::new( 103 | Colour::White.bold().paint("Language").to_string(), 104 | )]; 105 | headers.extend( 106 | vec!["Files", "Lines", "Code", "Comments", "Blanks"] 107 | .iter() 108 | .map(|header| { 109 | let mut cell = TableCell::new( 110 | Colour::White 111 | .bold() 112 | .paint(&(*header).to_string()) 113 | .to_string(), 114 | ); 115 | cell.alignment = Alignment::Right; 116 | 117 | cell 118 | }) 119 | .collect::>(), 120 | ); 121 | 122 | let header_row = Row::new(headers); 123 | table.add_row(header_row); 124 | table.add_row(Row::new(vec![" ", " ", " ", " ", " ", " "])); 125 | 126 | let (mut total_blanks, mut total_code, mut total_comments, mut total_files, mut total_lines) = 127 | (0, 0, 0, 0, 0); 128 | for (language_type, reports) in summary.children { 129 | let (mut blanks, mut code, mut comments, mut files, mut lines) = (0, 0, 0, 0, 0); 130 | 131 | for report in reports { 132 | blanks += report.stats.blanks; 133 | code += report.stats.code; 134 | comments += report.stats.comments; 135 | lines += report.stats.lines(); 136 | files += 1; 137 | } 138 | 139 | let mut data = vec![TableCell::new(language_type.to_string())]; 140 | data.extend( 141 | vec![ 142 | format!("{files}"), 143 | format!("{lines}"), 144 | code.to_string(), 145 | format!("{comments}"), 146 | format!("{blanks}"), 147 | ] 148 | .iter() 149 | .map(|data| { 150 | let mut cell = TableCell::new(data); 151 | cell.alignment = Alignment::Right; 152 | 153 | cell 154 | }) 155 | .collect::>(), 156 | ); 157 | 158 | table.add_row(Row::new(data)); 159 | 160 | total_blanks += blanks; 161 | total_code += code; 162 | total_comments += comments; 163 | total_files += files; 164 | total_lines += lines; 165 | } 166 | 167 | table.add_row(Row::new(vec![" ", " ", " ", " ", " ", " "])); 168 | 169 | let mut totals = vec![TableCell::new( 170 | Colour::White.bold().paint("Total").to_string(), 171 | )]; 172 | totals.extend( 173 | vec![ 174 | format!("{total_files}"), 175 | format!("{total_lines}"), 176 | format!("{total_code}"), 177 | format!("{total_comments}"), 178 | format!("{total_blanks}"), 179 | ] 180 | .iter() 181 | .map(|total| { 182 | let mut cell = TableCell::new(Colour::White.bold().paint(total).to_string()); 183 | cell.alignment = Alignment::Right; 184 | 185 | cell 186 | }) 187 | .collect::>(), 188 | ); 189 | 190 | table.add_row(Row::new(totals)); 191 | 192 | println!("\n{}", table.render()); 193 | } 194 | -------------------------------------------------------------------------------- /src/loc/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for counting lines of code. 2 | 3 | use std::{collections::BTreeMap, path::PathBuf}; 4 | 5 | use tokei::{Config, LanguageType, Report}; 6 | 7 | /// Get the `Report` for a specific file. 8 | /// 9 | /// ```rust 10 | /// tokei::Report { 11 | /// /// The code statistics found in the file. 12 | /// pub stats: CodeStats, 13 | /// /// File name. 14 | /// pub name: PathBuf, 15 | /// } 16 | /// ``` 17 | pub fn get_file_report( 18 | language_children: &BTreeMap>, 19 | path: PathBuf, 20 | ) -> Option<&'_ Report> { 21 | match LanguageType::from_path(&path, &Config::default()) { 22 | Some(language_type) => match language_children.get(&language_type) { 23 | Some(reports) => reports.iter().find(|report| report.name == path), 24 | None => None, 25 | }, 26 | None => None, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | //! Structs used when serializing/deserializing data from JSON. 2 | 3 | use std::collections::HashMap; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | 7 | /// Store all directory items. 8 | #[derive(Debug, Deserialize, Serialize)] 9 | pub struct Contents { 10 | /// Contains labeled directory paths. 11 | pub labeled: HashMap, 12 | /// Contains numbered directory items. 13 | pub numbered: HashMap, 14 | } 15 | -------------------------------------------------------------------------------- /src/releases/mod.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for self-updating `nomad`. 2 | 3 | use crate::errors::NomadError; 4 | 5 | use ansi_term::Colour; 6 | use anyhow::Result; 7 | use indicatif::{ProgressFinish, ProgressStyle}; 8 | use self_update::{ 9 | backends::github::{ReleaseList, Update}, 10 | cargo_crate_version, 11 | update::Release, 12 | }; 13 | 14 | use std::borrow::Cow; 15 | 16 | /// Check for updates. An update is only displayed if there is a working internet 17 | /// connection, if checking the GitHub repository is successful, and if there is 18 | /// an update available. 19 | pub fn check_for_update() -> Result<(), NomadError> { 20 | let releases = ReleaseList::configure() 21 | .repo_name("nomad") 22 | .repo_owner("JosephLai241") 23 | .build()? 24 | .fetch(); 25 | 26 | match releases { 27 | Ok(mut releases) => { 28 | let latest_release = releases.pop(); 29 | 30 | if let Some(latest) = latest_release { 31 | if latest.version != *env!("CARGO_PKG_VERSION") { 32 | println!( 33 | "\nNew release available! {} ==> {}\nRun `nd upgrade` to upgrade to the newest version.\n", 34 | Colour::Red.bold().paint(cargo_crate_version!()), 35 | Colour::Green.bold().paint(latest.version) 36 | ); 37 | } else { 38 | println!( 39 | "{}", 40 | Colour::Green 41 | .bold() 42 | .paint("\nYou are using the latest version of nomad! 💯\n") 43 | ) 44 | } 45 | } 46 | 47 | Ok(()) 48 | } 49 | Err(error) => Err(NomadError::SelfUpgradeError(error)), 50 | } 51 | } 52 | 53 | /// Return a list of `Release` objects containing release information. 54 | pub fn build_release_list() -> Result, NomadError> { 55 | Ok(ReleaseList::configure() 56 | .repo_name("nomad") 57 | .repo_owner("JosephLai241") 58 | .build()? 59 | .fetch()?) 60 | } 61 | 62 | /// Update `nomad`. 63 | pub fn update_self() -> Result<(), NomadError> { 64 | let current_version = cargo_crate_version!(); 65 | 66 | let update_status = Update::configure() 67 | .bin_name("nd") 68 | .current_version(cargo_crate_version!()) 69 | .repo_name("nomad") 70 | .repo_owner("JosephLai241") 71 | .show_download_progress(true) 72 | .set_progress_style( 73 | ProgressStyle::default_bar().on_finish(ProgressFinish::WithMessage(Cow::from("💯"))), 74 | ) 75 | .build()? 76 | .update()?; 77 | 78 | if update_status.updated() { 79 | println!( 80 | "\nSuccessfully updated nomad from {} to {}!\n", 81 | Colour::Fixed(172).bold().paint(current_version.to_string()), 82 | Colour::Green 83 | .bold() 84 | .paint(update_status.version().to_string()) 85 | ); 86 | } else { 87 | println!( 88 | "\n{}\n", 89 | Colour::Fixed(172) 90 | .bold() 91 | .paint("Already at the newest version.") 92 | ); 93 | } 94 | 95 | Ok(()) 96 | } 97 | -------------------------------------------------------------------------------- /src/style/mod.rs: -------------------------------------------------------------------------------- 1 | //! Styles for `nomad`. 2 | 3 | pub mod models; 4 | pub mod paint; 5 | pub mod settings; 6 | -------------------------------------------------------------------------------- /src/style/models.rs: -------------------------------------------------------------------------------- 1 | //! Struct used to store colors/styles for `nomad`. 2 | 3 | use ansi_term::{Colour, Style}; 4 | use ptree::print_config::UTF_CHARS; 5 | use tui::style::Color; 6 | 7 | /// Contains styles used throughout `nomad`. 8 | #[derive(Debug)] 9 | pub struct NomadStyle { 10 | /// The color and marker styles for all things Git. 11 | pub git: GitStyle, 12 | /// The styles for the tree. 13 | pub tree: TreeStyle, 14 | /// The color styles for all things TUI. 15 | pub tui: TUIStyle, 16 | } 17 | 18 | /// Contains color and marker styles for all things Git. 19 | #[derive(Debug)] 20 | pub struct GitStyle { 21 | /// The color of the conflicting file's marker. 22 | pub conflicted_color: Style, 23 | /// The string that marks a conflicting file. 24 | pub conflicted_marker: String, 25 | /// The color of the deleted file's marker. 26 | pub deleted_color: Style, 27 | /// The string that marks a deleted file. 28 | pub deleted_marker: String, 29 | /// The color of the modified file's marker. 30 | pub modified_color: Style, 31 | /// The string that marks a modified file. 32 | pub modified_marker: String, 33 | /// The color of the renamed file's marker. 34 | pub renamed_color: Style, 35 | /// The string that marks a renamed file. 36 | pub renamed_marker: String, 37 | /// The color of the staged added file. 38 | pub staged_added_color: Style, 39 | /// The string that marks a staged added file. 40 | pub staged_added_marker: String, 41 | /// The color of the staged deleted file. 42 | pub staged_deleted_color: Style, 43 | /// The string that marks a staged deleted file. 44 | pub staged_deleted_marker: String, 45 | /// The color of the staged modified file. 46 | pub staged_modified_color: Style, 47 | /// The string that marks a staged modified file. 48 | pub staged_modified_marker: String, 49 | /// The color of the staged renamed file. 50 | pub staged_renamed_color: Style, 51 | /// The string that marks a staged renamed file. 52 | pub staged_renamed_marker: String, 53 | /// The color that marks a staged typechanged file. 54 | pub staged_typechanged_color: Style, 55 | /// The string that marks a staged typechanged file. 56 | pub staged_typechanged_marker: String, 57 | /// The color that marks a typechanged file. 58 | pub typechanged_color: Style, 59 | /// The string that marks a typechanged file. 60 | pub typechanged_marker: String, 61 | /// The color of the untracked file's marker. 62 | pub untracked_color: Style, 63 | /// The string that marks an untracked file. 64 | pub untracked_marker: String, 65 | } 66 | 67 | /// Contains styles for the tree itself. 68 | #[derive(Debug)] 69 | pub struct TreeStyle { 70 | /// Contains the indentation setting. 71 | pub indent: usize, 72 | /// Contains indent characters for the tree itself. 73 | pub indent_chars: IndentStyles, 74 | /// Contains the colors for items in the tree. 75 | pub item_colors: ItemColors, 76 | /// Contains colors for the tree labels. 77 | pub label_colors: LabelColors, 78 | /// Contains the padding setting. 79 | pub padding: usize, 80 | /// The color styles for all things regex. 81 | pub regex: TreeRegexStyle, 82 | } 83 | 84 | /// Contains the colors for items in the tree. 85 | #[derive(Debug)] 86 | pub struct ItemColors { 87 | /// The color for directories. 88 | pub directory_color: Style, 89 | } 90 | 91 | /// Contains colors for the tree labels. 92 | #[derive(Debug)] 93 | pub struct LabelColors { 94 | /// The color for item labels. 95 | pub item_labels: Style, 96 | /// The color for directory labels. 97 | pub directory_labels: Style, 98 | } 99 | 100 | /// Contains the indent characters for the tree itself. 101 | #[derive(Debug)] 102 | pub struct IndentStyles { 103 | /// The character used for pointing straight down. 104 | pub down: String, 105 | /// The character used for pointing down and to the right. 106 | pub down_and_right: String, 107 | /// The character used for empty sections. 108 | pub empty: String, 109 | /// The character used for pointing right. 110 | pub right: String, 111 | /// The character used for turning from down to right. 112 | pub turn_right: String, 113 | } 114 | 115 | /// Contains color and marker styles for regex matches in the standard tree. 116 | #[derive(Debug)] 117 | pub struct TreeRegexStyle { 118 | /// The color of the matched substring. 119 | pub match_color: Style, 120 | } 121 | 122 | /// Contains color and marker styles for all things TUI. 123 | #[derive(Debug)] 124 | pub struct TUIStyle { 125 | /// The color of all widget borders. 126 | pub border_color: Color, 127 | /// Contains the Git styles for the TUI. 128 | pub git: TUIGitStyle, 129 | /// The color styles for all things regex. 130 | pub regex: TUIRegexStyle, 131 | /// The color of the tree item if it does not contain any Git changes. 132 | pub standard_item_highlight_color: Color, 133 | } 134 | 135 | /// Contains the Git styles for the TUI. 136 | #[derive(Debug)] 137 | pub struct TUIGitStyle { 138 | /// The color of the conflicting file's marker. 139 | pub conflicted_color: Color, 140 | /// The color of the deleted file's marker. 141 | pub deleted_color: Color, 142 | /// The color of the modified file's marker. 143 | pub modified_color: Color, 144 | /// The color of the renamed file's marker. 145 | pub renamed_color: Color, 146 | /// The color of the staged added file's marker. 147 | pub staged_added_color: Color, 148 | /// The color of the staged deleted file's marker. 149 | pub staged_deleted_color: Color, 150 | /// The color of the staged modified file's marker. 151 | pub staged_modified_color: Color, 152 | /// The color of the staged renamed file's marker. 153 | pub staged_renamed_color: Color, 154 | /// The color of the untracked file's marker. 155 | pub untracked_color: Color, 156 | } 157 | 158 | /// Contains color and marker styles for regex matches in the TUI. 159 | #[derive(Debug)] 160 | pub struct TUIRegexStyle { 161 | /// The color of the matched substring. 162 | pub match_color: Color, 163 | } 164 | 165 | impl Default for NomadStyle { 166 | /// Create a new `NomadStyle` with default values. 167 | fn default() -> Self { 168 | Self { 169 | git: GitStyle { 170 | conflicted_color: Colour::Red.bold(), 171 | conflicted_marker: "!".to_string(), 172 | deleted_color: Colour::Red.bold(), 173 | deleted_marker: "D".to_string(), 174 | modified_color: Colour::Fixed(172).bold(), 175 | modified_marker: "M".to_string(), 176 | renamed_color: Colour::Fixed(172).bold(), 177 | renamed_marker: "R".to_string(), 178 | staged_added_color: Colour::Green.bold(), 179 | staged_added_marker: "SA".to_string(), 180 | staged_deleted_color: Colour::Red.bold(), 181 | staged_deleted_marker: "SD".to_string(), 182 | staged_modified_color: Colour::Fixed(172).bold(), 183 | staged_modified_marker: "SM".to_string(), 184 | staged_renamed_color: Colour::Fixed(172).bold(), 185 | staged_renamed_marker: "SR".to_string(), 186 | staged_typechanged_color: Colour::Purple.bold(), 187 | staged_typechanged_marker: "STC".to_string(), 188 | typechanged_color: Colour::Purple.bold(), 189 | typechanged_marker: "TC".to_string(), 190 | untracked_color: Colour::Fixed(243).bold(), 191 | untracked_marker: "U".to_string(), 192 | }, 193 | tree: TreeStyle { 194 | indent: 4, 195 | indent_chars: IndentStyles { 196 | down: UTF_CHARS.down.to_string(), 197 | down_and_right: UTF_CHARS.down_and_right.to_string(), 198 | empty: UTF_CHARS.empty.to_string(), 199 | right: UTF_CHARS.right.to_string(), 200 | turn_right: UTF_CHARS.turn_right.to_string(), 201 | }, 202 | item_colors: ItemColors { 203 | directory_color: Colour::Blue.bold(), 204 | }, 205 | label_colors: LabelColors { 206 | item_labels: Colour::Fixed(068).bold(), 207 | directory_labels: Colour::Fixed(068).bold(), 208 | }, 209 | padding: 1, 210 | regex: TreeRegexStyle { 211 | match_color: Colour::Fixed(033).bold(), 212 | }, 213 | }, 214 | tui: TUIStyle { 215 | border_color: Color::Indexed(033), 216 | git: TUIGitStyle { 217 | conflicted_color: Color::Red, 218 | deleted_color: Color::Red, 219 | modified_color: Color::Indexed(172), 220 | renamed_color: Color::Indexed(172), 221 | staged_added_color: Color::Green, 222 | staged_deleted_color: Color::Red, 223 | staged_modified_color: Color::Indexed(172), 224 | staged_renamed_color: Color::Indexed(172), 225 | untracked_color: Color::Indexed(243), 226 | }, 227 | regex: TUIRegexStyle { 228 | match_color: Color::Indexed(033), 229 | }, 230 | standard_item_highlight_color: Color::Indexed(033), 231 | }, 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/style/settings.rs: -------------------------------------------------------------------------------- 1 | //! Configure style settings for `nomad`. 2 | 3 | use ansi_term::Colour; 4 | use ptree::print_config::UTF_CHARS; 5 | use tui::style::Color; 6 | 7 | use crate::config::models::{IndentCharacters, NomadConfig}; 8 | 9 | use super::{ 10 | models::NomadStyle, 11 | paint::{ 12 | convert_to_ansi_style, convert_to_tui_color, process_git_settings, 13 | process_tui_git_settings, process_tui_style_settings, 14 | }, 15 | }; 16 | 17 | /// Return a struct containing user-specified or default settings. 18 | pub fn process_settings(nomad_config: NomadConfig) -> NomadStyle { 19 | let mut nomad_style = NomadStyle::default(); 20 | 21 | if let Some(tree_settings) = nomad_config.tree { 22 | nomad_style.tree.indent = tree_settings.indent.unwrap_or(4); 23 | nomad_style.tree.padding = tree_settings.padding.unwrap_or(1); 24 | 25 | if let Some(label_settings) = tree_settings.labels { 26 | if let Some(color) = label_settings.item_labels { 27 | nomad_style.tree.label_colors.item_labels = 28 | convert_to_ansi_style(&color.to_lowercase()) 29 | } 30 | if let Some(color) = label_settings.directory_labels { 31 | nomad_style.tree.label_colors.directory_labels = 32 | convert_to_ansi_style(&color.to_lowercase()) 33 | } 34 | } 35 | 36 | if let Some(indent_chars) = tree_settings.indent_chars { 37 | process_indent_chars(indent_chars, &mut nomad_style); 38 | } 39 | 40 | if let Some(items) = tree_settings.items { 41 | if let Some(colors) = items.colors { 42 | if let Some(directory_color) = colors.directory_color { 43 | nomad_style.tree.item_colors.directory_color = 44 | convert_to_ansi_style(&directory_color.to_lowercase()); 45 | } 46 | } 47 | } 48 | 49 | if let Some(git_settings) = tree_settings.git { 50 | process_git_settings(&mut nomad_style, &git_settings); 51 | } 52 | 53 | nomad_style.tree.regex.match_color = match tree_settings.regex { 54 | Some(regex_setting) => match regex_setting.match_color { 55 | Some(color) => convert_to_ansi_style(&color), 56 | None => Colour::Fixed(033).bold(), 57 | }, 58 | None => Colour::Fixed(033).bold(), 59 | }; 60 | } 61 | 62 | if let Some(tui_settings) = nomad_config.tui { 63 | if let Some(git_settings) = tui_settings.git { 64 | process_tui_git_settings(&mut nomad_style, &git_settings); 65 | } 66 | 67 | if let Some(style_settings) = tui_settings.style { 68 | process_tui_style_settings(&mut nomad_style, &style_settings); 69 | } 70 | 71 | nomad_style.tui.regex.match_color = match tui_settings.regex { 72 | Some(regex_setting) => match regex_setting.match_color { 73 | Some(color) => convert_to_tui_color(&color), 74 | None => Color::Indexed(033), 75 | }, 76 | None => Color::Indexed(033), 77 | } 78 | } 79 | 80 | nomad_style 81 | } 82 | 83 | /// Set the indent characters for the tree itself. 84 | fn process_indent_chars(indent_chars: IndentCharacters, nomad_style: &mut NomadStyle) { 85 | nomad_style.tree.indent_chars.down = match indent_chars.down { 86 | Some(character) => character, 87 | None => UTF_CHARS.down.to_string(), 88 | }; 89 | nomad_style.tree.indent_chars.down_and_right = match indent_chars.down_and_right { 90 | Some(character) => character, 91 | None => UTF_CHARS.down_and_right.to_string(), 92 | }; 93 | nomad_style.tree.indent_chars.empty = match indent_chars.empty { 94 | Some(character) => character, 95 | None => UTF_CHARS.empty.to_string(), 96 | }; 97 | nomad_style.tree.indent_chars.right = match indent_chars.right { 98 | Some(character) => character, 99 | None => UTF_CHARS.right.to_string(), 100 | }; 101 | nomad_style.tree.indent_chars.turn_right = match indent_chars.turn_right { 102 | Some(character) => character, 103 | None => UTF_CHARS.turn_right.to_string(), 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /src/switches/config.rs: -------------------------------------------------------------------------------- 1 | //! Executing config subcommands. 2 | 3 | use crate::{ 4 | cli::config::ConfigOptions, 5 | config::preview::display_preview_tree, 6 | style::models::NomadStyle, 7 | utils::{open::open_files, paint::paint_error}, 8 | }; 9 | 10 | use ansi_term::Colour; 11 | 12 | /// `match` the config subcommand and execute it. 13 | pub fn run_config( 14 | config_options: &ConfigOptions, 15 | config_path: Option, 16 | nomad_style: &NomadStyle, 17 | ) { 18 | match config_options { 19 | ConfigOptions::Edit => { 20 | if let Some(config_path) = config_path { 21 | if let Err(error) = open_files(vec![config_path]) { 22 | paint_error(error) 23 | } 24 | } else { 25 | println!( 26 | "\n{}\n", 27 | Colour::Red 28 | .bold() 29 | .paint("Could not get the path to the configuration file!") 30 | ); 31 | } 32 | } 33 | ConfigOptions::Preview => { 34 | if let Err(error) = display_preview_tree(nomad_style) { 35 | paint_error(error); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/switches/filetype.rs: -------------------------------------------------------------------------------- 1 | //! Executing filetype subcommands. 2 | 3 | use ignore::types::TypesBuilder; 4 | 5 | use crate::{ 6 | cli::filetype::FileTypeOptions, 7 | style::models::NomadStyle, 8 | traverse::{ 9 | modes::NomadMode, 10 | utils::{build_types, build_walker, TypeOption}, 11 | walk_directory, 12 | }, 13 | utils::{ 14 | export::{export_tree, ExportMode}, 15 | paint::paint_error, 16 | table::{TableView, TabledItems}, 17 | }, 18 | }; 19 | 20 | /// `match` the filetype subcommand and execute it. 21 | pub fn run_filetypes( 22 | filetype_option: &FileTypeOptions, 23 | nomad_style: &NomadStyle, 24 | target_directory: &str, 25 | ) { 26 | let mut type_matcher = TypesBuilder::new(); 27 | type_matcher.add_defaults(); 28 | 29 | match filetype_option { 30 | FileTypeOptions::Match(match_options) => { 31 | match build_types( 32 | &match_options.filetypes, 33 | &match_options.globs, 34 | type_matcher, 35 | TypeOption::Match, 36 | ) { 37 | Ok(types) => { 38 | match build_walker(&match_options.general, target_directory, Some(types)) { 39 | Ok(mut walker) => { 40 | match walk_directory( 41 | &match_options.general, 42 | NomadMode::Normal, 43 | nomad_style, 44 | target_directory, 45 | &mut walker, 46 | ) { 47 | Ok((tree, config, _)) => { 48 | if let Some(export) = &match_options.general.export { 49 | if let Err(error) = export_tree( 50 | config, 51 | ExportMode::Filetype( 52 | &match_options.filetypes, 53 | &match_options.globs, 54 | ), 55 | export, 56 | tree, 57 | ) { 58 | paint_error(error); 59 | } 60 | } 61 | } 62 | Err(error) => paint_error(error), 63 | } 64 | } 65 | Err(error) => paint_error(error), 66 | } 67 | } 68 | Err(error) => paint_error(error), 69 | } 70 | } 71 | FileTypeOptions::Negate(negate_options) => { 72 | match build_types( 73 | &negate_options.filetypes, 74 | &negate_options.globs, 75 | type_matcher, 76 | TypeOption::Negate, 77 | ) { 78 | Ok(types) => { 79 | match build_walker(&negate_options.general, target_directory, Some(types)) { 80 | Ok(mut walker) => { 81 | match walk_directory( 82 | &negate_options.general, 83 | NomadMode::Normal, 84 | nomad_style, 85 | target_directory, 86 | &mut walker, 87 | ) { 88 | Ok((tree, config, _)) => { 89 | if let Some(export) = &negate_options.general.export { 90 | if let Err(error) = export_tree( 91 | config, 92 | ExportMode::Filetype( 93 | &negate_options.filetypes, 94 | &negate_options.globs, 95 | ), 96 | export, 97 | tree, 98 | ) { 99 | paint_error(error); 100 | } 101 | } 102 | } 103 | Err(error) => paint_error(error), 104 | } 105 | } 106 | Err(error) => paint_error(error), 107 | } 108 | } 109 | Err(error) => paint_error(error), 110 | } 111 | } 112 | FileTypeOptions::Options { filetype } => TabledItems::new( 113 | type_matcher.definitions(), 114 | vec!["Name".into(), "Globs".into()], 115 | 120, 116 | filetype.to_owned(), 117 | ) 118 | .display_table(), 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/switches/git.rs: -------------------------------------------------------------------------------- 1 | //! Executing Git subcommands. 2 | 3 | use crate::{ 4 | cli::{git::GitOptions, Args}, 5 | errors::NomadError, 6 | git::{ 7 | blame::bat_blame, 8 | branch::display_branches, 9 | commit::commit_changes, 10 | diff::{bat_diffs, get_repo_diffs}, 11 | status::{display_commits_ahead, display_status_tree}, 12 | trees::{modify_trees, TreeMode}, 13 | utils::{get_repo, get_repo_branch}, 14 | }, 15 | style::models::NomadStyle, 16 | utils::{ 17 | export::{export_tree, ExportMode}, 18 | paint::paint_error, 19 | search::{indiscriminate_search, SearchMode}, 20 | }, 21 | }; 22 | 23 | use ansi_term::Colour; 24 | use anyhow::anyhow; 25 | 26 | pub fn run_git( 27 | args: &Args, 28 | git_command: &GitOptions, 29 | nomad_style: &NomadStyle, 30 | target_directory: &str, 31 | ) { 32 | if let Some(repo) = get_repo(target_directory) { 33 | match git_command { 34 | GitOptions::Add(add_options) => { 35 | let stage_mode = match add_options.all { 36 | true => TreeMode::StageAll, 37 | false => TreeMode::Stage, 38 | }; 39 | 40 | if let Err(error) = modify_trees( 41 | args, 42 | &add_options.item_labels, 43 | nomad_style, 44 | &repo, 45 | stage_mode, 46 | target_directory, 47 | ) { 48 | paint_error(NomadError::GitError { 49 | context: "Unable to stage files".into(), 50 | source: error, 51 | }); 52 | } 53 | } 54 | GitOptions::Blame(blame_options) => match blame_options.file_number.parse::() { 55 | Ok(file_number) => { 56 | match indiscriminate_search( 57 | args, 58 | &[file_number.to_string()], 59 | nomad_style, 60 | Some(&repo), 61 | SearchMode::Normal, 62 | target_directory, 63 | ) { 64 | Some(ref mut found_items) => match found_items.pop() { 65 | Some(item) => { 66 | if blame_options.lines.len() > 2 { 67 | println!( 68 | "\n{}\n", 69 | Colour::Red 70 | .bold() 71 | .paint("Line range only takes two values - a lower and upper bound") 72 | ); 73 | } else if let Err(error) = 74 | bat_blame(item, blame_options, &repo, target_directory) 75 | { 76 | paint_error(error); 77 | } 78 | } 79 | None => println!( 80 | "\n{}\n", 81 | Colour::Red.bold().paint("Could not find a file to blame!") 82 | ), 83 | }, 84 | None => println!( 85 | "\n{}\n", 86 | Colour::Red.bold().paint("Could not find a file to blame!") 87 | ), 88 | } 89 | } 90 | Err(_) => paint_error(NomadError::GitBlameError), 91 | }, 92 | GitOptions::Branch(branch_options) => { 93 | match display_branches(branch_options, nomad_style, &repo, target_directory) { 94 | Ok(tree_items) => { 95 | if let Some((tree, config, _)) = tree_items { 96 | if let Some(export) = &branch_options.export { 97 | if let Err(error) = 98 | export_tree(config, ExportMode::GitBranch, export, tree) 99 | { 100 | paint_error(error); 101 | } 102 | } 103 | } 104 | } 105 | Err(error) => paint_error(error), 106 | } 107 | } 108 | GitOptions::Commit { message } => { 109 | if let Err(error) = commit_changes(message, &repo) { 110 | paint_error(error); 111 | } 112 | } 113 | GitOptions::Diff { item_labels } => match get_repo_diffs(&repo) { 114 | Ok(diff) => { 115 | match indiscriminate_search( 116 | args, 117 | item_labels, 118 | nomad_style, 119 | Some(&repo), 120 | SearchMode::GitDiff, 121 | target_directory, 122 | ) { 123 | Some(found_items) => { 124 | if let Err(error) = bat_diffs(diff, Some(found_items), target_directory) 125 | { 126 | paint_error(error); 127 | } 128 | } 129 | None => { 130 | if let Err(error) = bat_diffs(diff, None, target_directory) { 131 | paint_error(error); 132 | } 133 | } 134 | } 135 | } 136 | Err(error) => paint_error(NomadError::GitError { 137 | context: "Unable to get Git diff".into(), 138 | source: error, 139 | }), 140 | }, 141 | GitOptions::Restore(restore_options) => { 142 | if let Err(error) = modify_trees( 143 | args, 144 | &restore_options.item_labels, 145 | nomad_style, 146 | &repo, 147 | TreeMode::RestoreWorkingDirectory, 148 | target_directory, 149 | ) { 150 | paint_error(NomadError::GitError { 151 | context: "Unable to restore files!".to_string(), 152 | source: error, 153 | }); 154 | } 155 | } 156 | GitOptions::Status(status_options) => { 157 | if let Some(branch_name) = get_repo_branch(&repo) { 158 | println!( 159 | "\nOn branch: {}", 160 | Colour::Green.bold().paint(branch_name.to_string()) 161 | ); 162 | 163 | if let Err(error) = display_commits_ahead(&branch_name, &repo) { 164 | paint_error(error); 165 | } 166 | } 167 | 168 | match display_status_tree(status_options, nomad_style, &repo, target_directory) { 169 | Ok(tree_items) => { 170 | if let Some((tree, config)) = tree_items { 171 | if let Some(export) = &status_options.export { 172 | if let Err(error) = 173 | export_tree(config, ExportMode::GitStatus, export, tree) 174 | { 175 | paint_error(error); 176 | } 177 | } 178 | } 179 | } 180 | Err(error) => { 181 | paint_error(error); 182 | } 183 | } 184 | } 185 | } 186 | } else { 187 | paint_error(NomadError::Error(anyhow!("Cannot run Git commands here!"))); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/switches/mod.rs: -------------------------------------------------------------------------------- 1 | //! Chunks of match commands for program execution. 2 | 3 | pub mod config; 4 | pub mod filetype; 5 | pub mod git; 6 | pub mod release; 7 | -------------------------------------------------------------------------------- /src/switches/release.rs: -------------------------------------------------------------------------------- 1 | //! Executing release commands. 2 | 3 | use crate::{ 4 | cli::releases::ReleaseOptions, 5 | releases::build_release_list, 6 | utils::{ 7 | paint::paint_error, 8 | table::{TableView, TabledItems}, 9 | }, 10 | }; 11 | 12 | pub fn run_releases(release_option: &ReleaseOptions) { 13 | match release_option { 14 | ReleaseOptions::All => match build_release_list() { 15 | Ok(releases) => TabledItems::new( 16 | releases, 17 | vec![ 18 | "Name".into(), 19 | "Version".into(), 20 | "Release Date".into(), 21 | "Description".into(), 22 | "Assets".into(), 23 | ], 24 | 180, 25 | None, 26 | ) 27 | .display_table(), 28 | Err(error) => paint_error(error), 29 | }, 30 | ReleaseOptions::Info { release_version } => match build_release_list() { 31 | Ok(releases) => TabledItems::new( 32 | releases, 33 | vec![ 34 | "Name".into(), 35 | "Version".into(), 36 | "Release Date".into(), 37 | "Description".into(), 38 | "Assets".into(), 39 | ], 40 | 180, 41 | release_version.to_owned(), 42 | ) 43 | .display_table(), 44 | Err(error) => paint_error(error), 45 | }, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/traverse/mod.rs: -------------------------------------------------------------------------------- 1 | //! Traverse the target directory. 2 | 3 | pub mod format; 4 | pub mod models; 5 | pub mod modes; 6 | pub mod traits; 7 | pub mod utils; 8 | 9 | use self::{ 10 | models::{DirItem, FoundItem}, 11 | modes::NomadMode, 12 | traits::{ToTree, TransformFound}, 13 | }; 14 | use crate::{ 15 | cli::global::GlobalArgs, errors::NomadError, git::markers::extend_marker_map, 16 | style::models::NomadStyle, utils::paths::canonicalize_path, 17 | }; 18 | 19 | use anyhow::{Result, __private}; 20 | use ignore::{self, Walk}; 21 | use ptree::{item::StringItem, PrintConfig}; 22 | use regex::Regex; 23 | 24 | use std::{collections::HashMap, path::Path}; 25 | 26 | /// Traverse the directory and display files and directories accordingly. 27 | pub fn walk_directory( 28 | args: &GlobalArgs, 29 | nomad_mode: NomadMode, 30 | nomad_style: &NomadStyle, 31 | target_directory: &str, 32 | walker: &mut Walk, 33 | ) -> Result<(StringItem, PrintConfig, Option>), NomadError> { 34 | let regex_expression = if let Some(ref pattern) = args.regex.pattern { 35 | match Regex::new(&pattern.clone()) { 36 | Ok(regex) => Some(regex), 37 | Err(error) => return __private::Err(NomadError::RegexError(error)), 38 | } 39 | } else { 40 | None 41 | }; 42 | 43 | let mut git_markers: HashMap = HashMap::new(); 44 | extend_marker_map( 45 | &args.style, 46 | &mut git_markers, 47 | nomad_style, 48 | Path::new(target_directory).to_str().unwrap_or("?"), 49 | ); 50 | 51 | let (tree, config, directory_items) = walker 52 | .filter_map(|dir_entry| { 53 | if let Ok(entry) = dir_entry { 54 | if entry.path().is_dir() { 55 | extend_marker_map( 56 | &args.style, 57 | &mut git_markers, 58 | nomad_style, 59 | entry.path().to_str().unwrap_or("?"), 60 | ); 61 | None 62 | } else if let Some(ref regex) = regex_expression { 63 | if let Some(matched) = regex.find( 64 | entry 65 | .path() 66 | .strip_prefix(target_directory) 67 | .unwrap_or_else(|_| Path::new("?")) 68 | .to_str() 69 | .unwrap_or("?"), 70 | ) { 71 | Some(FoundItem { 72 | marker: git_markers 73 | .get( 74 | &canonicalize_path(entry.path().to_str().unwrap_or("?")) 75 | .unwrap_or_else(|_| "?".to_string()), 76 | ) 77 | .map(|marker| marker.to_string()), 78 | matched: Some((matched.start(), matched.end())), 79 | path: entry.path().to_str().unwrap_or("?").to_string(), 80 | }) 81 | } else { 82 | None 83 | } 84 | } else { 85 | Some(FoundItem { 86 | marker: git_markers 87 | .get( 88 | &canonicalize_path(entry.path().to_str().unwrap_or("?")) 89 | .unwrap_or_else(|_| "?".to_string()), 90 | ) 91 | .map(|marker| marker.to_string()), 92 | matched: None, 93 | path: entry.path().to_str().unwrap_or("?").to_string(), 94 | }) 95 | } 96 | } else { 97 | None 98 | } 99 | }) 100 | .collect::>() 101 | .transform(target_directory)? 102 | .to_tree(args, nomad_mode, nomad_style, target_directory)?; 103 | 104 | Ok((tree, config, directory_items)) 105 | } 106 | -------------------------------------------------------------------------------- /src/traverse/models.rs: -------------------------------------------------------------------------------- 1 | //! Structs used during directory traversal. 2 | 3 | /// Contains the path of the found item and its corresponding Git marker if applicable. 4 | /// 5 | /// This struct is used to convert `DirEntry`s returned by the `Walk` object. 6 | #[derive(Debug)] 7 | pub struct FoundItem { 8 | /// The Git status marker indicating the change that was made to the file. 9 | pub marker: Option, 10 | /// The start and end of the pattern match in the path. 11 | pub matched: Option<(usize, usize)>, 12 | /// The filepath. 13 | pub path: String, 14 | } 15 | 16 | /// Contains metadata for each path. 17 | /// 18 | /// The `TransformFound` trait converts a `FoundItem` into this struct for tree building. 19 | #[derive(Debug)] 20 | pub struct TransformedItem { 21 | /// The filepath broken down into its individual components. 22 | pub components: Vec, 23 | /// The depth of the file relative to the root of the directory. 24 | pub depth: i32, 25 | /// Indicates whether this is a directory. 26 | pub is_dir: bool, 27 | /// Indicates whether this is a file. 28 | pub is_file: bool, 29 | /// The Git status marker indicating the change that was made to the file. 30 | pub marker: Option, 31 | /// The start and end of the pattern match in the path. 32 | pub matched: Option<(usize, usize)>, 33 | /// The absolute filepath. 34 | pub path: String, 35 | } 36 | 37 | /// Contains metadata for `git branch` items. 38 | /// 39 | /// This struct is used to convert Git branches into a struct containing metadata used for tree 40 | /// building. 41 | #[derive(Debug)] 42 | pub struct FoundBranch { 43 | /// The full branch name. 44 | pub full_branch: String, 45 | /// Indicates whether this is the current branch. 46 | pub is_current_branch: bool, 47 | /// Indicates whether this branch points to `HEAD`. 48 | pub is_head: bool, 49 | /// The marker indicating whether this is the current branch. 50 | pub marker: Option, 51 | /// The start and end of the pattern match in the branch name. 52 | pub matched: Option<(usize, usize)>, 53 | /// The upstream branch if it exists. 54 | pub upstream: Option, 55 | } 56 | 57 | /// The `TransformFound` trait converts a `FoundBranch` into this struct for tree building. 58 | /// 59 | /// These fields assume branch names are formatted like directory paths, ie. 60 | /// `feature/something_new`. 61 | #[derive(Debug)] 62 | pub struct TransformedBranch { 63 | /// The branch name broken down into its individual components. 64 | pub components: Vec, 65 | /// The depth of the branch relative to its components. 66 | pub depth: i32, 67 | /// The full branch name. 68 | pub full_branch: String, 69 | /// Indicates whether this is the current branch. 70 | pub is_current_branch: bool, 71 | /// Indicates whether this is the end of a branch name. 72 | pub is_end: bool, 73 | /// Indicates whether this branch points to `HEAD`. 74 | pub is_head: bool, 75 | /// Indicates whether the branch name has a parent name. For example, if the 76 | /// branch name is `feature/something_new`, the parent would be `feature`. 77 | pub is_parent: bool, 78 | /// The marker indicating whether this is the current branch. 79 | pub marker: Option, 80 | /// The start and end of the pattern match in the branch name. 81 | pub matched: Option<(usize, usize)>, 82 | /// The upstream branch if it exists. This is also formatted if it points to 83 | /// `HEAD`. 84 | pub upstream: Option, 85 | } 86 | 87 | /// Contains metadata for each item in the directory. 88 | /// 89 | /// This struct is used when the user is in Rootless mode. 90 | #[derive(Debug)] 91 | pub struct DirItem { 92 | /// The Git marker associated with this item if there is one. 93 | pub marker: Option, 94 | /// The absolute path to this item. 95 | pub path: String, 96 | } 97 | -------------------------------------------------------------------------------- /src/traverse/modes.rs: -------------------------------------------------------------------------------- 1 | //! Traversal modes for `nomad`. 2 | 3 | /// Modes in which `nomad` may operate. 4 | pub enum NomadMode { 5 | /// Run `nomad` in `git branch` mode. 6 | GitBranch, 7 | /// Run `nomad` in `git status` mode. 8 | GitStatus, 9 | /// Run `nomad` in normal mode. 10 | Normal, 11 | /// Run `nomad` in rootless (interactive) mode. 12 | Rootless, 13 | } 14 | -------------------------------------------------------------------------------- /src/traverse/utils.rs: -------------------------------------------------------------------------------- 1 | //! Directory traversal utilities. 2 | 3 | use crate::{ 4 | cli::global::GlobalArgs, 5 | errors::NomadError, 6 | style::models::NomadStyle, 7 | utils::{ 8 | cache::{get_json_file, write_to_json}, 9 | meta::get_metadata, 10 | }, 11 | EXTENSION_ICON_MAP, NAME_ICON_MAP, 12 | }; 13 | 14 | use ansi_term::Colour; 15 | use anyhow::Result; 16 | use ignore::{ 17 | types::{Types, TypesBuilder}, 18 | Walk, WalkBuilder, 19 | }; 20 | use ptree::{Color, PrintConfig, Style, TreeBuilder}; 21 | use serde_json::{json, Value}; 22 | 23 | use std::{ 24 | collections::HashMap, 25 | ffi::OsStr, 26 | path::{Component, Path}, 27 | }; 28 | 29 | use super::modes::NomadMode; 30 | 31 | /// Contains options for `Types` building. 32 | pub enum TypeOption { 33 | /// Build a `Types` that matches a filetype. 34 | Match, 35 | /// Build a `Types` that negates a filetype. 36 | Negate, 37 | } 38 | 39 | /// Build an `ignore` `Types` depending on the 40 | pub fn build_types( 41 | filetypes: &[String], 42 | globs: &[String], 43 | mut type_matcher: TypesBuilder, 44 | type_option: TypeOption, 45 | ) -> Result { 46 | for filetype in filetypes { 47 | match type_option { 48 | TypeOption::Match => { 49 | type_matcher.select(filetype); 50 | } 51 | TypeOption::Negate => { 52 | type_matcher.negate(filetype); 53 | } 54 | }; 55 | } 56 | 57 | for (index, glob) in globs.iter().enumerate() { 58 | let glob_label = index.to_string(); 59 | 60 | type_matcher.add(&glob_label, glob)?; 61 | 62 | match type_option { 63 | TypeOption::Match => { 64 | type_matcher.select(&glob_label); 65 | } 66 | TypeOption::Negate => { 67 | type_matcher.negate(&glob_label); 68 | } 69 | } 70 | } 71 | 72 | type_matcher 73 | .build() 74 | .map_or_else(|error| Err(NomadError::IgnoreError(error)), Ok) 75 | } 76 | 77 | /// Build a `Walk` object based on the client's CLI parameters. 78 | pub fn build_walker( 79 | args: &GlobalArgs, 80 | target_directory: &str, 81 | types: Option, 82 | ) -> Result { 83 | if Path::new(target_directory).is_dir() { 84 | let mut walk = WalkBuilder::new(target_directory); 85 | 86 | walk.follow_links(true) 87 | .git_exclude(!args.modifiers.disrespect) 88 | .git_global(!args.modifiers.disrespect) 89 | .git_ignore(!args.modifiers.disrespect) 90 | .hidden(!args.modifiers.hidden) 91 | .ignore(!args.modifiers.disrespect) 92 | .max_depth(args.modifiers.max_depth) 93 | .max_filesize(args.modifiers.max_filesize) 94 | .parents(!args.modifiers.disrespect) 95 | .sort_by_file_path(|a, b| a.cmp(b)); 96 | 97 | if let Some(types) = types { 98 | walk.types(types); 99 | } 100 | 101 | Ok(walk.build()) 102 | } else { 103 | Err(NomadError::NotADirectory(target_directory.into())) 104 | } 105 | } 106 | 107 | /// Get the file's corresponding icon. 108 | pub fn get_file_icon(item_path: &Path) -> String { 109 | if let Some(icon) = EXTENSION_ICON_MAP.get( 110 | item_path 111 | .extension() 112 | .unwrap_or_else(|| OsStr::new("none")) 113 | .to_str() 114 | .unwrap(), 115 | ) { 116 | icon.to_string() 117 | } else if let Some(icon) = NAME_ICON_MAP.get( 118 | &item_path 119 | .file_name() 120 | .unwrap_or_else(|| OsStr::new("?")) 121 | .to_str() 122 | .unwrap_or("?"), 123 | ) { 124 | icon.to_string() 125 | } else { 126 | "\u{f016}".to_string() //  127 | } 128 | } 129 | 130 | /// Build a `ptree` object and set the tree's style/configuration. 131 | pub fn build_tree( 132 | args: &GlobalArgs, 133 | nomad_mode: &NomadMode, 134 | nomad_style: &NomadStyle, 135 | target_directory: &Path, 136 | ) -> (PrintConfig, TreeBuilder) { 137 | let directory_icon = &"\u{f115}"; //  138 | 139 | let plain_name = target_directory 140 | .file_name() 141 | .unwrap_or_else(|| OsStr::new("?")) 142 | .to_str() 143 | .unwrap_or("?") 144 | .to_string(); 145 | let directory_name = match nomad_mode { 146 | NomadMode::GitBranch => format!( 147 | "{}{} [{}]", 148 | match args.style.no_icons { 149 | true => "", 150 | false => "\u{f1d3} ", 151 | }, 152 | Colour::Blue.bold().paint(plain_name), 153 | Colour::Fixed(172).bold().paint("BRANCHES") 154 | ), 155 | _ => { 156 | if args.style.plain { 157 | plain_name 158 | } else if args.style.no_colors { 159 | format!("{directory_icon} {plain_name}") 160 | } else { 161 | format!("{directory_icon} {}", Colour::Blue.bold().paint(plain_name)) 162 | } 163 | } 164 | }; 165 | 166 | let mut tree_label = directory_name; 167 | match nomad_mode { 168 | NomadMode::GitBranch => {} 169 | _ => { 170 | if args.meta.metadata { 171 | let metadata = get_metadata(args, target_directory); 172 | tree_label = format!("{metadata} {tree_label}"); 173 | } 174 | } 175 | } 176 | 177 | let tree = TreeBuilder::new(tree_label); 178 | let config = build_tree_style(nomad_style); 179 | 180 | (config, tree) 181 | } 182 | 183 | /// Build a new `Style` based on the settings in `NomadStyle`. 184 | pub fn build_tree_style(nomad_style: &NomadStyle) -> PrintConfig { 185 | let mut branch_style = Style::default(); 186 | 187 | branch_style.bold = true; 188 | branch_style.foreground = Some(Color::White); 189 | 190 | let mut config = PrintConfig::default(); 191 | 192 | config.branch = branch_style; 193 | config.indent = nomad_style.tree.indent; 194 | config.padding = nomad_style.tree.padding; 195 | 196 | config.characters.down = nomad_style.tree.indent_chars.down.to_string(); 197 | config.characters.down_and_right = nomad_style.tree.indent_chars.down_and_right.to_string(); 198 | config.characters.empty = nomad_style.tree.indent_chars.empty.to_string(); 199 | config.characters.right = nomad_style.tree.indent_chars.right.to_string(); 200 | config.characters.turn_right = nomad_style.tree.indent_chars.turn_right.to_string(); 201 | 202 | config 203 | } 204 | 205 | /// Run checks to ensure tree nesting is correct. Make any corrections if applicable. 206 | pub fn check_nesting( 207 | current_depth: usize, 208 | item: &Path, 209 | nomad_mode: &NomadMode, 210 | previous_item: &Path, 211 | target_directory: &str, 212 | tree: &mut TreeBuilder, 213 | ) { 214 | let mut item_depth = 0; 215 | let item_components = match nomad_mode { 216 | NomadMode::GitBranch => item.components(), 217 | _ => item 218 | .strip_prefix(target_directory) 219 | .unwrap_or_else(|_| Path::new("?")) 220 | .components(), 221 | }; 222 | 223 | for component in item_components { 224 | if let Component::Normal(_) = component { 225 | item_depth += 1; 226 | } 227 | } 228 | 229 | if item_depth < current_depth { 230 | if previous_item.is_dir() { 231 | let item_parent = item 232 | .parent() 233 | .expect("Could not get the current item's parent!"); 234 | let previous_parent = previous_item 235 | .parent() 236 | .expect("Could not get the previous item's parent!"); 237 | 238 | if item_parent != previous_parent { 239 | tree.end_child(); 240 | } 241 | } 242 | 243 | for _ in 0..current_depth - item_depth { 244 | tree.end_child(); 245 | } 246 | } else if item_depth == current_depth && previous_item.is_dir() { 247 | tree.end_child(); 248 | } 249 | } 250 | 251 | /// Write the labeled directories or numbered directory contents to a temporary file. 252 | pub fn store_directory_contents( 253 | labeled_items: HashMap, 254 | numbered_items: HashMap, 255 | ) -> Result<(), NomadError> { 256 | let mut json = json!({ "labeled": {}, "numbered": {} }); 257 | 258 | write_map(labeled_items, &mut json, "labeled"); 259 | write_map(numbered_items, &mut json, "numbered"); 260 | 261 | let mut json_file = get_json_file(false)?; 262 | write_to_json(&mut json_file, json)?; 263 | 264 | Ok(()) 265 | } 266 | 267 | /// Write each key, value within a HashMap to JSON `Value` object. 268 | fn write_map(items: HashMap, json: &mut Value, target_key: &str) { 269 | for (key, value) in items.iter() { 270 | json[target_key] 271 | .as_object_mut() 272 | .unwrap() 273 | .insert(key.clone(), json!(value.clone())); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/ui/layouts.rs: -------------------------------------------------------------------------------- 1 | //! Building layouts for the UI. 2 | 3 | use tui::layout::{Constraint, Direction, Layout, Rect}; 4 | 5 | /// Create a centered `Rect` for input popups. 6 | pub fn get_single_line_popup_area(frame: Rect) -> Rect { 7 | let popup_layout = Layout::default() 8 | .direction(Direction::Vertical) 9 | .constraints([ 10 | Constraint::Percentage(45), 11 | Constraint::Length(3), 12 | Constraint::Percentage(45), 13 | ]) 14 | .split(frame); 15 | 16 | Layout::default() 17 | .direction(Direction::Horizontal) 18 | .constraints([ 19 | Constraint::Percentage(33), 20 | Constraint::Percentage(33), 21 | Constraint::Percentage(33), 22 | ]) 23 | .split(popup_layout[1])[1] 24 | } 25 | 26 | /// Create a centered `Rect` for error popups. 27 | pub fn get_error_popup_area(frame: Rect) -> Rect { 28 | let error_layout = Layout::default() 29 | .direction(Direction::Vertical) 30 | .constraints([ 31 | Constraint::Percentage(40), 32 | Constraint::Percentage(20), 33 | Constraint::Percentage(40), 34 | ]) 35 | .split(frame); 36 | 37 | Layout::default() 38 | .direction(Direction::Horizontal) 39 | .constraints([ 40 | Constraint::Percentage(20), 41 | Constraint::Percentage(60), 42 | Constraint::Percentage(20), 43 | ]) 44 | .split(error_layout[1])[1] 45 | } 46 | 47 | /// Create a centered popup area to display the available keybindings. 48 | pub fn get_keybindings_area(frame: Rect) -> Rect { 49 | let keybindings_layout = Layout::default() 50 | .direction(Direction::Vertical) 51 | .constraints([ 52 | Constraint::Percentage(30), 53 | Constraint::Percentage(40), 54 | Constraint::Percentage(30), 55 | ]) 56 | .split(frame); 57 | 58 | Layout::default() 59 | .direction(Direction::Horizontal) 60 | .constraints([ 61 | Constraint::Percentage(20), 62 | Constraint::Percentage(60), 63 | Constraint::Percentage(20), 64 | ]) 65 | .split(keybindings_layout[1])[1] 66 | } 67 | 68 | /// Create a centered popup area to display the current settings. 69 | pub fn get_settings_area(frame: Rect) -> Rect { 70 | let settings_layout = Layout::default() 71 | .direction(Direction::Vertical) 72 | .constraints([ 73 | Constraint::Percentage(30), 74 | Constraint::Percentage(40), 75 | Constraint::Percentage(30), 76 | ]) 77 | .split(frame); 78 | 79 | Layout::default() 80 | .direction(Direction::Horizontal) 81 | .constraints([ 82 | Constraint::Percentage(30), 83 | Constraint::Percentage(40), 84 | Constraint::Percentage(30), 85 | ]) 86 | .split(settings_layout[1])[1] 87 | } 88 | -------------------------------------------------------------------------------- /src/ui/models.rs: -------------------------------------------------------------------------------- 1 | //! Structs used in the UI. 2 | 3 | /// Contains keybindings for all modes excluding help mode. 4 | #[derive(Debug)] 5 | pub struct Keybindings<'a> { 6 | /// The keybindings for breadcrumbs mode. 7 | pub breadcrumbs: Vec<(&'a str, &'a str)>, 8 | /// The keybindings for inspect mode. 9 | pub inspect: Vec<(&'a str, &'a str)>, 10 | /// The keybindings for normal mode. 11 | pub normal: Vec<(&'a str, &'a str)>, 12 | } 13 | 14 | impl Default for Keybindings<'_> { 15 | /// Create the `Keybindings` struct with the keybindings for each mode 16 | /// (breadcrumbs, inspect, and normal modes). 17 | fn default() -> Self { 18 | let breadcrumbs = vec![ 19 | (" h, left", "move left in the breadcrumbs"), 20 | (" l, right", "move right in the breadcrumbs"), 21 | (" ", "switch back to normal tree mode"), 22 | (" ", "enter the selected directory"), 23 | ]; 24 | let inspect = vec![ 25 | (" /", "search for a pattern within the file"), 26 | (" 0", "scroll to the top of the file"), 27 | (" 1 - 9", "scroll 'n' lines down"), 28 | ("", " + 'n' scrolls 'n' lines up"), 29 | (" j, down", "scroll down the file"), 30 | (" k, up", "scroll up the file"), 31 | (" n", "snap to the next pattern match"), 32 | ("", "in the file"), 33 | (" N", "snap to the previous pattern match"), 34 | ("", "in the file"), 35 | (" R", "refresh the file contents"), 36 | (" ", "return to normal tree mode"), 37 | ]; 38 | let normal = vec![ 39 | (" /", "search for a pattern in file paths"), 40 | (" 0", "scroll to the top of the tree"), 41 | (" d", "toggle only displaying directories"), 42 | (" e", "edit the selected item in a text editor"), 43 | ("", "(if it is a file)"), 44 | (" g", "toggle Git markers"), 45 | (" h", "toggle displaying hidden items"), 46 | (" i", "toggle icons"), 47 | (" j, down", "scroll down the directory tree"), 48 | (" k, up", "scroll up the directory tree"), 49 | (" l", "toggle directory labels"), 50 | (" m", "toggle displaying item metadata"), 51 | (" n", "toggle item numbers"), 52 | (" p", "toggle plain mode"), 53 | (" r", "refresh the tree with your current settings"), 54 | (" s", "toggle the settings pane for the current tree"), 55 | (" D", "toggle disrespecting all rules specified"), 56 | ("", "in ignore-type files"), 57 | (" L", "toggle all labels (directories and items)"), 58 | (" R", "reset all current settings and refresh"), 59 | ("", "the tree"), 60 | (" ", "move to breadcrumbs mode"), 61 | (" ", "enter the selected directory, or inspect"), 62 | ("", "the selected file"), 63 | ]; 64 | 65 | Self { 66 | breadcrumbs, 67 | inspect, 68 | normal, 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ui/stateful_widgets.rs: -------------------------------------------------------------------------------- 1 | //! Stateful widgets for the user interface. 2 | 3 | use tui::widgets::{ListState, TableState}; 4 | 5 | /// A widget that contains its own state. 6 | pub struct StatefulWidget { 7 | /// Items that are contained in this widget. 8 | pub items: Vec, 9 | /// The widget's state. 10 | pub state: U, 11 | /// The widget's mode. 12 | pub widget_mode: WidgetMode, 13 | } 14 | 15 | /// The modes in which the widget may operate. 16 | pub enum WidgetMode { 17 | /// The widget is in the files mode (files may be selected from the `App` 18 | /// based on which index is `selected()`). 19 | Files, 20 | /// The widget is in standard mode. 21 | Standard, 22 | } 23 | 24 | impl StatefulWidget { 25 | /// Create a new `StatefulList` containing generic `items`. 26 | pub fn new(items: Vec, state: U, widget_mode: WidgetMode) -> StatefulWidget { 27 | StatefulWidget { 28 | items, 29 | state, 30 | widget_mode, 31 | } 32 | } 33 | } 34 | 35 | /// Enable round robin behavior for lists containing a `ListState`. 36 | impl StatefulWidget { 37 | /// Get the next item in `self.items`. 38 | pub fn next(&mut self) { 39 | let limit = match self.widget_mode { 40 | WidgetMode::Files => 2, 41 | WidgetMode::Standard => 1, 42 | }; 43 | 44 | self.state.select(Some(match self.state.selected() { 45 | Some(index) => { 46 | if index >= self.items.len() - limit { 47 | 0 48 | } else { 49 | index + 1 50 | } 51 | } 52 | None => 0, 53 | })); 54 | } 55 | 56 | /// Get the previous item in `self.items`. 57 | pub fn previous(&mut self) { 58 | let limit = match self.widget_mode { 59 | WidgetMode::Files => 2, 60 | WidgetMode::Standard => 1, 61 | }; 62 | 63 | self.state.select(Some(match self.state.selected() { 64 | Some(index) => { 65 | if index == 0 { 66 | self.items.len() - limit 67 | } else { 68 | index - 1 69 | } 70 | } 71 | None => 0, 72 | })); 73 | } 74 | } 75 | 76 | /// Enable round robin behavior for tables containing a `TableState`. 77 | impl StatefulWidget { 78 | /// Get the next item in `self.items`. 79 | pub fn next(&mut self) { 80 | self.state.select(Some(match self.state.selected() { 81 | Some(index) => { 82 | if index >= self.items.len() - 1 { 83 | 0 84 | } else { 85 | index + 1 86 | } 87 | } 88 | None => 0, 89 | })); 90 | } 91 | 92 | /// Get the previous item in `self.items`. 93 | pub fn previous(&mut self) { 94 | self.state.select(Some(match self.state.selected() { 95 | Some(index) => { 96 | if index == 0 { 97 | self.items.len() - 1 98 | } else { 99 | index - 1 100 | } 101 | } 102 | None => 0, 103 | })); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/ui/text.rs: -------------------------------------------------------------------------------- 1 | //! Text constants for TUI help messages. 2 | 3 | /// The help text displayed in the help menu after pressing '?'. 4 | pub const HELP_TEXT: &str = r#" 5 | Press to exit this screen. 6 | 7 | Use the directional or Vim directional keys [j, k] to scroll. 8 | 9 | Optionally type a number or its counterpart to scroll down/up `n` lines, 10 | ie. '4' to scroll down 4 lines and '$' to scroll up 4 lines. 11 | 12 | ------------------------------------------------------------------------------- 13 | 14 | Table of Contents 15 | ================= 16 | 17 | * Widgets 18 | + Cycling Through Widgets 19 | + Normal Widget (Tree View) 20 | + Breadcrumbs Widget 21 | + Inspect Widget 22 | * Keybindings 23 | + Navigation 24 | + Commands 25 | 26 | ------------------------------------------------------------------------------- 27 | 28 | Widgets 29 | ======= 30 | 31 | Rootless mode has 4 widgets: 32 | 33 | * Normal (tree view) 34 | * Breadcrumbs 35 | * Inspect (file view) 36 | * Help (this widget) 37 | 38 | The border of the active widget is colorized and other widgets are dimmed to 39 | make it obvious which one you are in. 40 | 41 | **TIP**: If you are not sure what keybindings are available for the current 42 | widget, press 'K' to display a list of available keybindings. 43 | 44 | Cycling Through Widgets 45 | ----------------------- 46 | 47 | allows you to exit/cycle through the widgets. Here is a small diagram 48 | that depicts how works. 49 | 50 | 51 | Breadcrumbs <=======> Normal <======= Inspect 52 | 53 | You can enter the help widget at any time by pressing '?'. ing from the 54 | help widget will always take you back to the normal/tree view. 55 | 56 | 57 | Normal <======= Help 58 | 59 | Normal Widget (Tree View) 60 | ------------------------- 61 | 62 | This is the widget you are in when you enter Rootless mode and is the leftmost 63 | widget. You can control the appearance of the tree, enter into the directories 64 | or files, open a file in a text editor, filter results by pattern, etc. See the 65 | Keybindings section for details. 66 | 67 | Breadcrumbs Widget 68 | ------------------ 69 | 70 | This is the widget at the very top of the TUI. This enables you to enter parent 71 | directories. You can traverse left or right and press to refresh the 72 | tree with the contents of the selected directory. 73 | 74 | Inspect Widget 75 | -------------- 76 | 77 | This widget is conditionally rendered based on the current highlighted item in 78 | the tree. This widget appears on the right side of the TUI if the highlighted 79 | item is a file and will contain the contents of that file. 80 | 81 | You can scroll up/down and search for patterns in the file while in this widget. 82 | See the Keybindings section for details. 83 | 84 | ------------------------------------------------------------------------------- 85 | 86 | Keybindings 87 | =========== 88 | 89 | Navigation 90 | ---------- 91 | 92 | Use the directional keys or Vim directional keys [h, j, k, l] to navigate the TUI. 93 | 94 | Commands 95 | -------- 96 | 97 | Listed below are the commands you can use, which widgets support it, and a short 98 | description of what it does. 99 | 100 | 101 | 0 Widgets: Normal, Inspect, Help 102 | Scroll to the top of the widget 103 | 104 | 1 - 9 Widgets: Inspect, Help 105 | Scroll `n` lines down. + `n` scrolls `n` lines up. 106 | 107 | d Widget: Normal 108 | Toggle only displaying directories 109 | 110 | e Widgets: Normal, Inspect 111 | Open the selected file in a text editor (Neovim, Vim, Vi, or Nano) 112 | This only applies if the selected item is a file 113 | 114 | g Widget: Normal 115 | Toggle Git status markers 116 | 117 | h Widget: Normal 118 | Toggle displaying hidden items 119 | 120 | i Widget: Normal 121 | Toggle icons 122 | 123 | l Widget: Normal 124 | Toggle directory labels 125 | 126 | m Widget: Normal 127 | Toggle metadata 128 | 129 | n Widget: Normal 130 | Toggle item labels 131 | Widget: Inspect 132 | If a pattern was matched, snap to the next match 133 | 134 | p Widget: Normal 135 | Toggle plain mode 136 | 137 | q Widgets: all 138 | Quit Rootless mode 139 | 140 | r Widget: Normal 141 | Refresh the tree 142 | 143 | s Widget: Normal 144 | Display the settings for the current tree 145 | 146 | D Widget: Normal 147 | Toggle disrespecting all rules specified in ignore-type files 148 | 149 | K Widget: Normal 150 | Display the keybindings available for the widget you are in 151 | 152 | L Widget: Normal 153 | Toggle all labels (directories and items) 154 | 155 | N Widget: Inspect 156 | If a pattern was matched, snap to the previous match 157 | 158 | R Widget: Normal 159 | Reset all options to its default value and refresh the current 160 | tree 161 | Widget: Inspect 162 | Refresh the current file 163 | 164 | / Widgets: Normal, Inspect 165 | Search for a pattern. Supports regex expressions and available in 166 | the Normal and Inspect widgets 167 | 168 | ? Widgets: all 169 | Display this help message 170 | 171 | Widget: Normal 172 | If a file is selected 173 | Enter the Inspect (file view) widget 174 | If a directory is selected 175 | Enter the selected directory and refresh the tree with the 176 | contents of the new directory 177 | Widget: Breadcrumbs 178 | Enter the selected directory and refresh the tree with the 179 | contents of the new directory. 180 | 181 | Widget: all 182 | Cycle through modes/widgets 183 | 184 | ------------------------------------------------------------------------------- 185 | 186 | ________ ________ ________ ________ _______ 187 | ╱ ╱ ╲╱ ╲╱ ╲╱ ╲_╱ ╲ 188 | ╱ ╱ ╱ ╱ ╱ ╱ 189 | ╱ ╱ ╱ ╱ ╱ ╱ 190 | ╲__╱_____╱╲________╱╲__╱__╱__╱╲___╱____╱╲________╱ 191 | 192 | [ ROOTLESS ] 193 | "#; 194 | -------------------------------------------------------------------------------- /src/ui/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for the TUI. 2 | 3 | use std::path::{Component, Path}; 4 | 5 | use ptree::write_tree_with; 6 | use tui::{ 7 | style::{Color, Style}, 8 | widgets::{Cell, Row}, 9 | }; 10 | 11 | use crate::{ 12 | cli::global::GlobalArgs, 13 | errors::NomadError, 14 | style::models::NomadStyle, 15 | traverse::{models::DirItem, modes::NomadMode, utils::build_walker, walk_directory}, 16 | }; 17 | 18 | /// Return all app settings formatted in `Row`s. 19 | pub fn get_settings<'a>(args: &GlobalArgs) -> Vec> { 20 | let assign_boolean_flag = |label: &'a str, flag| -> Row<'a> { 21 | Row::new(vec![ 22 | Cell::from(label), 23 | Cell::from(format!("{}", flag)).style(Style::default().fg(if flag { 24 | Color::Green 25 | } else { 26 | Color::Red 27 | })), 28 | ]) 29 | }; 30 | 31 | vec![ 32 | assign_boolean_flag(" all labels", args.labels.all_labels), 33 | assign_boolean_flag(" dirs", args.modifiers.dirs), 34 | assign_boolean_flag(" disrespect", args.modifiers.disrespect), 35 | assign_boolean_flag(" hidden", args.modifiers.hidden), 36 | assign_boolean_flag(" label directories", args.labels.label_directories), 37 | Row::new(vec![ 38 | Cell::from(" max depth"), 39 | Cell::from(if let Some(ref depth) = args.modifiers.max_depth { 40 | depth.to_string() 41 | } else { 42 | "None".to_string() 43 | }) 44 | .style(Style::default().fg(if args.modifiers.max_depth.is_some() { 45 | Color::Green 46 | } else { 47 | Color::Red 48 | })), 49 | ]), 50 | Row::new(vec![ 51 | Cell::from(" max filesize"), 52 | Cell::from(if let Some(ref size) = args.modifiers.max_filesize { 53 | size.to_string() 54 | } else { 55 | "None".to_string() 56 | }) 57 | .style( 58 | Style::default().fg(if args.modifiers.max_filesize.is_some() { 59 | Color::Green 60 | } else { 61 | Color::Red 62 | }), 63 | ), 64 | ]), 65 | assign_boolean_flag(" metadata", args.meta.metadata), 66 | assign_boolean_flag(" no Git", args.style.no_git), 67 | assign_boolean_flag(" no icons", args.style.no_icons), 68 | assign_boolean_flag(" numbered", args.labels.numbers), 69 | Row::new(vec![ 70 | Cell::from(" pattern"), 71 | Cell::from(if let Some(ref pattern) = args.regex.pattern { 72 | pattern.to_string() 73 | } else { 74 | "None".to_string() 75 | }) 76 | .style(Style::default().fg(if args.regex.pattern.is_some() { 77 | Color::Green 78 | } else { 79 | Color::Red 80 | })), 81 | ]), 82 | assign_boolean_flag(" plain", args.style.plain), 83 | ] 84 | } 85 | 86 | /// Get the breadcrumbs for the target directory. 87 | pub fn get_breadcrumbs(target_directory: &str) -> Result, NomadError> { 88 | let mut breadcrumbs = Vec::new(); 89 | for component in Path::new(target_directory).canonicalize()?.components() { 90 | if let Component::Normal(section) = component { 91 | breadcrumbs.push(section.to_str().unwrap_or("?").to_string()); 92 | } 93 | } 94 | 95 | Ok(breadcrumbs) 96 | } 97 | 98 | /// Get the directory tree as a `Vec` and the directory items as an `Option>`. 99 | pub fn get_tree( 100 | args: &GlobalArgs, 101 | nomad_style: &NomadStyle, 102 | target_directory: &str, 103 | ) -> Result<(Vec, Option>), NomadError> { 104 | let (tree, config, directory_items) = walk_directory( 105 | args, 106 | NomadMode::Rootless, 107 | nomad_style, 108 | target_directory, 109 | &mut build_walker(args, target_directory, None)?, 110 | )?; 111 | 112 | // Write the tree to a buffer, then convert it to a `Vec`. 113 | let mut tree_buf = Vec::new(); 114 | write_tree_with(&tree, &mut tree_buf, &config)?; 115 | 116 | Ok(( 117 | String::from_utf8_lossy(&tree_buf) 118 | .split('\n') 119 | .map(|line| line.to_string()) 120 | .collect::>(), 121 | directory_items, 122 | )) 123 | } 124 | 125 | /// Reset all settings to its original value. 126 | pub fn reset_args(args: &mut GlobalArgs) { 127 | if args.labels.all_labels { 128 | args.labels.all_labels = false; 129 | } 130 | if args.modifiers.dirs { 131 | args.modifiers.dirs = false; 132 | } 133 | if args.modifiers.disrespect { 134 | args.modifiers.disrespect = false; 135 | } 136 | if args.export.is_some() { 137 | args.export = None; 138 | } 139 | if args.modifiers.hidden { 140 | args.modifiers.hidden = false; 141 | } 142 | if args.labels.label_directories { 143 | args.labels.label_directories = false; 144 | } 145 | if args.modifiers.max_depth.is_some() { 146 | args.modifiers.max_depth = None; 147 | } 148 | if args.modifiers.max_filesize.is_some() { 149 | args.modifiers.max_filesize = None; 150 | } 151 | if args.meta.metadata { 152 | args.meta.metadata = false; 153 | } 154 | if args.style.no_git { 155 | args.style.no_git = false; 156 | } 157 | if args.style.no_icons { 158 | args.style.no_icons = false; 159 | } 160 | if args.labels.numbers { 161 | args.labels.numbers = false; 162 | } 163 | if args.regex.pattern.is_some() { 164 | args.regex.pattern = None; 165 | } 166 | if args.style.plain { 167 | args.style.plain = false; 168 | } 169 | if args.statistics { 170 | args.statistics = false; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/utils/bat.rs: -------------------------------------------------------------------------------- 1 | //! Run `bat`. 2 | 3 | use crate::errors::NomadError; 4 | 5 | use anyhow::Result; 6 | use bat::{Input, PagingMode, PrettyPrinter, WrappingMode}; 7 | 8 | use std::path::Path; 9 | 10 | /// Create a new `PrettyPrinter`, then run it against the file. 11 | pub fn run_bat(found_items: Vec) -> Result<(), NomadError> { 12 | PrettyPrinter::new() 13 | .grid(true) 14 | .header(true) 15 | .inputs( 16 | found_items 17 | .iter() 18 | .map(|path| Input::from_file(Path::new(path))) 19 | .collect::>(), 20 | ) 21 | .line_numbers(true) 22 | .paging_mode(PagingMode::QuitIfOneScreen) 23 | .true_color(true) 24 | .vcs_modification_markers(true) 25 | .wrapping_mode(WrappingMode::Character) 26 | .print() 27 | .map_or_else(|error| Err(NomadError::BatError(error)), |_| Ok(())) 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/cache.rs: -------------------------------------------------------------------------------- 1 | //! Cache utilities for `nomad`. 2 | 3 | use std::{ 4 | fs::{create_dir_all, File}, 5 | io::Write, 6 | }; 7 | 8 | use anyhow::Result; 9 | use directories::ProjectDirs; 10 | use serde_json::Value; 11 | 12 | use crate::errors::NomadError; 13 | 14 | /// Return a JSON `File` object in write/overwrite or read-only mode. 15 | pub fn get_json_file(read_only: bool) -> Result { 16 | match ProjectDirs::from("", "", "nomad") { 17 | Some(project_directory) => { 18 | let items_json = project_directory.cache_dir().join("items.json"); 19 | 20 | if !items_json.exists() { 21 | match &items_json.parent() { 22 | Some(parent) => create_dir_all(parent)?, 23 | None => { 24 | return Err(NomadError::PathError( 25 | "Could not get the path to nomad's application directory!".to_string(), 26 | )) 27 | } 28 | } 29 | } 30 | 31 | let file = match read_only { 32 | true => File::open(items_json)?, 33 | false => File::create(items_json)?, 34 | }; 35 | 36 | Ok(file) 37 | } 38 | None => Err(NomadError::ApplicationError), 39 | } 40 | } 41 | 42 | /// Write a JSON string to `items.json`. 43 | pub fn write_to_json(json_file: &mut File, values: Value) -> Result<(), NomadError> { 44 | json_file.write_all(serde_json::to_string(&values)?.as_bytes())?; 45 | 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /src/utils/export.rs: -------------------------------------------------------------------------------- 1 | //! Export a directory's tree to a file instead of saving. 2 | 3 | use crate::errors::NomadError; 4 | 5 | use ansi_term::*; 6 | use anyhow::Result; 7 | use chrono::Local; 8 | use ptree::{item::StringItem, write_tree_with, PrintConfig}; 9 | 10 | use std::{env, fs::File, io::Write}; 11 | 12 | /// Get the absolute path for the file name. 13 | fn get_absolute_path(file_name: &str) -> Result { 14 | Ok(env::current_dir()? 15 | .join(file_name) 16 | .into_os_string() 17 | .into_string() 18 | .expect("Could not get the current directory!")) 19 | } 20 | 21 | /// Variants for export modes. 22 | pub enum ExportMode<'a> { 23 | /// `nomad` was run in filetype mode. 24 | Filetype(&'a Vec, &'a Vec), 25 | /// `nomad` was run in normal mode. 26 | Normal, 27 | /// `nomad` was run in Git branch mode. 28 | GitBranch, 29 | /// `nomad` was run in Git status mode. 30 | GitStatus, 31 | } 32 | 33 | /// Export the tree to a file. Writes to a custom filename if specified, otherwise 34 | /// the filename corresponds to the tree mode (normal, filetype, or Git status) 35 | /// and the current timestamp. 36 | pub fn export_tree( 37 | config: PrintConfig, 38 | export_mode: ExportMode, 39 | filename: &Option, 40 | tree: StringItem, 41 | ) -> Result<(), NomadError> { 42 | let mut file_header = "nomad".to_string(); 43 | 44 | let mut default_filename = match export_mode { 45 | ExportMode::Filetype(filetypes, globs) => { 46 | let mut filetype_info = "\n\n".to_string(); 47 | if !filetypes.is_empty() { 48 | filetype_info.push_str(&format!("Filetypes: {}", filetypes.join(", "))); 49 | } 50 | 51 | if !globs.is_empty() { 52 | filetype_info.push_str(&format!("\nGlobs: {}", globs.join(", "))); 53 | } 54 | 55 | filetype_info.push_str("\n\n"); 56 | 57 | file_header.push_str(&filetype_info); 58 | 59 | "filetype".to_string() 60 | } 61 | ExportMode::Normal => { 62 | file_header.push_str("\n\n"); 63 | 64 | "nomad".to_string() 65 | } 66 | ExportMode::GitBranch => { 67 | file_header.push_str("\n\nMode: Git branch\n\n"); 68 | 69 | "git_branch".to_string() 70 | } 71 | ExportMode::GitStatus => { 72 | file_header.push_str("\n\nMode: Git status\n\n"); 73 | 74 | "git_status".to_string() 75 | } 76 | }; 77 | 78 | let export_filename = if let Some(filename) = filename { 79 | filename.to_string() 80 | } else { 81 | let timestamp = Local::now().format("%F_%H-%M-%S").to_string(); 82 | default_filename.push_str(&format!("_{}.txt", timestamp)); 83 | 84 | default_filename 85 | }; 86 | 87 | let file_path = get_absolute_path(&export_filename)?; 88 | let mut file = File::create(&file_path)?; 89 | write!(file, "{}", file_header)?; 90 | 91 | write_tree_with(&tree, file, &config).map_or_else( 92 | |error| { 93 | Err(NomadError::PTreeError { 94 | context: format!("Unable to export directory tree to {file_path}"), 95 | source: error, 96 | }) 97 | }, 98 | |_| { 99 | let success_message = Colour::Green 100 | .bold() 101 | .paint(format!("Tree was exported to {file_path}\n")); 102 | println!("{success_message}"); 103 | 104 | Ok(()) 105 | }, 106 | ) 107 | } 108 | -------------------------------------------------------------------------------- /src/utils/meta.rs: -------------------------------------------------------------------------------- 1 | //! Retrieving metadata for files. 2 | 3 | use crate::cli::global::GlobalArgs; 4 | 5 | use ansi_term::Colour; 6 | use chrono::{Local, NaiveDateTime}; 7 | use unix_mode::to_string; 8 | use users::{get_group_by_gid, get_user_by_uid}; 9 | 10 | use std::path::Path; 11 | 12 | #[cfg(target_family = "unix")] 13 | use std::os::unix::fs::{MetadataExt, PermissionsExt}; 14 | #[cfg(target_family = "windows")] 15 | use std::os::windows::fs::MetadataExt; 16 | 17 | /// Convert a UNIX timestamp to a readable format. 18 | pub fn convert_time(timestamp: i64) -> String { 19 | match NaiveDateTime::from_timestamp_opt(timestamp, 0) { 20 | Some(date_time) => date_time 21 | .and_local_timezone(Local) 22 | .single() 23 | .unwrap_or(Local::now()) 24 | .format("%a %b %e %H:%M:%S %Y") 25 | .to_string(), 26 | None => "".to_string(), 27 | } 28 | } 29 | 30 | /// Convert bytes to different units depending on size. 31 | /// 32 | /// Petabyte is the largest unit of data that may be converted. Otherwise, file 33 | /// sizes will be displayed in bytes. 34 | fn convert_bytes(bytes: i64) -> String { 35 | let (convert_by, label): (i64, &str) = match bytes { 36 | 1000..=999999 => (1000, "KB"), 37 | 1000000..=9999999 => (1000000, "MB"), 38 | 1000000000..=999999999999 => (1000000000, "GB"), 39 | 1000000000000..=999999999999999 => (1000000000000, "TB"), 40 | 1000000000000000..=999999999999999999 => (1000000000000000, "PB"), 41 | _ => (1, "B"), 42 | }; 43 | 44 | let rounded_size = ((bytes as f64 / convert_by as f64) * 100.0).round() / 100.0; 45 | 46 | let final_number = if rounded_size.fract() == 0.0 { 47 | let int = rounded_size.round() as i64; 48 | format!("{int:>3}") 49 | } else if rounded_size > 10.0 { 50 | let floored_number = rounded_size.floor() as i64; 51 | format!("{floored_number:>3}") 52 | } else { 53 | format!("{:>3}", format!("{:.1}", rounded_size)) 54 | }; 55 | 56 | format!("{final_number} {label:<2}") 57 | } 58 | 59 | /// Colorize the permission bits for a file. 60 | fn colorize_permission_bits(permissions: String) -> String { 61 | let mut colored_chars: Vec = vec![]; 62 | 63 | for character in permissions.chars() { 64 | colored_chars.push(match character { 65 | 'd' => Colour::Blue.paint(format!("{character}")).to_string(), 66 | 'r' => Colour::Yellow.paint(format!("{character}")).to_string(), 67 | 's' | 'S' => Colour::Purple.paint(format!("{character}")).to_string(), 68 | 't' | 'T' => Colour::Purple.paint(format!("{character}")).to_string(), 69 | 'w' => Colour::Fixed(172).paint(format!("{character}")).to_string(), // Orange. 70 | 'x' => Colour::Red.paint(format!("{character}")).to_string(), 71 | _ => Colour::White 72 | .dimmed() 73 | .paint(format!("{character}")) 74 | .to_string(), 75 | }) 76 | } 77 | 78 | colored_chars.into_iter().collect::() 79 | } 80 | 81 | /// Get the metadata for a directory or file. 82 | /// 83 | /// This is only compiled when on UNIX systems. 84 | #[cfg(target_family = "unix")] 85 | pub fn get_metadata(args: &GlobalArgs, item: &Path) -> String { 86 | let metadata = item.metadata().ok(); 87 | 88 | if let Some(metadata) = metadata { 89 | let plain_group = get_group_by_gid(metadata.gid()) 90 | .expect("None") 91 | .name() 92 | .to_str() 93 | .expect("None") 94 | .to_string(); 95 | let group = if args.style.plain || args.style.no_colors { 96 | plain_group 97 | } else { 98 | Colour::Fixed(193) 99 | .paint( 100 | get_group_by_gid(metadata.gid()) 101 | .expect("None") 102 | .name() 103 | .to_str() 104 | .expect("None"), 105 | ) 106 | .to_string() 107 | }; 108 | 109 | let plain_mode = to_string(metadata.permissions().mode()); 110 | let mode = if args.style.plain || args.style.no_colors { 111 | plain_mode 112 | } else { 113 | colorize_permission_bits(plain_mode) 114 | }; 115 | 116 | let plain_last_modified = convert_time(metadata.mtime()); 117 | let last_modified = if args.style.plain || args.style.no_colors { 118 | plain_last_modified 119 | } else { 120 | Colour::Fixed(035).paint(plain_last_modified).to_string() 121 | }; 122 | 123 | let plain_size = i64::try_from(metadata.size()) 124 | .map_or("unknown file size".to_string(), |converted_bytes| { 125 | convert_bytes(converted_bytes) 126 | }); 127 | let size = if args.style.plain || args.style.no_colors { 128 | plain_size 129 | } else { 130 | Colour::Fixed(172).paint(plain_size).to_string() 131 | }; 132 | 133 | let plain_user = get_user_by_uid(metadata.uid()) 134 | .expect("None") 135 | .name() 136 | .to_str() 137 | .expect("None") 138 | .to_string(); 139 | let user = if args.style.plain || args.style.no_colors { 140 | plain_user 141 | } else { 142 | Colour::Fixed(194).paint(plain_user).to_string() 143 | }; 144 | 145 | format!("{mode} {user} {group} {size} {last_modified}") 146 | } else { 147 | let missing_message = "-- No metadata available for this item --"; 148 | 149 | if args.style.plain || args.style.no_colors { 150 | missing_message.to_string() 151 | } else { 152 | Colour::Red 153 | .bold() 154 | .paint("-- No metadata available for this item --") 155 | .to_string() 156 | } 157 | } 158 | } 159 | 160 | /// Get the metadata for a directory or file. 161 | /// 162 | /// This is only compiled when on Windows systems. 163 | #[cfg(target_family = "windows")] 164 | pub fn get_metadata(args: &GlobalArgs, item: &Path) -> String { 165 | let metadata = item.metadata().ok(); 166 | 167 | if let Some(metadata) = metadata { 168 | let plain_file_attributes = match metadata.file_attributes() { 169 | 1 => "FILE_ATTRIBUTE_READONLY", 170 | 2 => "FILE_ATTRIBUTE_HIDDEN", 171 | 4 => "FILE_ATTRIBUTE_SYSTEM", 172 | 16 => "FILE_ATTRIBUTE_DIRECTORY", 173 | 32 => "FILE_ATTRIBUTE_ARCHIVE", 174 | 64 => "FILE_ATTRIBUTE_DEVICE", 175 | 128 => "FILE_ATTRIBUTE_NORMAL", 176 | 256 => "FILE_ATTRIBUTE_TEMPORARY", 177 | 512 => "FILE_ATTRIBUTE_SPARSE_FILE", 178 | 1024 => "FILE_ATTRIBUTE_REPARSE_POINT", 179 | 2048 => "FILE_ATTRIBUTE_COMPRESSED", 180 | 4096 => "FILE_ATTRIBUTE_OFFLINE", 181 | 8192 => "FILE_ATTRIBUTE_NOT_CONTENT_INDEXED", 182 | 16384 => "FILE_ATTRIBUTE_ENCRYPTED", 183 | 32768 => "FILE_ATTRIBUTE_INTEGRITY_STREAM", 184 | 65536 => "FILE_ATTRIBUTE_VIRTUAL", 185 | 131072 => "FILE_ATTRIBUTE_NO_SCRUB_DATA", 186 | 262144 => "FILE_ATTRIBUTE_RECALL_ON_OPEN", 187 | 4194304 => "FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS", 188 | }; 189 | let file_attributes = if args.style.plain || args.style.no_colors { 190 | plain_file_attributes.to_string() 191 | } else { 192 | Colour::Fixed(193) 193 | .paint(format!("{}", plain_file_attributes)) 194 | .to_string() 195 | }; 196 | 197 | let plain_last_modified = i64::try_from(metadata.last_write_time()).map_or( 198 | "unknown last modified time".to_string(), 199 | |converted_value| convert_time(converted_value), 200 | ); 201 | let last_modified = if args.style.plain || args.style.no_colors { 202 | plain_last_modified 203 | } else { 204 | Colour::Fixed(035).paint(plain_last_modified).to_string() 205 | }; 206 | 207 | let plain_size = i64::try_from(metadata.file_size()) 208 | .map_or("unknown file size".to_string(), |converted_bytes| { 209 | convert_bytes(converted_bytes) 210 | }); 211 | let size = if args.style.plain || args.style.no_colors { 212 | plain_size 213 | } else { 214 | Colour::Fixed(172).paint(plain_size).to_string() 215 | }; 216 | 217 | format!("{file_attributes} {last_modified} {size}") 218 | } else { 219 | Colour::Red 220 | .bold() 221 | .paint("-- No metadata available for this item --") 222 | .to_string() 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utilities used throughout this program. 2 | 3 | pub mod bat; 4 | pub mod cache; 5 | pub mod export; 6 | pub mod icons; 7 | pub mod meta; 8 | pub mod open; 9 | pub mod paint; 10 | pub mod paths; 11 | pub mod search; 12 | pub mod table; 13 | -------------------------------------------------------------------------------- /src/utils/open.rs: -------------------------------------------------------------------------------- 1 | //! Open a file using the client's system's `$EDITOR`. 2 | 3 | use super::cache::get_json_file; 4 | use crate::{errors::NomadError, models::Contents}; 5 | 6 | use anyhow::{anyhow, Result}; 7 | use serde_json::{self, from_str}; 8 | 9 | use std::{ 10 | env::var, 11 | io::Read, 12 | process::{Command, ExitStatus}, 13 | }; 14 | 15 | /// Open the target file with an editor. 16 | fn spawn_editor(editor: String, found_items: Vec) -> Result { 17 | Command::new(editor.clone()) 18 | .args(&found_items) 19 | .status() 20 | .map_err(|error| NomadError::EditorError { 21 | editor, 22 | reason: error, 23 | }) 24 | } 25 | 26 | /// Get the default text editor from the environment. If that environment variable 27 | /// is not set, try to open the file with Neovim, then Vim, and finally Nano. 28 | fn get_text_editors() -> Vec { 29 | var("EDITOR").map_or( 30 | vec![ 31 | "nvim".to_string(), 32 | "vim".to_string(), 33 | "vi".to_string(), 34 | "nano".to_string(), 35 | ], 36 | |editor| vec![editor], 37 | ) 38 | } 39 | 40 | /// Get the deserialized JSON file. 41 | pub fn get_deserialized_json() -> Result { 42 | let mut file = get_json_file(true)?; 43 | let mut data = String::new(); 44 | file.read_to_string(&mut data)?; 45 | 46 | Ok(from_str(&data)?) 47 | } 48 | 49 | /// Open the target file. 50 | pub fn open_files(found_items: Vec) -> Result<(), NomadError> { 51 | let editors = get_text_editors(); 52 | 53 | if editors.len() == 1 { 54 | spawn_editor(editors[0].to_string(), found_items).map_or_else(Err, |status_code| { 55 | println!("{status_code}"); 56 | 57 | Ok(()) 58 | }) 59 | } else { 60 | for editor in editors { 61 | if spawn_editor(editor, found_items.clone()).is_ok() { 62 | return Ok(()); 63 | }; 64 | } 65 | 66 | Err(NomadError::Error(anyhow!("Could not open the file with your $EDITOR, Neovim, Vim, Vi, or Nano!\nDo you have one of these editors installed?"))) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/utils/paths.rs: -------------------------------------------------------------------------------- 1 | //! Miscellaneous utilities for dealing with file paths. 2 | 3 | use crate::errors::NomadError; 4 | 5 | use anyhow::{Context, Result}; 6 | 7 | use std::{ 8 | env, 9 | ffi::OsStr, 10 | fs::read_link, 11 | path::{Path, PathBuf}, 12 | }; 13 | 14 | /// Get the current directory. 15 | pub fn get_current_directory() -> Result { 16 | Ok(env::current_dir() 17 | .with_context(|| "Could not get the current directory!")? 18 | .into_os_string() 19 | .into_string() 20 | .expect("Could not get the current directory!")) 21 | } 22 | 23 | /// Get the absolute file path based for the target_string. 24 | pub fn canonicalize_path(target: &str) -> Result { 25 | PathBuf::from(target) 26 | .canonicalize() 27 | .with_context(|| format!("\"{target}\" is not a directory!"))? 28 | .into_os_string() 29 | .into_string() 30 | .map_or( 31 | Err(NomadError::PathError(format!( 32 | "Could not canonicalize path to {target}" 33 | ))), 34 | Ok, 35 | ) 36 | } 37 | 38 | /// Get the filename for a `Path`. 39 | pub fn get_filename(item: &Path) -> String { 40 | item.file_name() 41 | .unwrap_or_else(|| OsStr::new("?")) 42 | .to_str() 43 | .unwrap_or("?") 44 | .to_string() 45 | } 46 | 47 | /// Get the symlinked item. 48 | pub fn get_symlink(item: &Path) -> String { 49 | let points_to = read_link(item).map_or("?".to_string(), |pathbuf_path| { 50 | pathbuf_path 51 | .canonicalize() 52 | .unwrap_or_else(|_| PathBuf::from("?")) 53 | .into_os_string() 54 | .into_string() 55 | .map_or("?".to_string(), |path_string| path_string) 56 | }); 57 | 58 | format!("⇒ {points_to}") 59 | } 60 | -------------------------------------------------------------------------------- /src/utils/search.rs: -------------------------------------------------------------------------------- 1 | //! Search for a file within the tree. 2 | 3 | use std::path::Path; 4 | 5 | use crate::{cli::Args, git::markers::get_status_markers, style::models::NomadStyle}; 6 | 7 | use super::open::get_deserialized_json; 8 | 9 | use ansi_term::Colour; 10 | use git2::Repository; 11 | 12 | /// Modes for file searching. 13 | pub enum SearchMode { 14 | /// Search for files when using Git commands besides Git diff. If a directory 15 | /// label is passed, directory items will be compared with the Git status marker 16 | /// map. Only matching items that are tracked by Git AND are changed will be returned. 17 | Git, 18 | /// Search for changed items that are tracked by Git. Mutes the warning message 19 | /// that usually appears if no item labels are passed into a subcommand. 20 | GitDiff, 21 | /// Search for files in normal mode. If a directory label is passed, all 22 | /// directory items are returned regardless of Git status. 23 | Normal, 24 | } 25 | 26 | /// Get files by its number in the tree, or traverse a directory and return all files 27 | /// within it. 28 | /// 29 | /// If this function is in Git mode and directory labels are passed into 30 | /// `item_labels`, only matching items that are tracked by Git AND are changed 31 | /// will be returned. 32 | /// 33 | /// If this function is in normal mode and directory labels are passed into 34 | /// `item_labels`, all items within that directory are returned. 35 | pub fn indiscriminate_search( 36 | args: &Args, 37 | item_labels: &[String], 38 | nomad_style: &NomadStyle, 39 | repo: Option<&Repository>, 40 | search_mode: SearchMode, 41 | target_directory: &str, 42 | ) -> Option> { 43 | if let Ok(contents) = get_deserialized_json() { 44 | let mut found: Vec = Vec::new(); 45 | let mut not_found: Vec = Vec::new(); 46 | 47 | for label in item_labels { 48 | match label.parse::() { 49 | Ok(_) => match contents.numbered.get(label) { 50 | Some(file_path) => { 51 | found.push(file_path.to_string()); 52 | } 53 | None => not_found.push(label.into()), 54 | }, 55 | Err(_) => match contents.labeled.get(label) { 56 | Some(directory_path) => match search_mode { 57 | SearchMode::Git | SearchMode::GitDiff => { 58 | if let Some(repo) = repo { 59 | if let Ok(marker_map) = get_status_markers( 60 | &args.global.style, 61 | nomad_style, 62 | repo, 63 | target_directory, 64 | ) { 65 | for file_path in marker_map.keys() { 66 | let path_parent = Path::new(file_path) 67 | .parent() 68 | .unwrap_or_else(|| Path::new("?")) 69 | .to_str() 70 | .unwrap_or("?"); 71 | 72 | if path_parent.contains(directory_path) { 73 | found.push(file_path.to_string()); 74 | } 75 | } 76 | } else { 77 | println!( 78 | "{}", 79 | Colour::Red.bold().paint( 80 | "\nCould not get the HashMap containing Git items!\n" 81 | ) 82 | ); 83 | } 84 | } else { 85 | println!( 86 | "{}", 87 | Colour::Red.bold().paint( 88 | "\nUnable to search for Git files: The Git repository is missing!\n" 89 | ) 90 | ); 91 | } 92 | } 93 | SearchMode::Normal => { 94 | for path in contents.numbered.values() { 95 | if path.contains(directory_path) { 96 | found.push(path.to_owned()); 97 | } 98 | } 99 | } 100 | }, 101 | None => not_found.push(label.into()), 102 | }, 103 | }; 104 | } 105 | 106 | if !not_found.is_empty() { 107 | println!( 108 | "{}", 109 | Colour::Fixed(172).bold().paint( 110 | "\nThe following item numbers or directory labels did not match any items in the tree:\n" 111 | ) 112 | ); 113 | 114 | for label in not_found { 115 | println!("==> {}", Colour::Fixed(172).bold().paint(label)); 116 | } 117 | } 118 | 119 | if !found.is_empty() { 120 | Some(found) 121 | } else { 122 | match search_mode { 123 | SearchMode::Git => println!( 124 | "{}", 125 | Colour::Fixed(172).bold().paint( 126 | "\nDid not find any changed files matching the labels you've entered.\nAre you sure the file or directory contains changed files tracked by Git?\n" 127 | ) 128 | ), 129 | SearchMode::GitDiff => { 130 | if !item_labels.is_empty() { 131 | println!( 132 | "{}", 133 | Colour::Fixed(172).bold().paint("\nDid not find any changed files matching the labels you've entered.\nDisplaying all diffs.\n")); 134 | } 135 | } 136 | SearchMode::Normal => println!("{}", Colour::Red.bold().paint("\nNo items were matched!\n")), 137 | } 138 | 139 | None 140 | } 141 | } else { 142 | println!( 143 | "{}", 144 | Colour::Red 145 | .bold() 146 | .paint("\nCould not retrieve stored directories and directory contents!\n") 147 | ); 148 | 149 | None 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/utils/table.rs: -------------------------------------------------------------------------------- 1 | //! Displaying items in a neat table. 2 | 3 | use ansi_term::Colour; 4 | use ignore::types::FileTypeDef; 5 | use self_update::update::Release; 6 | use term_table::{row::Row, table_cell::TableCell, Table, TableStyle}; 7 | 8 | /// Contains information used to build a table. 9 | pub struct TabledItems { 10 | /// Items of type `T` to iterate. 11 | items: Vec, 12 | /// Table labels. 13 | labels: Vec, 14 | /// The max width of the table when displayed in the terminal. 15 | table_width: usize, 16 | /// An optional target to search for in the Vec of `items`. 17 | target: Option, 18 | } 19 | 20 | impl TabledItems { 21 | /// Create a new `TabledItems` object. 22 | pub fn new( 23 | items: Vec, 24 | labels: Vec, 25 | table_width: usize, 26 | target: Option, 27 | ) -> Self { 28 | Self { 29 | items, 30 | labels, 31 | table_width, 32 | target, 33 | } 34 | } 35 | } 36 | 37 | /// Enables a table view for a particular type. 38 | pub trait TableView { 39 | /// Create and then display a table for this type of item. 40 | fn display_table(self) 41 | where 42 | Self: Sized, 43 | { 44 | } 45 | } 46 | 47 | impl TableView for TabledItems { 48 | /// Display a table for `String`s. 49 | fn display_table(self) { 50 | let mut table = Table::new(); 51 | 52 | table.max_column_width = self.table_width; 53 | table.style = TableStyle::rounded(); 54 | 55 | table.add_row(Row::new(self.labels.iter().map(|label| { 56 | TableCell::new(Colour::White.bold().paint(label).to_string()) 57 | }))); 58 | 59 | for item in self.items { 60 | table.add_row(Row::new(vec![TableCell::new(item)])); 61 | } 62 | 63 | println!("\n{}", table.render()); 64 | } 65 | } 66 | 67 | impl TableView for TabledItems<(&str, &String, &String)> { 68 | /// Display a table for items that need to be contained in a `(&str, &String, &String)`. 69 | fn display_table(self) { 70 | let mut table = Table::new(); 71 | 72 | table.max_column_width = self.table_width; 73 | table.style = TableStyle::rounded(); 74 | 75 | table.add_row(Row::new( 76 | self.labels 77 | .iter() 78 | .map(|label| TableCell::new(Colour::White.bold().paint(label).to_string())) 79 | .collect::>(), 80 | )); 81 | 82 | for (git_status, marker, filename_style) in self.items { 83 | table.add_row(Row::new(vec![ 84 | TableCell::new(git_status), 85 | TableCell::new(marker), 86 | TableCell::new(filename_style), 87 | ])); 88 | } 89 | 90 | println!("\n{}", table.render()); 91 | } 92 | } 93 | 94 | impl TableView for TabledItems { 95 | /// List all filetypes and their associated globs. Or optionally search for 96 | /// a filetype. 97 | fn display_table(self) { 98 | let mut table = Table::new(); 99 | 100 | table.max_column_width = self.table_width; 101 | table.style = TableStyle::rounded(); 102 | 103 | table.add_row(Row::new(vec![ 104 | TableCell::new(Colour::White.bold().paint("Name").to_string()), 105 | TableCell::new(Colour::White.bold().paint("Globs").to_string()), 106 | ])); 107 | 108 | let mut found = false; 109 | for definition in self.items { 110 | if let Some(ref filetype) = self.target { 111 | if definition.name() == filetype { 112 | table.add_row(Row::new(vec![ 113 | TableCell::new(definition.name()), 114 | TableCell::new(definition.globs().join("\n")), 115 | ])); 116 | 117 | found = true; 118 | break; 119 | } 120 | } else { 121 | table.add_row(Row::new(vec![ 122 | TableCell::new(definition.name()), 123 | TableCell::new(definition.globs().join("\n")), 124 | ])); 125 | } 126 | } 127 | 128 | if let (Some(filetype), false) = (self.target, found) { 129 | println!( 130 | "{}", 131 | Colour::Red 132 | .bold() 133 | .paint(format!("\nNo globs available for {filetype} filetypes!\n")) 134 | ); 135 | 136 | return; 137 | } 138 | 139 | println!("\n{}", table.render()); 140 | } 141 | } 142 | 143 | impl TableView for TabledItems { 144 | /// List all releases for `nomad`. Or optionally search for a version number. 145 | fn display_table(self) { 146 | let mut table = Table::new(); 147 | 148 | table.max_column_width = self.table_width; 149 | table.style = TableStyle::rounded(); 150 | 151 | table.add_row(Row::new(self.labels.into_iter().map(|label| { 152 | TableCell::new(Colour::White.bold().paint(label).to_string()) 153 | }))); 154 | 155 | let mut found = false; 156 | for release in self.items { 157 | if let Some(ref version) = self.target { 158 | if version == &release.version { 159 | let mut assets_table = Table::new(); 160 | assets_table.add_row(Row::new(vec![ 161 | TableCell::new("Asset Name"), 162 | TableCell::new("Download URL"), 163 | ])); 164 | 165 | for asset in release.assets { 166 | assets_table.add_row(Row::new(vec![TableCell::new(asset.name)])); 167 | assets_table.add_row(Row::new(vec![TableCell::new(asset.download_url)])); 168 | } 169 | 170 | table.add_row(Row::new(vec![ 171 | TableCell::new(release.name), 172 | TableCell::new(release.version), 173 | TableCell::new(release.date), 174 | TableCell::new(match release.body { 175 | Some(body) => body, 176 | None => "".to_string(), 177 | }), 178 | TableCell::new(assets_table), 179 | ])); 180 | 181 | found = true; 182 | break; 183 | } 184 | } else { 185 | let mut assets_table = Table::new(); 186 | for asset in release.assets { 187 | assets_table.add_row(Row::new(vec![TableCell::new(asset.name)])); 188 | assets_table.add_row(Row::new(vec![TableCell::new(asset.download_url)])); 189 | } 190 | 191 | table.add_row(Row::new(vec![ 192 | TableCell::new(release.name), 193 | TableCell::new(release.version), 194 | TableCell::new(release.date), 195 | TableCell::new(match release.body { 196 | Some(body) => body, 197 | None => "".to_string(), 198 | }), 199 | TableCell::new(assets_table), 200 | ])); 201 | } 202 | } 203 | 204 | if let (Some(version), false) = (self.target, found) { 205 | println!( 206 | "{}", 207 | Colour::Red 208 | .bold() 209 | .paint(format!("\nDid not find a version matching {version}!\n")) 210 | ); 211 | 212 | return; 213 | } 214 | 215 | println!("\n{}", table.render()); 216 | } 217 | } 218 | --------------------------------------------------------------------------------