mirror of
				https://github.com/sharkdp/bat.git
				synced 2025-10-31 07:04:04 +00:00 
			
		
		
		
	Compare commits
	
		
			244 Commits
		
	
	
		
			v0.24.0
			...
			update-war
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 018a482621 | ||
|  | 4790def1ef | ||
|  | 07c26adc35 | ||
|  | f29f9387b5 | ||
|  | c290bfff1e | ||
|  | 42153f2b99 | ||
|  | 6d7537d3ec | ||
|  | b30ec9f975 | ||
|  | a7074f10d4 | ||
|  | d185f0973b | ||
|  | 071874ea8f | ||
|  | 46a2c004a2 | ||
|  | 26ac179548 | ||
|  | 4c85483486 | ||
|  | 487bed2d95 | ||
|  | 6f69682552 | ||
|  | bc5beaec5d | ||
|  | 83b00bc653 | ||
|  | f041ff8c5f | ||
|  | 1fbdbfc4b2 | ||
|  | 2323aa0def | ||
|  | 6c2ce63101 | ||
|  | 13204c46e2 | ||
|  | 9bb0271e7d | ||
|  | 0e4e10edb6 | ||
|  | 0c7e5299bf | ||
|  | c36ed32816 | ||
|  | e1a3fc5529 | ||
|  | 1ae9e843ed | ||
|  | dbe4cfb763 | ||
|  | 4549f83689 | ||
|  | e6e8f847be | ||
|  | b9e249f782 | ||
|  | 3ffa3648cf | ||
|  | 5c2cc53882 | ||
|  | a6f01af8de | ||
|  | 85a549e293 | ||
|  | b718889ba2 | ||
|  | 708c74f6af | ||
|  | 74d666f5c0 | ||
|  | 7604fe5567 | ||
|  | 0080b043c4 | ||
|  | c7bce46622 | ||
|  | 2b4339663c | ||
|  | 6a6b02117b | ||
|  | 511cd30105 | ||
|  | 92915e22e7 | ||
|  | d499191b0a | ||
|  | 152d69fe98 | ||
|  | 81aa24310c | ||
|  | 75cdabaf13 | ||
|  | 1f628203e5 | ||
|  | 1b9fc1d5af | ||
|  | bc1ca1a346 | ||
|  | f735120978 | ||
|  | 25b5a41189 | ||
|  | c94cf4e14e | ||
|  | 84d80eebd0 | ||
|  | 915dd9fbf8 | ||
|  | 9d77c1373c | ||
|  | c3f2ddf509 | ||
|  | 8a51172b11 | ||
|  | 875046e4cd | ||
|  | a5bd9f51be | ||
|  | 5a2a20af42 | ||
|  | 61029c8bd2 | ||
|  | 1023399c5e | ||
|  | 6549e26f5d | ||
|  | 165c495e75 | ||
|  | 6b9b085be3 | ||
|  | 2d46d54ae3 | ||
|  | 3d04699710 | ||
|  | 054421268f | ||
|  | 414403b062 | ||
|  | c29bf2ff28 | ||
|  | ab4e5ed52e | ||
|  | 1a54c9bf6d | ||
|  | 02077db53e | ||
|  | 7ce010d9ed | ||
|  | 95993cf37e | ||
|  | 3761df9112 | ||
|  | adfaef19da | ||
|  | f7bea6de5b | ||
|  | 65aae5d0a1 | ||
|  | e3866b1f7e | ||
|  | 23de8e093b | ||
|  | 196a4cb18f | ||
|  | 695cf1f387 | ||
|  | 0af1df5258 | ||
|  | a8d07333e9 | ||
|  | 7f12989127 | ||
|  | 60e32cf823 | ||
|  | e9a6aaa30f | ||
|  | 9be2a36a01 | ||
|  | 22254936a2 | ||
|  | f6d76e0104 | ||
|  | c911829771 | ||
|  | b33e33fe26 | ||
|  | 9239b125b1 | ||
|  | 2086cd2668 | ||
|  | 1b88267320 | ||
|  | e586751208 | ||
|  | e7256a624b | ||
|  | 5c1f47359e | ||
|  | 45ee2dc4c7 | ||
|  | db66e4459b | ||
|  | 55e02e101d | ||
|  | 230abfd2bc | ||
|  | c0f2d6f934 | ||
|  | 9f36a7a284 | ||
|  | e4d637a3d8 | ||
|  | 98a2b6bc17 | ||
|  | 8e66bc8722 | ||
|  | cd81c7fa6b | ||
|  | b4fdb5dc36 | ||
|  | c76ed99db2 | ||
|  | 06aef22943 | ||
|  | 128b0d6dd3 | ||
|  | 15dc20109f | ||
|  | 5c4bcd6611 | ||
|  | ecf4029dc7 | ||
|  | c261b41578 | ||
|  | 6f1cc80d68 | ||
|  | 3b0ade9cb8 | ||
|  | 57016f4e04 | ||
|  | 497342fabb | ||
|  | bf56cd90f0 | ||
|  | 0acb979e9e | ||
|  | d7503bfc09 | ||
|  | b89dc15be1 | ||
|  | 15ab4478c9 | ||
|  | 5b4ce684a1 | ||
|  | 0027055a83 | ||
|  | 321b3ec81b | ||
|  | 1679460f42 | ||
|  | 907af9e35f | ||
|  | 12b74dfb4e | ||
|  | fd84e4f49f | ||
|  | f0a6fe216d | ||
|  | d792dc5804 | ||
|  | 8a08025091 | ||
|  | 586c804b1e | ||
|  | e30161ac3c | ||
|  | 3865908439 | ||
|  | 9474b4cf8b | ||
|  | b48bda21a3 | ||
|  | daf33709a0 | ||
|  | 36073a3d95 | ||
|  | 12fa2cb1eb | ||
|  | 1f10d846a3 | ||
|  | 22531eab90 | ||
|  | 0c1b80faab | ||
|  | 2c9bf229e1 | ||
|  | 822e81bb24 | ||
|  | ad628c0471 | ||
|  | f483d2df42 | ||
|  | 4ad3002543 | ||
|  | cfd622d6e1 | ||
|  | 1c7c9a6b6d | ||
|  | 0c93ca80f4 | ||
|  | de6d418d42 | ||
|  | c016b462c0 | ||
|  | 7e1fbcfe95 | ||
|  | 4815b6155e | ||
|  | 075b5b288a | ||
|  | 7cfd1e0d78 | ||
|  | 9f7d70f642 | ||
|  | 0fea82cff9 | ||
|  | 64840fbbae | ||
|  | 827b3eca2f | ||
|  | 9478d2dfe8 | ||
|  | d24501ab5e | ||
|  | 9f4259721a | ||
|  | 77e491161c | ||
|  | 97780f987e | ||
|  | d1bc0ef0d4 | ||
|  | 52f94b4623 | ||
|  | 37fd050100 | ||
|  | 83286975ff | ||
|  | f705fcb984 | ||
|  | 9ca1f20f43 | ||
|  | 6ad800e43a | ||
|  | 069318b139 | ||
|  | b9b554248d | ||
|  | 4863d428dd | ||
|  | 2e103ee6b3 | ||
|  | 28990bc451 | ||
|  | 748e2a681f | ||
|  | bfa0b5241f | ||
|  | 4af4bfc0f1 | ||
|  | 51203ff750 | ||
|  | 96cef9a24e | ||
|  | b43d31b75a | ||
|  | ad3ff26960 | ||
|  | 86b40993c3 | ||
|  | 31bed250ba | ||
|  | 7658334645 | ||
|  | 491ae70aa9 | ||
|  | d64c568196 | ||
|  | b5982a6174 | ||
|  | 04e7d2a313 | ||
|  | bcc2de86b4 | ||
|  | 1296aea836 | ||
|  | 5498c24c33 | ||
|  | 79a03b4299 | ||
|  | f3a5e9a73c | ||
|  | 2710a19ecb | ||
|  | 6d0ef259f6 | ||
|  | b1577cc083 | ||
|  | 28d947fd8b | ||
|  | b000db8f32 | ||
|  | 116a6cc9a8 | ||
|  | c8291a36b7 | ||
|  | 8180c76890 | ||
|  | a0f33b1cdc | ||
|  | 8b60dae81c | ||
|  | 4b33093f9e | ||
|  | 3d87b25b19 | ||
|  | f2f6902279 | ||
|  | c0b17e73e1 | ||
|  | 94544d963b | ||
|  | 72abbd22de | ||
|  | 64e10ffb21 | ||
|  | 35d8146bba | ||
|  | a5a7ede698 | ||
|  | b551049706 | ||
|  | 99cfc13eab | ||
|  | 4b0b5afa13 | ||
|  | d343428441 | ||
|  | 16e409ec87 | ||
|  | 94d059f258 | ||
|  | c8b9de889d | ||
|  | 75340d54f9 | ||
|  | b28383e0fa | ||
|  | 8e866db281 | ||
|  | 0eb157e090 | ||
|  | 85636c28bc | ||
|  | a70e5c6c65 | ||
|  | 32e01f740b | ||
|  | 7b20f8fc7b | ||
|  | 86ac48d68e | ||
|  | c42fc810ea | ||
|  | 6baebd79fa | ||
|  | c6cae09f99 | 
							
								
								
									
										23
									
								
								.github/workflows/Auto-merge-dependabot-PRs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.github/workflows/Auto-merge-dependabot-PRs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| # This workflow triggers auto-merge of any PR that dependabot creates so that | ||||
| # PRs will be merged automatically without maintainer intervention if CI passes | ||||
| name: Auto-merge dependabot PRs | ||||
|  | ||||
| on: | ||||
|   pull_request_target: | ||||
|     types: [opened] | ||||
|  | ||||
| jobs: | ||||
|   auto-merge: | ||||
|     if: github.repository == 'sharkdp/bat' && startsWith(github.head_ref, 'dependabot/') | ||||
|     runs-on: ubuntu-latest | ||||
|     environment: | ||||
|       name: auto-merge | ||||
|       url: https://github.com/sharkdp/bat/blob/main/.github/workflows/Auto-merge-dependabot-PRs.yml | ||||
|     env: | ||||
|       GITHUB_TOKEN: ${{ secrets.AUTO_MERGE_GITHUB_TOKEN }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - run: | | ||||
|           gh pr review ${{ github.event.pull_request.number }} --comment --body "If CI passes, this dependabot PR will be [auto-merged](https://github.com/sharkdp/bat/blob/main/.github/workflows/Auto-merge-dependabot-PRs.yml) 🚀" | ||||
|       - run: | | ||||
|           gh pr merge --auto --squash ${{ github.event.pull_request.number }} | ||||
							
								
								
									
										33
									
								
								.github/workflows/CICD.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										33
									
								
								.github/workflows/CICD.yml
									
									
									
									
										vendored
									
									
								
							| @@ -163,17 +163,17 @@ jobs: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         job: | ||||
|           - { target: aarch64-unknown-linux-gnu   , os: ubuntu-20.04, use-cross: true } | ||||
|           - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } | ||||
|           - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } | ||||
|           - { target: i686-pc-windows-msvc        , os: windows-2019                  } | ||||
|           - { target: i686-unknown-linux-gnu      , os: ubuntu-20.04, use-cross: true } | ||||
|           - { target: i686-unknown-linux-musl     , os: ubuntu-20.04, use-cross: true } | ||||
|           - { target: x86_64-apple-darwin         , os: macos-12                      } | ||||
|           - { target: x86_64-pc-windows-gnu       , os: windows-2019                  } | ||||
|           - { target: x86_64-pc-windows-msvc      , os: windows-2019                  } | ||||
|           - { target: x86_64-unknown-linux-gnu    , os: ubuntu-20.04, use-cross: true } | ||||
|           - { target: x86_64-unknown-linux-musl   , os: ubuntu-20.04, use-cross: true } | ||||
|           - { target: aarch64-unknown-linux-gnu   , os: ubuntu-20.04, dpkg_arch: arm64,            use-cross: true } | ||||
|           - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, dpkg_arch: armhf,            use-cross: true } | ||||
|           - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, dpkg_arch: musl-linux-armhf, use-cross: true } | ||||
|           - { target: i686-pc-windows-msvc        , os: windows-2019,                                              } | ||||
|           - { target: i686-unknown-linux-gnu      , os: ubuntu-20.04, dpkg_arch: i686,             use-cross: true } | ||||
|           - { target: i686-unknown-linux-musl     , os: ubuntu-20.04, dpkg_arch: musl-linux-i686,  use-cross: true } | ||||
|           - { target: x86_64-apple-darwin         , os: macos-12,                                                  } | ||||
|           - { target: x86_64-pc-windows-gnu       , os: windows-2019,                                              } | ||||
|           - { target: x86_64-pc-windows-msvc      , os: windows-2019,                                              } | ||||
|           - { target: x86_64-unknown-linux-gnu    , os: ubuntu-20.04, dpkg_arch: amd64,            use-cross: true } | ||||
|           - { target: x86_64-unknown-linux-musl   , os: ubuntu-20.04, dpkg_arch: musl-linux-amd64, use-cross: true } | ||||
|     env: | ||||
|       BUILD_CMD: cargo | ||||
|     steps: | ||||
| @@ -337,16 +337,7 @@ jobs: | ||||
|         DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }}-musl | ||||
|         case ${{ matrix.job.target }} in *-musl) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac; | ||||
|         DPKG_VERSION=${{ needs.crate_metadata.outputs.version }} | ||||
|  | ||||
|         unset DPKG_ARCH | ||||
|         case ${{ matrix.job.target }} in | ||||
|           aarch64-*-linux-*) DPKG_ARCH=arm64 ;; | ||||
|           arm-*-linux-*hf) DPKG_ARCH=armhf ;; | ||||
|           i686-*-linux-*) DPKG_ARCH=i686 ;; | ||||
|           x86_64-*-linux-*) DPKG_ARCH=amd64 ;; | ||||
|           *) DPKG_ARCH=notset ;; | ||||
|         esac; | ||||
|  | ||||
|         DPKG_ARCH="${{ matrix.job.dpkg_arch }}" | ||||
|         DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb" | ||||
|         echo "DPKG_NAME=${DPKG_NAME}" >> $GITHUB_OUTPUT | ||||
|  | ||||
|   | ||||
							
								
								
									
										33
									
								
								.github/workflows/require-changelog-for-PRs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								.github/workflows/require-changelog-for-PRs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| name: Changelog | ||||
|  | ||||
| on: | ||||
|   pull_request: | ||||
|  | ||||
| jobs: | ||||
|   check-changelog: | ||||
|     name: Check for changelog entry | ||||
|     runs-on: ubuntu-latest | ||||
|     # dependabot PRs are automerged if CI passes; we shouldn't block these | ||||
|     if: github.actor != 'dependabot[bot]' | ||||
|     env: | ||||
|       PR_NUMBER: ${{ github.event.number }} | ||||
|       PR_BASE: ${{ github.base_ref }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - name: Fetch PR base | ||||
|         run: git fetch --no-tags --prune --depth=1 origin | ||||
|  | ||||
|       # cannot use `github.actor`: the triggering commit may be authored by a maintainer | ||||
|       - name: Get PR submitter | ||||
|         id: get-submitter | ||||
|         run: curl -sSfL https://api.github.com/repos/sharkdp/bat/pulls/${PR_NUMBER} | jq -r '"submitter=" + .user.login' | tee -a $GITHUB_OUTPUT | ||||
|  | ||||
|       - name: Search for added line in changelog | ||||
|         env: | ||||
|           PR_SUBMITTER: ${{ steps.get-submitter.outputs.submitter }} | ||||
|         run: | | ||||
|           ADDED=$(git diff -U0 "origin/${PR_BASE}" HEAD -- CHANGELOG.md | grep -P '^\+[^\+].+$') | ||||
|           echo "Added lines in CHANGELOG.md:" | ||||
|           echo "$ADDED" | ||||
|           echo "Grepping for PR info (see CONTRIBUTING.md):" | ||||
|           grep "#${PR_NUMBER}\\b.*@${PR_SUBMITTER}\\b" <<< "$ADDED" | ||||
							
								
								
									
										58
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,3 +1,61 @@ | ||||
| # unreleased | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - Set terminal title to file names when Paging is not Paging::Never #2807 (@Oliver-Looney) | ||||
| - `bat --squeeze-blank`/`bat -s` will now squeeze consecutive empty lines, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815) | ||||
| - `bat --squeeze-limit` to set the maximum number of empty consecutive when using `--squeeze-blank`, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815) | ||||
| - `PrettyPrinter::squeeze_empty_lines` to support line squeezing for bat as a library, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815) | ||||
|  | ||||
| ## Bugfixes | ||||
|  | ||||
| - Fix long file name wrapping in header, see #2835 (@FilipRazek) | ||||
| - Fix `NO_COLOR` support, see #2767 (@acuteenvy) | ||||
| - Fix handling of inputs with OSC ANSI escape sequences, see #2541 and #2544 (@eth-p) | ||||
| - Fix handling of inputs with combined ANSI color and attribute sequences, see #2185 and #2856 (@eth-p) | ||||
| - Fix panel width when line 10000 wraps, see #2854 (@eth-p) | ||||
|  | ||||
| ## Other | ||||
|  | ||||
| - Upgrade to Rust 2021 edition #2748 (@cyqsimon) | ||||
| - Refactor and cleanup build script #2756 (@cyqsimon) | ||||
| - Checks changelog has been written to for PRs in CI #2766 (@cyqsimon) | ||||
|   - Use GitHub API to get correct PR submitter #2791 (@cyqsimon) | ||||
| - Minor benchmark script improvements #2768 (@cyqsimon) | ||||
| - Update Arch Linux package URL in README files #2779 (@brunobell) | ||||
| - Update and improve `zsh` completion, see #2772 (@okapia) | ||||
| - More extensible syntax mapping mechanism #2755 (@cyqsimon) | ||||
| - Use proper Architecture for Debian packages built for musl, see #2811 (@Enselic) | ||||
| - Pull in fix for unsafe-libyaml security advisory, see #2812 (@dtolnay) | ||||
| - Update git-version dependency to use Syn v2, see #2816 (@dtolnay) | ||||
| - Update git2 dependency to v0.18.2, see #2852 (@eth-p) | ||||
| - Improve performance when color output disabled, see #2397 and #2857 (@eth-p) | ||||
| - Relax syntax mapping rule restrictions to allow brace expansion #2865 (@cyqsimon) | ||||
| - Apply clippy fixes #2864 (@cyqsimon) | ||||
| - Faster startup by offloading glob matcher building to a worker thread #2868 (@cyqsimon) | ||||
|  | ||||
| ## Syntaxes | ||||
|  | ||||
| - `cmd-help`: scope subcommands followed by other terms, and other misc improvements, see #2819 (@victor-gp) | ||||
| - Upgrade JQ syntax, see #2820 (@dependabot[bot]) | ||||
| - Add syntax mapping for quadman quadlets #2866 (@cyqsimon) | ||||
| - Map containers .conf files to TOML syntax #2867 (@cyqsimon) | ||||
| - Associate `xsh` files with `xonsh` syntax that is Python, see #2840 (@anki-code). | ||||
| - Added auto detect syntax for `.jsonc` #2795 (@mxaddict) | ||||
| - Added auto detect syntax for `.aws/{config,credentials}` #2795 (@mxaddict) | ||||
| - Add syntax mapping for Wireguard config #2874 (@cyqsimon) | ||||
|  | ||||
| ## Themes | ||||
|  | ||||
| ## `bat` as a library | ||||
|  | ||||
| - Changes to `syntax_mapping::SyntaxMapping` #2755 (@cyqsimon) | ||||
|   - `SyntaxMapping::get_syntax_for` is now correctly public | ||||
|   - [BREAKING] `SyntaxMapping::{empty,builtin}` are removed; use `SyntaxMapping::new` instead | ||||
|   - [BREAKING] `SyntaxMapping::mappings` is replaced by `SyntaxMapping::{builtin,custom,all}_mappings` | ||||
| - Make `Controller::run_with_error_handler`'s error handler `FnMut`, see #2831 (@rhysd) | ||||
| - Improve compile time by 20%, see #2815 (@dtolnay) | ||||
|  | ||||
| # v0.24.0 | ||||
|  | ||||
| ## Features | ||||
|   | ||||
| @@ -6,21 +6,42 @@ Thank you for considering to contribute to `bat`! | ||||
|  | ||||
| ## Add an entry to the changelog | ||||
|  | ||||
| If your contribution changes the behavior of `bat` (as opposed to a typo-fix | ||||
| in the documentation), please update the [`CHANGELOG.md`](CHANGELOG.md) file | ||||
| and describe your changes. This makes the release process much easier and | ||||
| therefore helps to get your changes into a new `bat` release faster. | ||||
| Keeping the [`CHANGELOG.md`](CHANGELOG.md) file up-to-date makes the release | ||||
| process much easier and therefore helps to get your changes into a new `bat` | ||||
| release faster. However, not every change to the repository requires a | ||||
| changelog entry. Below are a few examples of that. | ||||
|  | ||||
| Please update the changelog if your contribution contains changes regarding | ||||
| any of the following: | ||||
|   - the behavior of `bat` | ||||
|   - syntax mappings | ||||
|   - syntax definitions | ||||
|   - themes | ||||
|   - the build system, linting, or CI workflows | ||||
|  | ||||
| A changelog entry is not necessary when: | ||||
|   - updating documentation | ||||
|   - fixing typos | ||||
|  | ||||
| >[!NOTE] | ||||
| > For PRs, a CI workflow verifies that a suitable changelog entry is | ||||
| > added. If such an entry is missing, the workflow will fail. If your | ||||
| > changes do not need an entry to the changelog (see above), that | ||||
| > workflow failure can be disregarded. | ||||
|  | ||||
|  | ||||
| ### Changelog entry format | ||||
|  | ||||
| The top of the `CHANGELOG` contains a *"unreleased"* section with a few | ||||
| subsections (Features, Bugfixes, …). Please add your entry to the subsection | ||||
| that best describes your change. | ||||
|  | ||||
| Entries follow this format: | ||||
| Entries must follow this format: | ||||
| ``` | ||||
| - Short description of what has been changed, see #123 (@user) | ||||
| ``` | ||||
| Here, `#123` is the number of the original issue and/or your pull request. | ||||
| Please replace `@user` by your GitHub username. | ||||
| Please replace `#123` with the number of your pull request (not issue) and | ||||
| `@user` by your GitHub username. | ||||
|  | ||||
|  | ||||
| ## Development | ||||
|   | ||||
							
								
								
									
										617
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										617
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										62
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										62
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -8,8 +8,8 @@ name = "bat" | ||||
| repository = "https://github.com/sharkdp/bat" | ||||
| version = "0.24.0" | ||||
| exclude = ["assets/syntaxes/*", "assets/themes/*"] | ||||
| build = "build.rs" | ||||
| edition = '2018' | ||||
| build = "build/main.rs" | ||||
| edition = '2021' | ||||
| rust-version = "1.70" | ||||
|  | ||||
| [features] | ||||
| @@ -41,32 +41,33 @@ regex-onig = ["syntect/regex-onig"] # Use the "oniguruma" regex engine | ||||
| regex-fancy = ["syntect/regex-fancy"] # Use the rust-only "fancy-regex" engine | ||||
|  | ||||
| [dependencies] | ||||
| nu-ansi-term = "0.49.0" | ||||
| nu-ansi-term = "0.50.0" | ||||
| ansi_colours = "^1.2" | ||||
| bincode = "1.0" | ||||
| console = "0.15.5" | ||||
| console = "0.15.7" | ||||
| flate2 = "1.0" | ||||
| once_cell = "1.18" | ||||
| once_cell = "1.19" | ||||
| thiserror = "1.0" | ||||
| wild = { version = "2.1", optional = true } | ||||
| wild = { version = "2.2", optional = true } | ||||
| content_inspector = "0.2.4" | ||||
| shell-words = { version = "1.1.0", optional = true } | ||||
| unicode-width = "0.1.10" | ||||
| unicode-width = "0.1.11" | ||||
| globset = "0.4" | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
| serde_yaml = "0.9" | ||||
| serde = "1.0" | ||||
| serde_derive = "1.0" | ||||
| serde_yaml = "0.9.28" | ||||
| semver = "1.0" | ||||
| path_abs = { version = "0.5", default-features = false } | ||||
| clircle = "0.4" | ||||
| clircle = "0.5" | ||||
| bugreport = { version = "0.5.0", optional = true } | ||||
| etcetera = { version = "0.8.0", optional = true } | ||||
| grep-cli = { version = "0.1.9", optional = true } | ||||
| regex = { version = "1.8.3", optional = true } | ||||
| walkdir = { version = "2.3", optional = true } | ||||
| grep-cli = { version = "0.1.10", optional = true } | ||||
| regex = { version = "1.10.2", optional = true } | ||||
| walkdir = { version = "2.4", optional = true } | ||||
| bytesize = { version = "1.3.0" } | ||||
| encoding_rs = "0.8.33" | ||||
| os_str_bytes = { version = "~6.4", optional = true } | ||||
| run_script = { version = "^0.10.0", optional = true} | ||||
| os_str_bytes = { version = "~6.6", optional = true } | ||||
| run_script = { version = "^0.10.1", optional = true} | ||||
|  | ||||
| [dependencies.git2] | ||||
| version = "0.18" | ||||
| @@ -74,32 +75,45 @@ optional = true | ||||
| default-features = false | ||||
|  | ||||
| [dependencies.syntect] | ||||
| version = "5.0.0" | ||||
| version = "5.2.0" | ||||
| default-features = false | ||||
| features = ["parsing"] | ||||
|  | ||||
| [dependencies.clap] | ||||
| version = "4.4.6" | ||||
| version = "4.4.12" | ||||
| optional = true | ||||
| features = ["wrap_help", "cargo"] | ||||
|  | ||||
| [target.'cfg(target_os = "macos")'.dependencies] | ||||
| home = "0.5.4" | ||||
| plist = "1.4.3" | ||||
| home = "0.5.9" | ||||
| plist = "1.6.0" | ||||
|  | ||||
| [dev-dependencies] | ||||
| assert_cmd = "2.0.10" | ||||
| assert_cmd = "2.0.12" | ||||
| expect-test = "1.4.1" | ||||
| serial_test = { version = "2.0.0", default-features = false } | ||||
| predicates = "3.0.3" | ||||
| predicates = "3.0.4" | ||||
| wait-timeout = "0.2.0" | ||||
| tempfile = "3.8.0" | ||||
| tempfile = "3.8.1" | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
|  | ||||
| [target.'cfg(unix)'.dev-dependencies] | ||||
| nix = { version = "0.26.2", default-features = false, features = ["term"] } | ||||
| nix = { version = "0.26.4", default-features = false, features = ["term"] } | ||||
|  | ||||
| [build-dependencies] | ||||
| anyhow = "1.0.78" | ||||
| indexmap = { version = "2.2.2", features = ["serde"] } | ||||
| itertools = "0.12.1" | ||||
| once_cell = "1.18" | ||||
| regex = "1.10.2" | ||||
| serde = "1.0" | ||||
| serde_derive = "1.0" | ||||
| serde_with = { version = "3.6.1", default-features = false, features = ["macros"] } | ||||
| toml = { version = "0.8.9", features = ["preserve_order"] } | ||||
| walkdir = "2.4" | ||||
|  | ||||
| [build-dependencies.clap] | ||||
| version = "4.4.6" | ||||
| version = "4.4.12" | ||||
| optional = true | ||||
| features = ["wrap_help", "cargo"] | ||||
|  | ||||
|   | ||||
							
								
								
									
										48
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								README.md
									
									
									
									
									
								
							| @@ -254,7 +254,7 @@ Please report any issues with the help syntax in [this repository](https://githu | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| [](https://repology.org/project/bat-cat/versions) | ||||
| [](https://repology.org/project/bat-cat/versions) | ||||
|  | ||||
| ### On Ubuntu (using `apt`) | ||||
| *... and other Debian-based Linux distributions.* | ||||
| @@ -296,7 +296,7 @@ apk add bat | ||||
|  | ||||
| ### On Arch Linux | ||||
|  | ||||
| You can install [the `bat` package](https://www.archlinux.org/packages/community/x86_64/bat/) | ||||
| You can install [the `bat` package](https://www.archlinux.org/packages/extra/x86_64/bat/) | ||||
| from the official sources: | ||||
|  | ||||
| ```bash | ||||
| @@ -602,7 +602,8 @@ set, `less` is used by default. If you want to use a different pager, you can ei | ||||
| `PAGER` variable or set the `BAT_PAGER` environment variable to override what is specified in | ||||
| `PAGER`. | ||||
|  | ||||
| **Note**: If `PAGER` is `more` or `most`, `bat` will silently use `less` instead to ensure support for colors. | ||||
| >[!NOTE] | ||||
| > If `PAGER` is `more` or `most`, `bat` will silently use `less` instead to ensure support for colors. | ||||
|  | ||||
| If you want to pass command-line arguments to the pager, you can also set them via the | ||||
| `PAGER`/`BAT_PAGER` variables: | ||||
| @@ -613,20 +614,37 @@ export BAT_PAGER="less -RF" | ||||
|  | ||||
| Instead of using environment variables, you can also use `bat`s [configuration file](https://github.com/sharkdp/bat#configuration-file) to configure the pager (`--pager` option). | ||||
|  | ||||
| **Note**: By default, if the pager is set to `less` (and no command-line options are specified), | ||||
| `bat` will pass the following command line options to the pager: `-R`/`--RAW-CONTROL-CHARS`, | ||||
| `-F`/`--quit-if-one-screen` and `-X`/`--no-init`. The last option (`-X`) is only used for `less` | ||||
| versions older than 530. | ||||
|  | ||||
| The `-R` option is needed to interpret ANSI colors correctly. The second option (`-F`) instructs | ||||
| less to exit immediately if the output size is smaller than the vertical size of the terminal. | ||||
| This is convenient for small files because you do not have to press `q` to quit the pager. The | ||||
| third option (`-X`) is needed to fix a bug with the `--quit-if-one-screen` feature in old versions | ||||
| of `less`. Unfortunately, it also breaks mouse-wheel support in `less`. | ||||
| ### Using `less` as a pager | ||||
|  | ||||
| If you want to enable mouse-wheel scrolling on older versions of `less`, you can pass just `-R` (as | ||||
| in the example above, this will disable the quit-if-one-screen feature). For less 530 or newer, | ||||
| it should work out of the box. | ||||
| When using `less` as a pager, `bat` will automatically pass extra options along to `less` | ||||
| to improve the experience. Specifically, `-R`/`--RAW-CONTROL-CHARS`, `-F`/`--quit-if-one-screen`, | ||||
| and under certain conditions, `-X`/`--no-init` and/or `-S`/`--chop-long-lines`. | ||||
|  | ||||
| >[!IMPORTANT] | ||||
| > These options will not be added if: | ||||
| > - The pager is not named `less`. | ||||
| > - The `--pager` argument contains any command-line arguments (e.g. `--pager="less -R"`). | ||||
| > - The `BAT_PAGER` environment variable contains any command-line arguments (e.g. `export BAT_PAGER="less -R"`) | ||||
| > | ||||
| > The `--quit-if-one-screen` option will not be added when: | ||||
| > - The `--paging=always` argument is used. | ||||
| > - The `BAT_PAGING` environment is set to `always`. | ||||
|  | ||||
| The `-R` option is needed to interpret ANSI colors correctly. | ||||
|  | ||||
| The `-F` option instructs `less` to exit immediately if the output size is smaller than | ||||
| the vertical size of the terminal. This is convenient for small files because you do not | ||||
| have to press `q` to quit the pager. | ||||
|  | ||||
| The `-X` option is needed to fix a bug with the `--quit-if-one-screen` feature in versions | ||||
| of `less` older than version 530. Unfortunately, it also breaks mouse-wheel support in `less`. | ||||
| If you want to enable mouse-wheel scrolling on older versions of `less` and do not mind losing | ||||
| the quit-if-one-screen feature, you can set the pager (via `--pager` or `BAT_PAGER`) to `less -R`. | ||||
| For `less` 530 or newer, it should work out of the box. | ||||
|  | ||||
| The `-S` option is added when `bat`'s `-S`/`--chop-long-lines` option is used. This tells `less` | ||||
| to truncate any lines larger than the terminal width. | ||||
|  | ||||
| ### Indentation | ||||
|  | ||||
|   | ||||
							
								
								
									
										8
									
								
								assets/completions/bat.fish.in
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								assets/completions/bat.fish.in
									
									
									
									
										vendored
									
									
								
							| @@ -147,6 +147,8 @@ complete -c $bat -s d -l diff -d "Only show lines with Git changes" -n __bat_no_ | ||||
|  | ||||
| complete -c $bat -l diff-context -x -d "Show N context lines around Git changes" -n "__fish_seen_argument -s d -l diff" | ||||
|  | ||||
| complete -c $bat -l generate-config-file -f -d "Generates a default configuration file" -n __fish_is_first_arg | ||||
|  | ||||
| complete -c $bat -l file-name -x -d "Specify the display name" -n __bat_no_excl_args | ||||
|  | ||||
| complete -c $bat -s f -l force-colorization -d "Force color and decorations" -n __bat_no_excl_args | ||||
| @@ -173,6 +175,12 @@ complete -c $bat -l list-themes -f -d "List syntax highlighting themes" -n __fis | ||||
|  | ||||
| complete -c $bat -s m -l map-syntax -x -a "(__bat_complete_map_syntax)" -d "Map <glob pattern>:<language syntax>" -n __bat_no_excl_args | ||||
|  | ||||
| complete -c $bat -l no-config -d "Do not use the configuration file" | ||||
|  | ||||
| complete -c $bat -l no-custom-assets -d "Do not load custom assets" | ||||
|  | ||||
| complete -c $bat -l no-lessopen -d "Disable the $LESSOPEN preprocessor if enabled (overrides --lessopen)" | ||||
|  | ||||
| complete -c $bat -s n -l number -d "Only show line numbers, no other decorations" -n __bat_no_excl_args | ||||
|  | ||||
| complete -c $bat -l pager -x -a "$pager_opts" -d "Which pager to use" -n __bat_no_excl_args | ||||
|   | ||||
							
								
								
									
										115
									
								
								assets/completions/bat.zsh.in
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										115
									
								
								assets/completions/bat.zsh.in
									
									
									
									
										vendored
									
									
								
							| @@ -1,19 +1,20 @@ | ||||
| #compdef {{PROJECT_EXECUTABLE}} | ||||
|  | ||||
| local context state state_descr line | ||||
| local curcontext="$curcontext" ret=1 | ||||
| local -a state state_descr line | ||||
| typeset -A opt_args | ||||
|  | ||||
| (( $+functions[_{{PROJECT_EXECUTABLE}}_cache_subcommand] )) || | ||||
| _{{PROJECT_EXECUTABLE}}_cache_subcommand() { | ||||
|     local -a args | ||||
|     args=( | ||||
|         '(-b --build -c --clear)'{-b,--build}'[Initialize or update the syntax/theme cache]' | ||||
|         '(-b --build -c --clear)'{-c,--clear}'[Remove the cached syntax definitions and themes]' | ||||
|         '(--source)'--source='[Use a different directory to load syntaxes and themes from]:directory:_files -/' | ||||
|         '(--target)'--target='[Use a different directory to store the cached syntax and theme set]:directory:_files -/' | ||||
|         '(--blank)'--blank'[Create completely new syntax and theme sets]' | ||||
|         '(: -)'{-h,--help}'[Prints help information]' | ||||
|         '*: :' | ||||
|         '(-b --build -c --clear)'{-b,--build}'[initialize or update the syntax/theme cache]' | ||||
|         '(-b --build -c --clear)'{-c,--clear}'[remove the cached syntax definitions and themes]' | ||||
|         --source='[specify directory to load syntaxes and themes from]:directory:_files -/' | ||||
|         --target='[specify directory to store the cached syntax and theme set in]:directory:_files -/' | ||||
|         --blank'[create completely new syntax and theme sets]' | ||||
|         --acknowledgements'[build acknowledgements.bin]' | ||||
|         '(: -)'{-h,--help}'[show help information]' | ||||
|     ) | ||||
|  | ||||
|     _arguments -S -s $args | ||||
| @@ -23,69 +24,79 @@ _{{PROJECT_EXECUTABLE}}_cache_subcommand() { | ||||
| _{{PROJECT_EXECUTABLE}}_main() { | ||||
|     local -a args | ||||
|     args=( | ||||
|         '(-A --show-all)'{-A,--show-all}'[Show non-printable characters (space, tab, newline, ..)]' | ||||
|         '*'{-p,--plain}'[Show plain style (alias for `--style=plain`), repeat twice to disable disable automatic paging (alias for `--paging=never`)]' | ||||
|         '(-l --language)'{-l+,--language=}'[Set the language for syntax highlighting]:<language>:->language' | ||||
|         '(-H --highlight-line)'{-H,--highlight-line}'[Highlight lines N through M]:<N\:M>...' | ||||
|         '(--file-name)'--file-name'[Specify the name to display for a file]:<name>...:_files' | ||||
|         '(-d --diff)'--diff'[Only show lines that have been added/removed/modified]' | ||||
|         '(--diff-context)'--diff-context'[Include N lines of context around added/removed/modified lines when using `--diff`]:<N> (lines):()' | ||||
|         '(--tabs)'--tabs'[Set the tab width to T spaces]:<T> (tab width):()' | ||||
|         '(--wrap)'--wrap='[Specify the text-wrapping mode]:<when>:(auto never character)' | ||||
|         '(--terminal-width)'--terminal-width'[Explicitly set the width of the terminal instead of determining it automatically]:<width>' | ||||
|         '(-n --number)'{-n,--number}'[Show line numbers]' | ||||
|         '(--color)'--color='[When to use colors]:<when>:(auto never always)' | ||||
|         '(--italic-text)'--italic-text='[Use italics in output]:<when>:(always never)' | ||||
|         '(--decorations)'--decorations='[When to show the decorations]:<when>:(auto never always)' | ||||
|         '(--paging)'--paging='[Specify when to use the pager]:<when>:(auto never always)' | ||||
|         '(-m --map-syntax)'{-m+,--map-syntax=}'[Use the specified syntax for files matching the glob pattern]:<glob\:syntax>...' | ||||
|         '(--theme)'--theme='[Set the color theme for syntax highlighting]:<theme>:->theme' | ||||
|         '(: --list-themes --list-languages -L)'--list-themes'[Display all supported highlighting themes]' | ||||
|         '(--style)'--style='[Comma-separated list of style elements to display]:<components>:->style' | ||||
|         '(-r --line-range)'{-r+,--line-range=}'[Only print the lines from N to M]:<N\:M>...' | ||||
|         '(: --list-themes --list-languages -L)'{-L,--list-languages}'[Display all supported languages]' | ||||
|         '(: --no-config)'--no-config'[Do not use the configuration file]' | ||||
|         '(: --no-custom-assets)'--no-custom-assets'[Do not load custom assets]' | ||||
|         '(: --lessopen)'--lessopen'[Enable the $LESSOPEN preprocessor]' | ||||
|         '(: --no-lessopen)'--no-lessopen'[Disable the $LESSOPEN preprocessor if enabled (overrides --lessopen)]' | ||||
|         '(: --config-dir)'--config-dir'[Show bat'"'"'s configuration directory]' | ||||
|         '(: --config-file)'--config-file'[Show path to the configuration file]' | ||||
|         '(: --generate-config-file)'--generate-config-file'[Generates a default configuration file]' | ||||
|         '(: --cache-dir)'--cache-dir'[Show bat'"'"'s cache directory]' | ||||
|         '(: -)'{-h,--help}'[Print this help message]' | ||||
|         '(: -)'{-V,--version}'[Show version information]' | ||||
|         '*: :_files' | ||||
|         '(-A --show-all)'{-A,--show-all}'[show non-printable characters (space, tab, newline, ..)]' | ||||
|         --nonprintable-notation='[specify how to display non-printable characters when using --show-all]:notation:(caret unicode)' | ||||
|         \*{-p,--plain}'[show plain style (alias for `--style=plain`), repeat twice to disable disable automatic paging (alias for `--paging=never`)]' | ||||
|         '(-l --language)'{-l+,--language=}'[set the language for syntax highlighting]:language:->languages' | ||||
|         \*{-H+,--highlight-line=}'[highlight specified block of lines]:start\:end' | ||||
|         \*--file-name='[specify the name to display for a file]:name:_files' | ||||
|         '(-d --diff)'--diff'[only show lines that have been added/removed/modified]' | ||||
|         --diff-context='[specify lines of context around added/removed/modified lines when using `--diff`]:lines' | ||||
|         --tabs='[set the tab width]:tab width [4]' | ||||
|         --wrap='[specify the text-wrapping mode]:mode [auto]:(auto never character)' | ||||
|         '!(--wrap)'{-S,--chop-long-lines} | ||||
|         --terminal-width='[explicitly set the width of the terminal instead of determining it automatically]:width' | ||||
|         '(-n --number --diff --diff-context)'{-n,--number}'[show line numbers]' | ||||
|         --color='[specify when to use colors]:when:(auto never always)' | ||||
|         --italic-text='[use italics in output]:when:(always never)' | ||||
|         --decorations='[specify when to show the decorations]:when:(auto never always)' | ||||
|         --paging='[specify when to use the pager]:when:(auto never always)' | ||||
|         '(-m --map-syntax)'{-m+,--map-syntax=}'[map a glob pattern to an existing syntax name]: :->syntax-maps' | ||||
|         '(--theme)'--theme='[set the color theme for syntax highlighting]:theme:->themes' | ||||
|         '(: --list-themes --list-languages -L)'--list-themes'[show all supported highlighting themes]' | ||||
|         --style='[comma-separated list of style elements to display]: : _values "style [default]" | ||||
|             default auto full plain changes header header-filename header-filesize grid rule numbers snip' | ||||
|         \*{-r+,--line-range=}'[only print the specified line range]:start\:end' | ||||
|         '(* -)'{-L,--list-languages}'[display all supported languages]' | ||||
|         "--no-config[don't use the configuration file]" | ||||
|         "--no-custom-assets[don't load custom assets]" | ||||
|         '(--no-lessopen)'--lessopen'[enable the $LESSOPEN preprocessor]' | ||||
|         '(--lessopen)'--no-lessopen'[disable the $LESSOPEN preprocessor if enabled (overrides --lessopen)]' | ||||
|         '(* -)'--config-dir"[show bat's configuration directory]" | ||||
|         '(* -)'--config-file'[show path to the configuration file]' | ||||
|         '(* -)'--generate-config-file'[generate a default configuration file]' | ||||
|         '(* -)'--cache-dir"[show bat's cache directory]" | ||||
|         '(* -)'{-h,--help}'[show help information]' | ||||
|         '(* -)'{-V,--version}'[show version information]' | ||||
|         '*: :{ _files || compadd cache }' | ||||
|     ) | ||||
|  | ||||
|     _arguments -S -s $args | ||||
|     _arguments -S -s $args && ret=0 | ||||
|  | ||||
|     case "$state" in | ||||
|         language) | ||||
|         syntax-maps) | ||||
|           if ! compset -P '*:'; then | ||||
|             _message -e patterns 'glob pattern:language' | ||||
|             return | ||||
|           fi | ||||
|         ;& # fall-through | ||||
|  | ||||
|         languages) | ||||
|             local IFS=$'\n' | ||||
|             local -a languages | ||||
|             languages=( $({{PROJECT_EXECUTABLE}} --list-languages | awk -F':|,' '{ for (i = 1; i <= NF; ++i) printf("%s:%s\n", $i, $1) }') ) | ||||
|  | ||||
|             _describe 'language' languages | ||||
|             _describe 'language' languages && ret=0 | ||||
|         ;; | ||||
|  | ||||
|         theme) | ||||
|             local IFS=$'\n' | ||||
|             local -a themes | ||||
|             themes=( $({{PROJECT_EXECUTABLE}} --list-themes | sort) ) | ||||
|         themes) | ||||
|             local -a themes expl | ||||
|             themes=( ${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} ) | ||||
|  | ||||
|             _values 'theme' $themes | ||||
|         ;; | ||||
|  | ||||
|         style) | ||||
|             _values -s , 'style' default auto full plain changes header header-filename header-filesize grid rule numbers snip | ||||
|             _wanted themes expl 'theme' compadd -a themes && ret=0 | ||||
|         ;; | ||||
|     esac | ||||
|  | ||||
|     return ret | ||||
| } | ||||
|  | ||||
| case $words[2] in | ||||
|     cache) | ||||
|         ## Completion of the 'cache' command itself is removed for better UX | ||||
|         ## See https://github.com/sharkdp/bat/issues/2085#issuecomment-1271646802 | ||||
|         shift words | ||||
|         (( CURRENT-- )) | ||||
|         curcontext="${curcontext%:*}-${words[1]}:" | ||||
|         _{{PROJECT_EXECUTABLE}}_cache_subcommand | ||||
|     ;; | ||||
|  | ||||
|   | ||||
							
								
								
									
										2
									
								
								assets/syntaxes/02_Extra/SublimeJQ
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
									
										2
									
								
								assets/syntaxes/02_Extra/SublimeJQ
									
									
									
									
										vendored
									
									
								
							 Submodule assets/syntaxes/02_Extra/SublimeJQ updated: 687058289c...b7e53e5d86
									
								
							
							
								
								
									
										2
									
								
								assets/syntaxes/02_Extra/cmd-help
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
									
										2
									
								
								assets/syntaxes/02_Extra/cmd-help
									
									
									
									
										vendored
									
									
								
							 Submodule assets/syntaxes/02_Extra/cmd-help updated: f41e5fc838...209559b72f
									
								
							
							
								
								
									
										2
									
								
								assets/themes/zenburn
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
									
										2
									
								
								assets/themes/zenburn
									
									
									
									
										vendored
									
									
								
							 Submodule assets/themes/zenburn updated: 43dc527731...86d4ee7a1f
									
								
							
							
								
								
									
										108
									
								
								build.rs
									
									
									
									
									
								
							
							
						
						
									
										108
									
								
								build.rs
									
									
									
									
									
								
							| @@ -1,108 +0,0 @@ | ||||
| // TODO: Re-enable generation of shell completion files (below) when clap 3 is out. | ||||
| // For more details, see https://github.com/sharkdp/bat/issues/372 | ||||
|  | ||||
| // For bat-as-a-library, no build script is required. The build script is for | ||||
| // the manpage and completions, which are only relevant to the bat application. | ||||
| #[cfg(not(feature = "application"))] | ||||
| fn main() {} | ||||
|  | ||||
| #[cfg(feature = "application")] | ||||
| fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     use std::collections::HashMap; | ||||
|     use std::error::Error; | ||||
|     use std::fs; | ||||
|     use std::path::Path; | ||||
|  | ||||
|     // Read environment variables. | ||||
|     let project_name = option_env!("PROJECT_NAME").unwrap_or("bat"); | ||||
|     let executable_name = option_env!("PROJECT_EXECUTABLE").unwrap_or(project_name); | ||||
|     let executable_name_uppercase = executable_name.to_uppercase(); | ||||
|     static PROJECT_VERSION: &str = env!("CARGO_PKG_VERSION"); | ||||
|  | ||||
|     /// Generates a file from a template. | ||||
|     fn template( | ||||
|         variables: &HashMap<&str, &str>, | ||||
|         in_file: &str, | ||||
|         out_file: impl AsRef<Path>, | ||||
|     ) -> Result<(), Box<dyn Error>> { | ||||
|         let mut content = fs::read_to_string(in_file)?; | ||||
|  | ||||
|         for (variable_name, value) in variables { | ||||
|             // Replace {{variable_name}} by the value | ||||
|             let pattern = format!("{{{{{variable_name}}}}}", variable_name = variable_name); | ||||
|             content = content.replace(&pattern, value); | ||||
|         } | ||||
|  | ||||
|         fs::write(out_file, content)?; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     let mut variables = HashMap::new(); | ||||
|     variables.insert("PROJECT_NAME", project_name); | ||||
|     variables.insert("PROJECT_EXECUTABLE", executable_name); | ||||
|     variables.insert("PROJECT_EXECUTABLE_UPPERCASE", &executable_name_uppercase); | ||||
|     variables.insert("PROJECT_VERSION", PROJECT_VERSION); | ||||
|  | ||||
|     let out_dir_env = std::env::var_os("BAT_ASSETS_GEN_DIR") | ||||
|         .or_else(|| std::env::var_os("OUT_DIR")) | ||||
|         .expect("BAT_ASSETS_GEN_DIR or OUT_DIR to be set in build.rs"); | ||||
|     let out_dir = Path::new(&out_dir_env); | ||||
|  | ||||
|     fs::create_dir_all(out_dir.join("assets/manual")).unwrap(); | ||||
|     fs::create_dir_all(out_dir.join("assets/completions")).unwrap(); | ||||
|  | ||||
|     template( | ||||
|         &variables, | ||||
|         "assets/manual/bat.1.in", | ||||
|         out_dir.join("assets/manual/bat.1"), | ||||
|     )?; | ||||
|     template( | ||||
|         &variables, | ||||
|         "assets/completions/bat.bash.in", | ||||
|         out_dir.join("assets/completions/bat.bash"), | ||||
|     )?; | ||||
|     template( | ||||
|         &variables, | ||||
|         "assets/completions/bat.fish.in", | ||||
|         out_dir.join("assets/completions/bat.fish"), | ||||
|     )?; | ||||
|     template( | ||||
|         &variables, | ||||
|         "assets/completions/_bat.ps1.in", | ||||
|         out_dir.join("assets/completions/_bat.ps1"), | ||||
|     )?; | ||||
|     template( | ||||
|         &variables, | ||||
|         "assets/completions/bat.zsh.in", | ||||
|         out_dir.join("assets/completions/bat.zsh"), | ||||
|     )?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| // #[macro_use] | ||||
| // extern crate clap; | ||||
|  | ||||
| // use clap::Shell; | ||||
| // use std::fs; | ||||
|  | ||||
| // include!("src/clap_app.rs"); | ||||
|  | ||||
| // const BIN_NAME: &str = "bat"; | ||||
|  | ||||
| // fn main() { | ||||
| //     let outdir = std::env::var_os("SHELL_COMPLETIONS_DIR").or(std::env::var_os("OUT_DIR")); | ||||
|  | ||||
| //     let outdir = match outdir { | ||||
| //         None => return, | ||||
| //         Some(outdir) => outdir, | ||||
| //     }; | ||||
|  | ||||
| //     fs::create_dir_all(&outdir).unwrap(); | ||||
|  | ||||
| //     let mut app = build_app(true); | ||||
| //     app.gen_completions(BIN_NAME, Shell::Bash, &outdir); | ||||
| //     app.gen_completions(BIN_NAME, Shell::Fish, &outdir); | ||||
| //     app.gen_completions(BIN_NAME, Shell::Zsh, &outdir); | ||||
| //     app.gen_completions(BIN_NAME, Shell::PowerShell, &outdir); | ||||
| // } | ||||
							
								
								
									
										67
									
								
								build/application.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								build/application.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| use std::{env, fs, path::PathBuf}; | ||||
|  | ||||
| use crate::util::render_template; | ||||
|  | ||||
| /// Generate manpage and shell completions for the bat application. | ||||
| pub fn gen_man_and_comp() -> anyhow::Result<()> { | ||||
|     println!("cargo:rerun-if-changed=assets/manual/"); | ||||
|     println!("cargo:rerun-if-changed=assets/completions/"); | ||||
|  | ||||
|     println!("cargo:rerun-if-env-changed=PROJECT_NAME"); | ||||
|     println!("cargo:rerun-if-env-changed=PROJECT_EXECUTABLE"); | ||||
|     println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION"); | ||||
|     println!("cargo:rerun-if-env-changed=BAT_ASSETS_GEN_DIR"); | ||||
|  | ||||
|     // Read environment variables. | ||||
|     let project_name = env::var("PROJECT_NAME").unwrap_or("bat".into()); | ||||
|     let executable_name = env::var("PROJECT_EXECUTABLE").unwrap_or(project_name.clone()); | ||||
|     let executable_name_uppercase = executable_name.to_uppercase(); | ||||
|     let project_version = env::var("CARGO_PKG_VERSION")?; | ||||
|  | ||||
|     let variables = [ | ||||
|         ("PROJECT_NAME", project_name), | ||||
|         ("PROJECT_EXECUTABLE", executable_name), | ||||
|         ("PROJECT_EXECUTABLE_UPPERCASE", executable_name_uppercase), | ||||
|         ("PROJECT_VERSION", project_version), | ||||
|     ] | ||||
|     .into_iter() | ||||
|     .collect(); | ||||
|  | ||||
|     let Some(out_dir) = env::var_os("BAT_ASSETS_GEN_DIR") | ||||
|         .or_else(|| env::var_os("OUT_DIR")) | ||||
|         .map(PathBuf::from) | ||||
|     else { | ||||
|         anyhow::bail!("BAT_ASSETS_GEN_DIR or OUT_DIR should be set for build.rs"); | ||||
|     }; | ||||
|  | ||||
|     fs::create_dir_all(out_dir.join("assets/manual")).unwrap(); | ||||
|     fs::create_dir_all(out_dir.join("assets/completions")).unwrap(); | ||||
|  | ||||
|     render_template( | ||||
|         &variables, | ||||
|         "assets/manual/bat.1.in", | ||||
|         out_dir.join("assets/manual/bat.1"), | ||||
|     )?; | ||||
|     render_template( | ||||
|         &variables, | ||||
|         "assets/completions/bat.bash.in", | ||||
|         out_dir.join("assets/completions/bat.bash"), | ||||
|     )?; | ||||
|     render_template( | ||||
|         &variables, | ||||
|         "assets/completions/bat.fish.in", | ||||
|         out_dir.join("assets/completions/bat.fish"), | ||||
|     )?; | ||||
|     render_template( | ||||
|         &variables, | ||||
|         "assets/completions/_bat.ps1.in", | ||||
|         out_dir.join("assets/completions/_bat.ps1"), | ||||
|     )?; | ||||
|     render_template( | ||||
|         &variables, | ||||
|         "assets/completions/bat.zsh.in", | ||||
|         out_dir.join("assets/completions/bat.zsh"), | ||||
|     )?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										17
									
								
								build/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								build/main.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| #[cfg(feature = "application")] | ||||
| mod application; | ||||
| mod syntax_mapping; | ||||
| mod util; | ||||
|  | ||||
| fn main() -> anyhow::Result<()> { | ||||
|     // only watch manually-designated files | ||||
|     // see: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-changed | ||||
|     println!("cargo:rerun-if-changed=build/"); | ||||
|  | ||||
|     syntax_mapping::build_static_mappings()?; | ||||
|  | ||||
|     #[cfg(feature = "application")] | ||||
|     application::gen_man_and_comp()?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										294
									
								
								build/syntax_mapping.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								build/syntax_mapping.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,294 @@ | ||||
| use std::{ | ||||
|     convert::Infallible, | ||||
|     env, fs, | ||||
|     path::{Path, PathBuf}, | ||||
|     str::FromStr, | ||||
| }; | ||||
|  | ||||
| use anyhow::{anyhow, bail}; | ||||
| use indexmap::IndexMap; | ||||
| use itertools::Itertools; | ||||
| use once_cell::sync::Lazy; | ||||
| use regex::Regex; | ||||
| use serde_derive::Deserialize; | ||||
| use serde_with::DeserializeFromStr; | ||||
| use walkdir::WalkDir; | ||||
|  | ||||
| /// Known mapping targets. | ||||
| /// | ||||
| /// Corresponds to `syntax_mapping::MappingTarget`. | ||||
| #[allow(clippy::enum_variant_names)] | ||||
| #[derive(Clone, Debug, Eq, PartialEq, Hash, DeserializeFromStr)] | ||||
| pub enum MappingTarget { | ||||
|     MapTo(String), | ||||
|     MapToUnknown, | ||||
|     MapExtensionToUnknown, | ||||
| } | ||||
| impl FromStr for MappingTarget { | ||||
|     type Err = Infallible; | ||||
|     fn from_str(s: &str) -> Result<Self, Self::Err> { | ||||
|         match s { | ||||
|             "MappingTarget::MapToUnknown" => Ok(Self::MapToUnknown), | ||||
|             "MappingTarget::MapExtensionToUnknown" => Ok(Self::MapExtensionToUnknown), | ||||
|             syntax => Ok(Self::MapTo(syntax.into())), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| impl MappingTarget { | ||||
|     fn codegen(&self) -> String { | ||||
|         match self { | ||||
|             Self::MapTo(syntax) => format!(r###"MappingTarget::MapTo(r#"{syntax}"#)"###), | ||||
|             Self::MapToUnknown => "MappingTarget::MapToUnknown".into(), | ||||
|             Self::MapExtensionToUnknown => "MappingTarget::MapExtensionToUnknown".into(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr)] | ||||
| /// A single matcher. | ||||
| /// | ||||
| /// Codegen converts this into a `Lazy<Option<GlobMatcher>>`. | ||||
| struct Matcher(Vec<MatcherSegment>); | ||||
| /// Parse a matcher. | ||||
| /// | ||||
| /// Note that this implementation is rather strict: it will greedily interpret | ||||
| /// every valid environment variable replacement as such, then immediately | ||||
| /// hard-error if it finds a '$' anywhere in the remaining text segments. | ||||
| /// | ||||
| /// The reason for this strictness is I currently cannot think of a valid reason | ||||
| /// why you would ever need '$' as plaintext in a glob pattern. Therefore any | ||||
| /// such occurrences are likely human errors. | ||||
| /// | ||||
| /// If we later discover some edge cases, it's okay to make it more permissive. | ||||
| /// | ||||
| /// Revision history: | ||||
| /// - 2024-02-20: allow `{` and `}` (glob brace expansion) | ||||
| impl FromStr for Matcher { | ||||
|     type Err = anyhow::Error; | ||||
|     fn from_str(s: &str) -> Result<Self, Self::Err> { | ||||
|         use MatcherSegment as Seg; | ||||
|         static VAR_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\$\{([\w\d_]+)\}").unwrap()); | ||||
|  | ||||
|         let mut segments = vec![]; | ||||
|         let mut text_start = 0; | ||||
|         for capture in VAR_REGEX.captures_iter(s) { | ||||
|             let match_0 = capture.get(0).unwrap(); | ||||
|  | ||||
|             // text before this var | ||||
|             let text_end = match_0.start(); | ||||
|             segments.push(Seg::Text(s[text_start..text_end].into())); | ||||
|             text_start = match_0.end(); | ||||
|  | ||||
|             // this var | ||||
|             segments.push(Seg::Env(capture.get(1).unwrap().as_str().into())); | ||||
|         } | ||||
|         // possible trailing text | ||||
|         segments.push(Seg::Text(s[text_start..].into())); | ||||
|  | ||||
|         // cleanup empty text segments | ||||
|         let non_empty_segments = segments | ||||
|             .into_iter() | ||||
|             .filter(|seg| seg.text().map(|t| !t.is_empty()).unwrap_or(true)) | ||||
|             .collect_vec(); | ||||
|  | ||||
|         // sanity check | ||||
|         if non_empty_segments | ||||
|             .windows(2) | ||||
|             .any(|segs| segs[0].is_text() && segs[1].is_text()) | ||||
|         { | ||||
|             unreachable!("Parsed into consecutive text segments: {non_empty_segments:?}"); | ||||
|         } | ||||
|  | ||||
|         // guard empty case | ||||
|         if non_empty_segments.is_empty() { | ||||
|             bail!(r#"Parsed an empty matcher: "{s}""#); | ||||
|         } | ||||
|  | ||||
|         // guard variable syntax leftover fragments | ||||
|         if non_empty_segments | ||||
|             .iter() | ||||
|             .filter_map(Seg::text) | ||||
|             .any(|t| t.contains('$')) | ||||
|         { | ||||
|             bail!(r#"Invalid matcher: "{s}""#); | ||||
|         } | ||||
|  | ||||
|         Ok(Self(non_empty_segments)) | ||||
|     } | ||||
| } | ||||
| impl Matcher { | ||||
|     fn codegen(&self) -> String { | ||||
|         match self.0.len() { | ||||
|             0 => unreachable!("0-length matcher should never be created"), | ||||
|             // if-let guard would be ideal here | ||||
|             // see: https://github.com/rust-lang/rust/issues/51114 | ||||
|             1 if self.0[0].is_text() => { | ||||
|                 let s = self.0[0].text().unwrap(); | ||||
|                 format!(r###"Lazy::new(|| Some(build_matcher_fixed(r#"{s}"#)))"###) | ||||
|             } | ||||
|             // parser logic ensures that this case can only happen when there are dynamic segments | ||||
|             _ => { | ||||
|                 let segs = self.0.iter().map(MatcherSegment::codegen).join(", "); | ||||
|                 format!(r###"Lazy::new(|| build_matcher_dynamic(&[{segs}]))"###) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// A segment in a matcher. | ||||
| /// | ||||
| /// Corresponds to `syntax_mapping::MatcherSegment`. | ||||
| #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||
| enum MatcherSegment { | ||||
|     Text(String), | ||||
|     Env(String), | ||||
| } | ||||
| #[allow(dead_code)] | ||||
| impl MatcherSegment { | ||||
|     fn is_text(&self) -> bool { | ||||
|         matches!(self, Self::Text(_)) | ||||
|     } | ||||
|     fn is_env(&self) -> bool { | ||||
|         matches!(self, Self::Env(_)) | ||||
|     } | ||||
|     fn text(&self) -> Option<&str> { | ||||
|         match self { | ||||
|             Self::Text(t) => Some(t), | ||||
|             Self::Env(_) => None, | ||||
|         } | ||||
|     } | ||||
|     fn env(&self) -> Option<&str> { | ||||
|         match self { | ||||
|             Self::Text(_) => None, | ||||
|             Self::Env(t) => Some(t), | ||||
|         } | ||||
|     } | ||||
|     fn codegen(&self) -> String { | ||||
|         match self { | ||||
|             Self::Text(s) => format!(r###"MatcherSegment::Text(r#"{s}"#)"###), | ||||
|             Self::Env(s) => format!(r###"MatcherSegment::Env(r#"{s}"#)"###), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// A struct that models a single .toml file in /src/syntax_mapping/builtins/. | ||||
| #[derive(Clone, Debug, Deserialize)] | ||||
| struct MappingDefModel { | ||||
|     mappings: IndexMap<MappingTarget, Vec<Matcher>>, | ||||
| } | ||||
| impl MappingDefModel { | ||||
|     fn into_mapping_list(self) -> MappingList { | ||||
|         let list = self | ||||
|             .mappings | ||||
|             .into_iter() | ||||
|             .flat_map(|(target, matchers)| { | ||||
|                 matchers | ||||
|                     .into_iter() | ||||
|                     .map(|matcher| (matcher, target.clone())) | ||||
|                     .collect::<Vec<_>>() | ||||
|             }) | ||||
|             .collect(); | ||||
|         MappingList(list) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Debug)] | ||||
| struct MappingList(Vec<(Matcher, MappingTarget)>); | ||||
| impl MappingList { | ||||
|     fn codegen(&self) -> String { | ||||
|         let array_items: Vec<_> = self | ||||
|             .0 | ||||
|             .iter() | ||||
|             .map(|(matcher, target)| { | ||||
|                 format!("({m}, {t})", m = matcher.codegen(), t = target.codegen()) | ||||
|             }) | ||||
|             .collect(); | ||||
|         let len = array_items.len(); | ||||
|  | ||||
|         format!( | ||||
|             "/// Generated by build script from /src/syntax_mapping/builtins/.\n\ | ||||
|             pub(crate) static BUILTIN_MAPPINGS: [(Lazy<Option<GlobMatcher>>, MappingTarget); {len}] = [\n{items}\n];", | ||||
|             items = array_items.join(",\n") | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Get the list of paths to all mapping definition files that should be | ||||
| /// included for the current target platform. | ||||
| fn get_def_paths() -> anyhow::Result<Vec<PathBuf>> { | ||||
|     let source_subdirs = [ | ||||
|         "common", | ||||
|         #[cfg(target_family = "unix")] | ||||
|         "unix-family", | ||||
|         #[cfg(any( | ||||
|             target_os = "freebsd", | ||||
|             target_os = "netbsd", | ||||
|             target_os = "openbsd", | ||||
|             target_os = "macos" | ||||
|         ))] | ||||
|         "bsd-family", | ||||
|         #[cfg(target_os = "linux")] | ||||
|         "linux", | ||||
|         #[cfg(target_os = "macos")] | ||||
|         "macos", | ||||
|         #[cfg(target_os = "windows")] | ||||
|         "windows", | ||||
|     ]; | ||||
|  | ||||
|     let mut toml_paths = vec![]; | ||||
|     for subdir in source_subdirs { | ||||
|         let wd = WalkDir::new(Path::new("src/syntax_mapping/builtins").join(subdir)); | ||||
|         let paths = wd | ||||
|             .into_iter() | ||||
|             .filter_map_ok(|entry| { | ||||
|                 let path = entry.path(); | ||||
|                 (path.is_file() && path.extension().map(|ext| ext == "toml").unwrap_or(false)) | ||||
|                     .then(|| path.to_owned()) | ||||
|             }) | ||||
|             .collect::<Result<Vec<_>, _>>()?; | ||||
|         toml_paths.extend(paths); | ||||
|     } | ||||
|  | ||||
|     toml_paths.sort_by_key(|path| { | ||||
|         path.file_name() | ||||
|             .expect("file name should not terminate in ..") | ||||
|             .to_owned() | ||||
|     }); | ||||
|  | ||||
|     Ok(toml_paths) | ||||
| } | ||||
|  | ||||
| fn read_all_mappings() -> anyhow::Result<MappingList> { | ||||
|     let mut all_mappings = vec![]; | ||||
|  | ||||
|     for path in get_def_paths()? { | ||||
|         let toml_string = fs::read_to_string(path)?; | ||||
|         let mappings = toml::from_str::<MappingDefModel>(&toml_string)?.into_mapping_list(); | ||||
|         all_mappings.extend(mappings.0); | ||||
|     } | ||||
|  | ||||
|     let duplicates = all_mappings | ||||
|         .iter() | ||||
|         .duplicates_by(|(matcher, _)| matcher) | ||||
|         .collect_vec(); | ||||
|     if !duplicates.is_empty() { | ||||
|         bail!("Rules with duplicate matchers found: {duplicates:?}"); | ||||
|     } | ||||
|  | ||||
|     Ok(MappingList(all_mappings)) | ||||
| } | ||||
|  | ||||
| /// Build the static syntax mappings defined in /src/syntax_mapping/builtins/ | ||||
| /// into a .rs source file, which is to be inserted with `include!`. | ||||
| pub fn build_static_mappings() -> anyhow::Result<()> { | ||||
|     println!("cargo:rerun-if-changed=src/syntax_mapping/builtins/"); | ||||
|  | ||||
|     let mappings = read_all_mappings()?; | ||||
|  | ||||
|     let codegen_path = Path::new(&env::var_os("OUT_DIR").ok_or(anyhow!("OUT_DIR is unset"))?) | ||||
|         .join("codegen_static_syntax_mappings.rs"); | ||||
|  | ||||
|     fs::write(codegen_path, mappings.codegen())?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										21
									
								
								build/util.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								build/util.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| #![allow(dead_code)] | ||||
|  | ||||
| use std::{collections::HashMap, fs, path::Path}; | ||||
|  | ||||
| /// Generates a file from a template. | ||||
| pub fn render_template( | ||||
|     variables: &HashMap<&str, String>, | ||||
|     in_file: &str, | ||||
|     out_file: impl AsRef<Path>, | ||||
| ) -> anyhow::Result<()> { | ||||
|     let mut content = fs::read_to_string(in_file)?; | ||||
|  | ||||
|     for (variable_name, value) in variables { | ||||
|         // Replace {{variable_name}} by the value | ||||
|         let pattern = format!("{{{{{variable_name}}}}}"); | ||||
|         content = content.replace(&pattern, value); | ||||
|     } | ||||
|  | ||||
|     fs::write(out_file, content)?; | ||||
|     Ok(()) | ||||
| } | ||||
| @@ -181,7 +181,7 @@ man 2 select | ||||
|  | ||||
| ## インストール | ||||
|  | ||||
| [](https://repology.org/project/bat-cat/versions) | ||||
| [](https://repology.org/project/bat-cat/versions) | ||||
|  | ||||
| ###  On Ubuntu (`apt` を使用) | ||||
| *... や他のDebianベースのLinuxディストリビューション* | ||||
| @@ -219,7 +219,7 @@ apk add bat | ||||
|  | ||||
| ###  On Arch Linux | ||||
|  | ||||
| [Arch Linuxの公式リソース](https://www.archlinux.org/packages/community/x86_64/bat/) | ||||
| [Arch Linuxの公式リソース](https://www.archlinux.org/packages/extra/x86_64/bat/) | ||||
| からインストールできます。 | ||||
|  | ||||
| ```bash | ||||
|   | ||||
| @@ -214,7 +214,7 @@ man 2 select | ||||
|  | ||||
| ## 설치 | ||||
|  | ||||
| [](https://repology.org/project/bat-cat/versions) | ||||
| [](https://repology.org/project/bat-cat/versions) | ||||
|  | ||||
| ### Ubuntu에서 (`apt` 사용) | ||||
| *... 그리고 다른 Debian 기반의 Linux 배포판들에서.* | ||||
| @@ -264,7 +264,7 @@ apk add bat | ||||
| ### Arch Linux에서 | ||||
|  | ||||
| 공식 소스를 통해 | ||||
| [`bat` 패키지](https://www.archlinux.org/packages/community/x86_64/bat/)를 | ||||
| [`bat` 패키지](https://www.archlinux.org/packages/extra/x86_64/bat/)를 | ||||
| 설치할 수 있습니다: | ||||
|  | ||||
| ```bash | ||||
|   | ||||
| @@ -12,7 +12,7 @@ | ||||
|   <a href="#установка">Установка</a> • | ||||
|   <a href="#кастомизация">Кастомизация</a> • | ||||
|   <a href="#цели-и-альтернативы">Цели и альтернативы </a><br> | ||||
|   [<a href="../README.md">English] | ||||
|   [<a href="../README.md">English</a>] | ||||
|   [<a href="README-zh.md">中文</a>] | ||||
|   [<a href="README-ja.md">日本語</a>] | ||||
|   [<a href="README-ko.md">한국어</a>] | ||||
| @@ -160,7 +160,7 @@ man 2 select | ||||
|  | ||||
| ## Установка | ||||
|  | ||||
| [](https://repology.org/project/bat-cat/versions) | ||||
| [](https://repology.org/project/bat-cat/versions) | ||||
|  | ||||
| ### Ubuntu (с помощью `apt`) | ||||
| *... и другие дистрибутивы основанные на Debian.* | ||||
| @@ -201,7 +201,7 @@ apk add bat | ||||
|  | ||||
| ### Arch Linux | ||||
|  | ||||
| Вы можете установить [`bat`](https://www.archlinux.org/packages/community/x86_64/bat/) из официального источника: | ||||
| Вы можете установить [`bat`](https://www.archlinux.org/packages/extra/x86_64/bat/) из официального источника: | ||||
|  | ||||
| ```bash | ||||
| pacman -S bat | ||||
|   | ||||
| @@ -191,7 +191,7 @@ man 2 select | ||||
|  | ||||
| ## 安装 | ||||
|  | ||||
| [](https://repology.org/project/bat-cat/versions) | ||||
| [](https://repology.org/project/bat-cat/versions) | ||||
|  | ||||
| ### Ubuntu (使用 `apt`) | ||||
|  | ||||
| @@ -232,7 +232,7 @@ apk add bat | ||||
|  | ||||
| ### Arch Linux | ||||
|  | ||||
| 你可以用下面下列命令从官方源中安装[`bat`包](https://www.archlinux.org/packages/community/x86_64/bat/): | ||||
| 你可以用下面下列命令从官方源中安装[`bat`包](https://www.archlinux.org/packages/extra/x86_64/bat/): | ||||
|  | ||||
| ```bash | ||||
| pacman -S bat | ||||
|   | ||||
| @@ -116,6 +116,12 @@ Options: | ||||
|       --list-themes | ||||
|           Display a list of supported themes for syntax highlighting. | ||||
|  | ||||
|   -s, --squeeze-blank | ||||
|           Squeeze consecutive empty lines into a single empty line. | ||||
|  | ||||
|       --squeeze-limit <squeeze-limit> | ||||
|           Set the maximum number of consecutive empty lines to be printed. | ||||
|  | ||||
|       --style <components> | ||||
|           Configure which elements (line numbers, file headers, grid borders, Git modifications, ..) | ||||
|           to display in addition to the file contents. The argument is a comma-separated list of | ||||
| @@ -123,6 +129,9 @@ Options: | ||||
|           set a default style, add the '--style=".."' option to the configuration file or export the | ||||
|           BAT_STYLE environment variable (e.g.: export BAT_STYLE=".."). | ||||
|            | ||||
|           By default, the following components are enabled: | ||||
|             changes, grid, header-filename, numbers, snip | ||||
|            | ||||
|           Possible values: | ||||
|            | ||||
|             * default: enables recommended style components (default). | ||||
| @@ -160,6 +169,9 @@ Options: | ||||
|       --acknowledgements | ||||
|           Show acknowledgements. | ||||
|  | ||||
|       --set-terminal-title | ||||
|           Sets terminal title to filenames when using a pager. | ||||
|  | ||||
|   -h, --help | ||||
|           Print help (see a summary with '-h') | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,16 @@ | ||||
| - [ ] Update the version and the min. supported Rust version in `README.md` and | ||||
|       `doc/README-*.md`. Check with | ||||
|       `git grep -i -e 'rust.*1\.' -e '1\..*rust' | grep README | grep -v tests/`. | ||||
| - [ ] Update `CHANGELOG.md`. Introduce a section for the new release. | ||||
|  | ||||
| ## CHANGELOG.md updates | ||||
|  | ||||
| - [ ] Go to https://github.com/sharkdp/bat/releases/new, click "Choose a tag", | ||||
|   type the name of the tag that will be created later, click "Generate release | ||||
|   notes". DO NOT ACTUALLY CREATE ANY RELEASE IN THIS STEP. | ||||
| - [ ] Compare current `CHANGELOG.md` with auto-generated release notes and add | ||||
|   missing entries. Expect in particular dependabot PRs to not be in | ||||
|   `CHANGELOG.md` since they are [auto-merged] if CI passes. | ||||
| - [ ] Introduce a section for the new release and perform final touch-ups. | ||||
|  | ||||
| ## Update syntaxes and themes (build assets) | ||||
|  | ||||
| @@ -71,3 +80,5 @@ | ||||
|  | ||||
|  | ||||
| ``` | ||||
|  | ||||
| [auto-merged]: https://github.com/sharkdp/bat/blob/master/.github/workflows/Auto-merge-dependabot-PRs.yml | ||||
|   | ||||
| @@ -43,6 +43,8 @@ Options: | ||||
|           Set the color theme for syntax highlighting. | ||||
|       --list-themes | ||||
|           Display all supported highlighting themes. | ||||
|   -s, --squeeze-blank | ||||
|           Squeeze consecutive empty lines. | ||||
|       --style <components> | ||||
|           Comma-separated list of style elements to display (*default*, auto, full, plain, changes, | ||||
|           header, header-filename, header-filesize, grid, rule, numbers, snip). | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 817 KiB After Width: | Height: | Size: 79 KiB | 
| @@ -13,6 +13,6 @@ fn main() { | ||||
|  | ||||
|     println!("Themes:"); | ||||
|     for theme in printer.themes() { | ||||
|         println!("- {}", theme); | ||||
|         println!("- {theme}"); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -380,7 +380,7 @@ fn asset_from_contents<T: serde::de::DeserializeOwned>( | ||||
|     } else { | ||||
|         bincode::deserialize_from(contents) | ||||
|     } | ||||
|     .map_err(|_| format!("Could not parse {}", description).into()) | ||||
|     .map_err(|_| format!("Could not parse {description}").into()) | ||||
| } | ||||
|  | ||||
| fn asset_from_cache<T: serde::de::DeserializeOwned>( | ||||
| @@ -396,7 +396,7 @@ fn asset_from_cache<T: serde::de::DeserializeOwned>( | ||||
|         ) | ||||
|     })?; | ||||
|     asset_from_contents(&contents[..], description, compressed) | ||||
|         .map_err(|_| format!("Could not parse cached {}", description).into()) | ||||
|         .map_err(|_| format!("Could not parse cached {description}").into()) | ||||
| } | ||||
|  | ||||
| #[cfg(target_os = "macos")] | ||||
| @@ -441,7 +441,7 @@ mod tests { | ||||
|         fn new() -> Self { | ||||
|             SyntaxDetectionTest { | ||||
|                 assets: HighlightingAssets::from_binary(), | ||||
|                 syntax_mapping: SyntaxMapping::builtin(), | ||||
|                 syntax_mapping: SyntaxMapping::new(), | ||||
|                 temp_dir: TempDir::new().expect("creation of temporary directory"), | ||||
|             } | ||||
|         } | ||||
| @@ -466,7 +466,7 @@ mod tests { | ||||
|             let file_path = self.temp_dir.path().join(file_name); | ||||
|             { | ||||
|                 let mut temp_file = File::create(&file_path).unwrap(); | ||||
|                 writeln!(temp_file, "{}", first_line).unwrap(); | ||||
|                 writeln!(temp_file, "{first_line}").unwrap(); | ||||
|             } | ||||
|  | ||||
|             let input = Input::ordinary_file(&file_path); | ||||
| @@ -514,8 +514,7 @@ mod tests { | ||||
|  | ||||
|             if !consistent { | ||||
|                 eprintln!( | ||||
|                     "Inconsistent syntax detection:\nFor File: {}\nFor Reader: {}", | ||||
|                     as_file, as_reader | ||||
|                     "Inconsistent syntax detection:\nFor File: {as_file}\nFor Reader: {as_reader}" | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ use std::path::Path; | ||||
| use std::time::SystemTime; | ||||
|  | ||||
| use semver::Version; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serde_derive::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::error::*; | ||||
|  | ||||
|   | ||||
| @@ -93,7 +93,7 @@ fn print_unlinked_contexts(syntax_set: &SyntaxSet) { | ||||
|     if !missing_contexts.is_empty() { | ||||
|         println!("Some referenced contexts could not be found!"); | ||||
|         for context in missing_contexts { | ||||
|             println!("- {}", context); | ||||
|             println!("- {context}"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -152,7 +152,7 @@ pub(crate) fn asset_to_contents<T: serde::Serialize>( | ||||
|     } else { | ||||
|         bincode::serialize_into(&mut contents, asset) | ||||
|     } | ||||
|     .map_err(|_| format!("Could not serialize {}", description))?; | ||||
|     .map_err(|_| format!("Could not serialize {description}"))?; | ||||
|     Ok(contents) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -80,7 +80,7 @@ fn handle_license(path: &Path) -> Result<Option<String>> { | ||||
|     } else if license_not_needed_in_acknowledgements(&license_text) { | ||||
|         Ok(None) | ||||
|     } else { | ||||
|         Err(format!("ERROR: License is of unknown type: {:?}", path).into()) | ||||
|         Err(format!("ERROR: License is of unknown type: {path:?}").into()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -125,7 +125,7 @@ fn append_to_acknowledgements( | ||||
|     relative_path: &str, | ||||
|     license_text: &str, | ||||
| ) { | ||||
|     write!(acknowledgements, "## {}\n\n{}", relative_path, license_text).ok(); | ||||
|     write!(acknowledgements, "## {relative_path}\n\n{license_text}").ok(); | ||||
|  | ||||
|     // Make sure the last char is a newline to not mess up formatting later | ||||
|     if acknowledgements | ||||
|   | ||||
| @@ -3,8 +3,7 @@ use super::*; | ||||
| use std::collections::BTreeMap; | ||||
| use std::convert::TryFrom; | ||||
|  | ||||
| use serde::Deserialize; | ||||
| use serde::Serialize; | ||||
| use serde_derive::{Deserialize, Serialize}; | ||||
|  | ||||
| use once_cell::unsync::OnceCell; | ||||
|  | ||||
| @@ -89,7 +88,7 @@ impl TryFrom<ThemeSet> for LazyThemeSet { | ||||
|             let lazy_theme = LazyTheme { | ||||
|                 serialized: crate::assets::build_assets::asset_to_contents( | ||||
|                     &theme, | ||||
|                     &format!("theme {}", name), | ||||
|                     &format!("theme {name}"), | ||||
|                     COMPRESS_LAZY_THEMES, | ||||
|                 )?, | ||||
|                 deserialized: OnceCell::new(), | ||||
|   | ||||
| @@ -29,6 +29,10 @@ fn is_truecolor_terminal() -> bool { | ||||
|         .unwrap_or(false) | ||||
| } | ||||
|  | ||||
| pub fn env_no_color() -> bool { | ||||
|     env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty()) | ||||
| } | ||||
|  | ||||
| pub struct App { | ||||
|     pub matches: ArgMatches, | ||||
|     interactive_output: bool, | ||||
| @@ -117,7 +121,11 @@ impl App { | ||||
|             _ => unreachable!("other values for --paging are not allowed"), | ||||
|         }; | ||||
|  | ||||
|         let mut syntax_mapping = SyntaxMapping::builtin(); | ||||
|         let mut syntax_mapping = SyntaxMapping::new(); | ||||
|         // start building glob matchers for builtin mappings immediately | ||||
|         // this is an appropriate approach because it's statistically likely that | ||||
|         // all the custom mappings need to be checked | ||||
|         syntax_mapping.start_offload_build_all(); | ||||
|  | ||||
|         if let Some(values) = self.matches.get_many::<String>("ignored-suffix") { | ||||
|             for suffix in values { | ||||
| @@ -126,7 +134,9 @@ impl App { | ||||
|         } | ||||
|  | ||||
|         if let Some(values) = self.matches.get_many::<String>("map-syntax") { | ||||
|             for from_to in values { | ||||
|             // later args take precedence over earlier ones, hence `.rev()` | ||||
|             // see: https://github.com/sharkdp/bat/pull/2755#discussion_r1456416875 | ||||
|             for from_to in values.rev() { | ||||
|                 let parts: Vec<_> = from_to.split(':').collect(); | ||||
|  | ||||
|                 if parts.len() != 2 { | ||||
| @@ -207,7 +217,7 @@ impl App { | ||||
|                 || match self.matches.get_one::<String>("color").map(|s| s.as_str()) { | ||||
|                     Some("always") => true, | ||||
|                     Some("never") => false, | ||||
|                     Some("auto") => env::var_os("NO_COLOR").is_none() && self.interactive_output, | ||||
|                     Some("auto") => !env_no_color() && self.interactive_output, | ||||
|                     _ => unreachable!("other values for --color are not allowed"), | ||||
|                 }, | ||||
|             paging_mode, | ||||
| @@ -283,6 +293,17 @@ impl App { | ||||
|             use_custom_assets: !self.matches.get_flag("no-custom-assets"), | ||||
|             #[cfg(feature = "lessopen")] | ||||
|             use_lessopen: self.matches.get_flag("lessopen"), | ||||
|             set_terminal_title: self.matches.get_flag("set-terminal-title"), | ||||
|             squeeze_lines: if self.matches.get_flag("squeeze-blank") { | ||||
|                 Some( | ||||
|                     self.matches | ||||
|                         .get_one::<usize>("squeeze-limit") | ||||
|                         .map(|limit| limit.to_owned()) | ||||
|                         .unwrap_or(1), | ||||
|                 ) | ||||
|             } else { | ||||
|                 None | ||||
|             }, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -44,7 +44,7 @@ pub fn assets_from_cache_or_binary( | ||||
| } | ||||
|  | ||||
| fn clear_asset(path: PathBuf, description: &str) { | ||||
|     print!("Clearing {} ... ", description); | ||||
|     print!("Clearing {description} ... "); | ||||
|     match fs::remove_file(&path) { | ||||
|         Err(err) if err.kind() == io::ErrorKind::NotFound => { | ||||
|             println!("skipped (not present)"); | ||||
|   | ||||
| @@ -19,7 +19,7 @@ static VERSION: Lazy<String> = Lazy::new(|| { | ||||
| }); | ||||
|  | ||||
| pub fn build_app(interactive_output: bool) -> Command { | ||||
|     let color_when = if interactive_output && env::var_os("NO_COLOR").is_none() { | ||||
|     let color_when = if interactive_output && !crate::app::env_no_color() { | ||||
|         ColorChoice::Auto | ||||
|     } else { | ||||
|         ColorChoice::Never | ||||
| @@ -387,6 +387,21 @@ pub fn build_app(interactive_output: bool) -> Command { | ||||
|                 .help("Display all supported highlighting themes.") | ||||
|                 .long_help("Display a list of supported themes for syntax highlighting."), | ||||
|         ) | ||||
|         .arg( | ||||
|             Arg::new("squeeze-blank") | ||||
|                 .long("squeeze-blank") | ||||
|                 .short('s') | ||||
|                 .action(ArgAction::SetTrue) | ||||
|                 .help("Squeeze consecutive empty lines.") | ||||
|                 .long_help("Squeeze consecutive empty lines into a single empty line.") | ||||
|         ) | ||||
|         .arg( | ||||
|             Arg::new("squeeze-limit") | ||||
|                 .long("squeeze-limit") | ||||
|                 .value_parser(|s: &str| s.parse::<usize>().map_err(|_| "Requires a non-negative number".to_owned())) | ||||
|                 .long_help("Set the maximum number of consecutive empty lines to be printed.") | ||||
|                 .hide_short_help(true) | ||||
|         ) | ||||
|         .arg( | ||||
|             Arg::new("style") | ||||
|                 .long("style") | ||||
| @@ -415,7 +430,7 @@ pub fn build_app(interactive_output: bool) -> Command { | ||||
|                     }); | ||||
|  | ||||
|                     if let Some(invalid) = invalid_vals.next() { | ||||
|                         Err(format!("Unknown style, '{}'", invalid)) | ||||
|                         Err(format!("Unknown style, '{invalid}'")) | ||||
|                     } else { | ||||
|                         Ok(val.to_owned()) | ||||
|                     } | ||||
| @@ -432,6 +447,8 @@ pub fn build_app(interactive_output: bool) -> Command { | ||||
|                      pre-defined style ('full'). To set a default style, add the \ | ||||
|                      '--style=\"..\"' option to the configuration file or export the \ | ||||
|                      BAT_STYLE environment variable (e.g.: export BAT_STYLE=\"..\").\n\n\ | ||||
|                      By default, the following components are enabled:\n  \ | ||||
|                         changes, grid, header-filename, numbers, snip\n\n\ | ||||
|                      Possible values:\n\n  \ | ||||
|                      * default: enables recommended style components (default).\n  \ | ||||
|                      * full: enables all available components.\n  \ | ||||
| @@ -567,6 +584,13 @@ pub fn build_app(interactive_output: bool) -> Command { | ||||
|                 .action(ArgAction::SetTrue) | ||||
|                 .hide_short_help(true) | ||||
|                 .help("Show acknowledgements."), | ||||
|         ) | ||||
|         .arg( | ||||
|             Arg::new("set-terminal-title") | ||||
|                 .long("set-terminal-title") | ||||
|                 .action(ArgAction::SetTrue) | ||||
|                 .hide_short_help(true) | ||||
|                 .help("Sets terminal title to filenames when using a pager."), | ||||
|         ); | ||||
|  | ||||
|     // Check if the current directory contains a file name cache. Otherwise, | ||||
|   | ||||
| @@ -78,9 +78,11 @@ fn run_cache_subcommand( | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| fn get_syntax_mapping_to_paths<'a>( | ||||
|     mappings: &[(GlobMatcher, MappingTarget<'a>)], | ||||
| ) -> HashMap<&'a str, Vec<String>> { | ||||
| fn get_syntax_mapping_to_paths<'r, 't, I>(mappings: I) -> HashMap<&'t str, Vec<String>> | ||||
| where | ||||
|     I: IntoIterator<Item = (&'r GlobMatcher, &'r MappingTarget<'t>)>, | ||||
|     't: 'r, // target text outlives rule | ||||
| { | ||||
|     let mut map = HashMap::new(); | ||||
|     for mapping in mappings { | ||||
|         if let (matcher, MappingTarget::MapTo(s)) = mapping { | ||||
| @@ -123,7 +125,7 @@ pub fn get_languages(config: &Config, cache_dir: &Path) -> Result<String> { | ||||
|  | ||||
|     languages.sort_by_key(|lang| lang.name.to_uppercase()); | ||||
|  | ||||
|     let configured_languages = get_syntax_mapping_to_paths(config.syntax_mapping.mappings()); | ||||
|     let configured_languages = get_syntax_mapping_to_paths(config.syntax_mapping.all_mappings()); | ||||
|  | ||||
|     for lang in &mut languages { | ||||
|         if let Some(additional_paths) = configured_languages.get(lang.name.as_str()) { | ||||
| @@ -220,16 +222,37 @@ pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result< | ||||
|         )?; | ||||
|     } else { | ||||
|         for theme in assets.themes() { | ||||
|             writeln!(stdout, "{}", theme)?; | ||||
|             writeln!(stdout, "{theme}")?; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| fn set_terminal_title_to(new_terminal_title: String) { | ||||
|     let osc_command_for_setting_terminal_title = "\x1b]0;"; | ||||
|     let osc_end_command = "\x07"; | ||||
|     print!("{osc_command_for_setting_terminal_title}{new_terminal_title}{osc_end_command}"); | ||||
|     io::stdout().flush().unwrap(); | ||||
| } | ||||
|  | ||||
| fn get_new_terminal_title(inputs: &Vec<Input>) -> String { | ||||
|     let mut new_terminal_title = "bat: ".to_string(); | ||||
|     for (index, input) in inputs.iter().enumerate() { | ||||
|         new_terminal_title += input.description().title(); | ||||
|         if index < inputs.len() - 1 { | ||||
|             new_terminal_title += ", "; | ||||
|         } | ||||
|     } | ||||
|     new_terminal_title | ||||
| } | ||||
|  | ||||
| fn run_controller(inputs: Vec<Input>, config: &Config, cache_dir: &Path) -> Result<bool> { | ||||
|     let assets = assets_from_cache_or_binary(config.use_custom_assets, cache_dir)?; | ||||
|     let controller = Controller::new(config, &assets); | ||||
|     if config.paging_mode != PagingMode::Never && config.set_terminal_title { | ||||
|         set_terminal_title_to(get_new_terminal_title(&inputs)); | ||||
|     } | ||||
|     controller.run(inputs, None) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -94,6 +94,12 @@ pub struct Config<'a> { | ||||
|     // Whether or not to use $LESSOPEN if set | ||||
|     #[cfg(feature = "lessopen")] | ||||
|     pub use_lessopen: bool, | ||||
|  | ||||
|     // Weather or not to set terminal title when using a pager | ||||
|     pub set_terminal_title: bool, | ||||
|  | ||||
|     /// The maximum number of consecutive empty lines to display | ||||
|     pub squeeze_lines: Option<usize>, | ||||
| } | ||||
|  | ||||
| #[cfg(all(feature = "minimal-application", feature = "paging"))] | ||||
|   | ||||
| @@ -47,7 +47,7 @@ impl<'b> Controller<'b> { | ||||
|         &self, | ||||
|         inputs: Vec<Input>, | ||||
|         output_buffer: Option<&mut dyn std::fmt::Write>, | ||||
|         handle_error: impl Fn(&Error, &mut dyn Write), | ||||
|         mut handle_error: impl FnMut(&Error, &mut dyn Write), | ||||
|     ) -> Result<bool> { | ||||
|         let mut output_type; | ||||
|  | ||||
|   | ||||
| @@ -46,7 +46,7 @@ impl Decoration for LineNumberDecoration { | ||||
|         _printer: &InteractivePrinter, | ||||
|     ) -> DecorationText { | ||||
|         if continuation { | ||||
|             if line_number > self.cached_wrap_invalid_at { | ||||
|             if line_number >= self.cached_wrap_invalid_at { | ||||
|                 let new_width = self.cached_wrap.width + 1; | ||||
|                 return DecorationText { | ||||
|                     text: self.color.paint(" ".repeat(new_width)).to_string(), | ||||
| @@ -56,7 +56,7 @@ impl Decoration for LineNumberDecoration { | ||||
|  | ||||
|             self.cached_wrap.clone() | ||||
|         } else { | ||||
|             let plain: String = format!("{:4}", line_number); | ||||
|             let plain: String = format!("{line_number:4}"); | ||||
|             DecorationText { | ||||
|                 width: plain.len(), | ||||
|                 text: self.color.paint(plain).to_string(), | ||||
|   | ||||
| @@ -197,7 +197,7 @@ impl<'a> Input<'a> { | ||||
|             InputKind::StdIn => { | ||||
|                 if let Some(stdout) = stdout_identifier { | ||||
|                     let input_identifier = Identifier::try_from(clircle::Stdio::Stdin) | ||||
|                         .map_err(|e| format!("Stdin: Error identifying file: {}", e))?; | ||||
|                         .map_err(|e| format!("Stdin: Error identifying file: {e}"))?; | ||||
|                     if stdout.surely_conflicts_with(&input_identifier) { | ||||
|                         return Err("IO circle detected. The input from stdin is also an output. Aborting to avoid infinite loop.".into()); | ||||
|                     } | ||||
|   | ||||
| @@ -91,31 +91,27 @@ pub fn replace_nonprintable( | ||||
|                     }); | ||||
|                     line_idx = 0; | ||||
|                 } | ||||
|                 // carriage return | ||||
|                 '\x0D' => output.push_str(match nonprintable_notation { | ||||
|                     NonprintableNotation::Caret => "^M", | ||||
|                     NonprintableNotation::Unicode => "␍", | ||||
|                 }), | ||||
|                 // null | ||||
|                 '\x00' => output.push_str(match nonprintable_notation { | ||||
|                     NonprintableNotation::Caret => "^@", | ||||
|                     NonprintableNotation::Unicode => "␀", | ||||
|                 }), | ||||
|                 // bell | ||||
|                 '\x07' => output.push_str(match nonprintable_notation { | ||||
|                     NonprintableNotation::Caret => "^G", | ||||
|                     NonprintableNotation::Unicode => "␇", | ||||
|                 }), | ||||
|                 // backspace | ||||
|                 '\x08' => output.push_str(match nonprintable_notation { | ||||
|                     NonprintableNotation::Caret => "^H", | ||||
|                     NonprintableNotation::Unicode => "␈", | ||||
|                 }), | ||||
|                 // escape | ||||
|                 '\x1B' => output.push_str(match nonprintable_notation { | ||||
|                     NonprintableNotation::Caret => "^[", | ||||
|                     NonprintableNotation::Unicode => "␛", | ||||
|                 }), | ||||
|                 // ASCII control characters | ||||
|                 '\x00'..='\x1F' => { | ||||
|                     let c = u32::from(chr); | ||||
|  | ||||
|                     match nonprintable_notation { | ||||
|                         NonprintableNotation::Caret => { | ||||
|                             let caret_character = char::from_u32(0x40 + c).unwrap(); | ||||
|                             write!(output, "^{caret_character}").ok(); | ||||
|                         } | ||||
|  | ||||
|                         NonprintableNotation::Unicode => { | ||||
|                             let replacement_symbol = char::from_u32(0x2400 + c).unwrap(); | ||||
|                             output.push(replacement_symbol) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 // delete | ||||
|                 '\x7F' => match nonprintable_notation { | ||||
|                     NonprintableNotation::Caret => output.push_str("^?"), | ||||
|                     NonprintableNotation::Unicode => output.push('\u{2421}'), | ||||
|                 }, | ||||
|                 // printable ASCII | ||||
|                 c if c.is_ascii_alphanumeric() | ||||
|                     || c.is_ascii_punctuation() | ||||
|   | ||||
| @@ -230,6 +230,12 @@ impl<'a> PrettyPrinter<'a> { | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Specify the maximum number of consecutive empty lines to print. | ||||
|     pub fn squeeze_empty_lines(&mut self, maximum: Option<usize>) -> &mut Self { | ||||
|         self.config.squeeze_lines = maximum; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Specify the highlighting theme | ||||
|     pub fn theme(&mut self, theme: impl AsRef<str>) -> &mut Self { | ||||
|         self.config.theme = theme.as_ref().to_owned(); | ||||
|   | ||||
							
								
								
									
										252
									
								
								src/printer.rs
									
									
									
									
									
								
							
							
						
						
									
										252
									
								
								src/printer.rs
									
									
									
									
									
								
							| @@ -7,10 +7,9 @@ use nu_ansi_term::Style; | ||||
|  | ||||
| use bytesize::ByteSize; | ||||
|  | ||||
| use console::AnsiCodeIterator; | ||||
|  | ||||
| use syntect::easy::HighlightLines; | ||||
| use syntect::highlighting::Color; | ||||
| use syntect::highlighting::FontStyle; | ||||
| use syntect::highlighting::Theme; | ||||
| use syntect::parsing::SyntaxSet; | ||||
|  | ||||
| @@ -33,9 +32,39 @@ use crate::line_range::RangeCheckResult; | ||||
| use crate::preprocessor::{expand_tabs, replace_nonprintable}; | ||||
| use crate::style::StyleComponent; | ||||
| use crate::terminal::{as_terminal_escaped, to_ansi_color}; | ||||
| use crate::vscreen::AnsiStyle; | ||||
| use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator}; | ||||
| use crate::wrapping::WrappingMode; | ||||
|  | ||||
| const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI { | ||||
|     raw_sequence: "\x1B[4m", | ||||
|     parameters: "4", | ||||
|     intermediates: "", | ||||
|     final_byte: "m", | ||||
| }; | ||||
|  | ||||
| const ANSI_UNDERLINE_DISABLE: EscapeSequence = EscapeSequence::CSI { | ||||
|     raw_sequence: "\x1B[24m", | ||||
|     parameters: "24", | ||||
|     intermediates: "", | ||||
|     final_byte: "m", | ||||
| }; | ||||
|  | ||||
| const EMPTY_SYNTECT_STYLE: syntect::highlighting::Style = syntect::highlighting::Style { | ||||
|     foreground: Color { | ||||
|         r: 127, | ||||
|         g: 127, | ||||
|         b: 127, | ||||
|         a: 255, | ||||
|     }, | ||||
|     background: Color { | ||||
|         r: 127, | ||||
|         g: 127, | ||||
|         b: 127, | ||||
|         a: 255, | ||||
|     }, | ||||
|     font_style: FontStyle::empty(), | ||||
| }; | ||||
|  | ||||
| pub enum OutputHandle<'a> { | ||||
|     IoWrite(&'a mut dyn io::Write), | ||||
|     FmtWrite(&'a mut dyn fmt::Write), | ||||
| @@ -72,11 +101,15 @@ pub(crate) trait Printer { | ||||
|  | ||||
| pub struct SimplePrinter<'a> { | ||||
|     config: &'a Config<'a>, | ||||
|     consecutive_empty_lines: usize, | ||||
| } | ||||
|  | ||||
| impl<'a> SimplePrinter<'a> { | ||||
|     pub fn new(config: &'a Config) -> Self { | ||||
|         SimplePrinter { config } | ||||
|         SimplePrinter { | ||||
|             config, | ||||
|             consecutive_empty_lines: 0, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -105,6 +138,21 @@ impl<'a> Printer for SimplePrinter<'a> { | ||||
|         _line_number: usize, | ||||
|         line_buffer: &[u8], | ||||
|     ) -> Result<()> { | ||||
|         // Skip squeezed lines. | ||||
|         if let Some(squeeze_limit) = self.config.squeeze_lines { | ||||
|             if String::from_utf8_lossy(line_buffer) | ||||
|                 .trim_end_matches(|c| c == '\r' || c == '\n') | ||||
|                 .is_empty() | ||||
|             { | ||||
|                 self.consecutive_empty_lines += 1; | ||||
|                 if self.consecutive_empty_lines > squeeze_limit { | ||||
|                     return Ok(()); | ||||
|                 } | ||||
|             } else { | ||||
|                 self.consecutive_empty_lines = 0; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if !out_of_range { | ||||
|             if self.config.show_nonprintable { | ||||
|                 let line = replace_nonprintable( | ||||
| @@ -112,7 +160,7 @@ impl<'a> Printer for SimplePrinter<'a> { | ||||
|                     self.config.tab_width, | ||||
|                     self.config.nonprintable_notation, | ||||
|                 ); | ||||
|                 write!(handle, "{}", line)?; | ||||
|                 write!(handle, "{line}")?; | ||||
|             } else { | ||||
|                 match handle { | ||||
|                     OutputHandle::IoWrite(handle) => handle.write_all(line_buffer)?, | ||||
| @@ -158,6 +206,7 @@ pub(crate) struct InteractivePrinter<'a> { | ||||
|     pub line_changes: &'a Option<LineChanges>, | ||||
|     highlighter_from_set: Option<HighlighterFromSet<'a>>, | ||||
|     background_color_highlight: Option<Color>, | ||||
|     consecutive_empty_lines: usize, | ||||
| } | ||||
|  | ||||
| impl<'a> InteractivePrinter<'a> { | ||||
| @@ -210,11 +259,13 @@ impl<'a> InteractivePrinter<'a> { | ||||
|             panel_width = 0; | ||||
|         } | ||||
|  | ||||
|         let highlighter_from_set = if input | ||||
|         // Get the highlighter for the output. | ||||
|         let is_printing_binary = input | ||||
|             .reader | ||||
|             .content_type | ||||
|             .map_or(false, |c| c.is_binary() && !config.show_nonprintable) | ||||
|         { | ||||
|             .map_or(false, |c| c.is_binary() && !config.show_nonprintable); | ||||
|  | ||||
|         let highlighter_from_set = if is_printing_binary || !config.colored_output { | ||||
|             None | ||||
|         } else { | ||||
|             // Determine the type of syntax for highlighting | ||||
| @@ -241,6 +292,7 @@ impl<'a> InteractivePrinter<'a> { | ||||
|             line_changes, | ||||
|             highlighter_from_set, | ||||
|             background_color_highlight, | ||||
|             consecutive_empty_lines: 0, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
| @@ -281,12 +333,20 @@ impl<'a> InteractivePrinter<'a> { | ||||
|             " ".repeat(self.panel_width - 1 - text_truncated.len()) | ||||
|         ); | ||||
|         if self.config.style_components.grid() { | ||||
|             format!("{} │ ", text_filled) | ||||
|             format!("{text_filled} │ ") | ||||
|         } else { | ||||
|             text_filled | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn get_header_component_indent_length(&self) -> usize { | ||||
|         if self.config.style_components.grid() && self.panel_width > 0 { | ||||
|             self.panel_width + 2 | ||||
|         } else { | ||||
|             self.panel_width | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn print_header_component_indent(&mut self, handle: &mut OutputHandle) -> Result<()> { | ||||
|         if self.config.style_components.grid() { | ||||
|             write!( | ||||
| @@ -302,6 +362,55 @@ impl<'a> InteractivePrinter<'a> { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn print_header_component_with_indent( | ||||
|         &mut self, | ||||
|         handle: &mut OutputHandle, | ||||
|         content: &str, | ||||
|     ) -> Result<()> { | ||||
|         self.print_header_component_indent(handle)?; | ||||
|         writeln!(handle, "{content}") | ||||
|     } | ||||
|  | ||||
|     fn print_header_multiline_component( | ||||
|         &mut self, | ||||
|         handle: &mut OutputHandle, | ||||
|         content: &str, | ||||
|     ) -> Result<()> { | ||||
|         let mut content = content; | ||||
|         let content_width = self.config.term_width - self.get_header_component_indent_length(); | ||||
|         while content.len() > content_width { | ||||
|             let (content_line, remaining) = content.split_at(content_width); | ||||
|             self.print_header_component_with_indent(handle, content_line)?; | ||||
|             content = remaining; | ||||
|         } | ||||
|         self.print_header_component_with_indent(handle, content) | ||||
|     } | ||||
|  | ||||
|     fn highlight_regions_for_line<'b>( | ||||
|         &mut self, | ||||
|         line: &'b str, | ||||
|     ) -> Result<Vec<(syntect::highlighting::Style, &'b str)>> { | ||||
|         let highlighter_from_set = match self.highlighter_from_set { | ||||
|             Some(ref mut highlighter_from_set) => highlighter_from_set, | ||||
|             _ => return Ok(vec![(EMPTY_SYNTECT_STYLE, line)]), | ||||
|         }; | ||||
|  | ||||
|         // skip syntax highlighting on long lines | ||||
|         let too_long = line.len() > 1024 * 16; | ||||
|  | ||||
|         let for_highlighting: &str = if too_long { "\n" } else { line }; | ||||
|  | ||||
|         let mut highlighted_line = highlighter_from_set | ||||
|             .highlighter | ||||
|             .highlight_line(for_highlighting, highlighter_from_set.syntax_set)?; | ||||
|  | ||||
|         if too_long { | ||||
|             highlighted_line[0].1 = &line; | ||||
|         } | ||||
|  | ||||
|         Ok(highlighted_line) | ||||
|     } | ||||
|  | ||||
|     fn preprocess(&self, text: &str, cursor: &mut usize) -> String { | ||||
|         if self.config.tab_width > 0 { | ||||
|             return expand_tabs(text, self.config.tab_width, cursor); | ||||
| @@ -377,31 +486,32 @@ impl<'a> Printer for InteractivePrinter<'a> { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         header_components.iter().try_for_each(|component| { | ||||
|             self.print_header_component_indent(handle)?; | ||||
|  | ||||
|             match component { | ||||
|                 StyleComponent::HeaderFilename => writeln!( | ||||
|                     handle, | ||||
|                     "{}{}{}", | ||||
|                     description | ||||
|                         .kind() | ||||
|                         .map(|kind| format!("{}: ", kind)) | ||||
|                         .unwrap_or_else(|| "".into()), | ||||
|                     self.colors.header_value.paint(description.title()), | ||||
|                     mode | ||||
|                 ), | ||||
|  | ||||
|         header_components | ||||
|             .iter() | ||||
|             .try_for_each(|component| match component { | ||||
|                 StyleComponent::HeaderFilename => { | ||||
|                     let header_filename = format!( | ||||
|                         "{}{}{}", | ||||
|                         description | ||||
|                             .kind() | ||||
|                             .map(|kind| format!("{kind}: ")) | ||||
|                             .unwrap_or_else(|| "".into()), | ||||
|                         self.colors.header_value.paint(description.title()), | ||||
|                         mode | ||||
|                     ); | ||||
|                     self.print_header_multiline_component(handle, &header_filename) | ||||
|                 } | ||||
|                 StyleComponent::HeaderFilesize => { | ||||
|                     let bsize = metadata | ||||
|                         .size | ||||
|                         .map(|s| format!("{}", ByteSize(s))) | ||||
|                         .unwrap_or_else(|| "-".into()); | ||||
|                     writeln!(handle, "Size: {}", self.colors.header_value.paint(bsize)) | ||||
|                     let header_filesize = | ||||
|                         format!("Size: {}", self.colors.header_value.paint(bsize)); | ||||
|                     self.print_header_multiline_component(handle, &header_filesize) | ||||
|                 } | ||||
|                 _ => Ok(()), | ||||
|             } | ||||
|         })?; | ||||
|             })?; | ||||
|  | ||||
|         if self.config.style_components.grid() { | ||||
|             if self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable { | ||||
| @@ -442,7 +552,7 @@ impl<'a> Printer for InteractivePrinter<'a> { | ||||
|             "{}", | ||||
|             self.colors | ||||
|                 .grid | ||||
|                 .paint(format!("{}{}{}{}", panel, snip_left, title, snip_right)) | ||||
|                 .paint(format!("{panel}{snip_left}{title}{snip_right}")) | ||||
|         )?; | ||||
|  | ||||
|         Ok(()) | ||||
| @@ -483,34 +593,23 @@ impl<'a> Printer for InteractivePrinter<'a> { | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         let regions = { | ||||
|             let highlighter_from_set = match self.highlighter_from_set { | ||||
|                 Some(ref mut highlighter_from_set) => highlighter_from_set, | ||||
|                 _ => { | ||||
|                     return Ok(()); | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             // skip syntax highlighting on long lines | ||||
|             let too_long = line.len() > 1024 * 16; | ||||
|  | ||||
|             let for_highlighting: &str = if too_long { "\n" } else { &line }; | ||||
|  | ||||
|             let mut highlighted_line = highlighter_from_set | ||||
|                 .highlighter | ||||
|                 .highlight_line(for_highlighting, highlighter_from_set.syntax_set)?; | ||||
|  | ||||
|             if too_long { | ||||
|                 highlighted_line[0].1 = &line; | ||||
|             } | ||||
|  | ||||
|             highlighted_line | ||||
|         }; | ||||
|  | ||||
|         let regions = self.highlight_regions_for_line(&line)?; | ||||
|         if out_of_range { | ||||
|             return Ok(()); | ||||
|         } | ||||
|  | ||||
|         // Skip squeezed lines. | ||||
|         if let Some(squeeze_limit) = self.config.squeeze_lines { | ||||
|             if line.trim_end_matches(|c| c == '\r' || c == '\n').is_empty() { | ||||
|                 self.consecutive_empty_lines += 1; | ||||
|                 if self.consecutive_empty_lines > squeeze_limit { | ||||
|                     return Ok(()); | ||||
|                 } | ||||
|             } else { | ||||
|                 self.consecutive_empty_lines = 0; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let mut cursor: usize = 0; | ||||
|         let mut cursor_max: usize = self.config.term_width; | ||||
|         let mut cursor_total: usize = 0; | ||||
| @@ -521,7 +620,7 @@ impl<'a> Printer for InteractivePrinter<'a> { | ||||
|             self.config.highlighted_lines.0.check(line_number) == RangeCheckResult::InRange; | ||||
|  | ||||
|         if highlight_this_line && self.config.theme == "ansi" { | ||||
|             self.ansi_style.update("^[4m"); | ||||
|             self.ansi_style.update(ANSI_UNDERLINE_ENABLE); | ||||
|         } | ||||
|  | ||||
|         let background_color = self | ||||
| @@ -548,23 +647,17 @@ impl<'a> Printer for InteractivePrinter<'a> { | ||||
|             let italics = self.config.use_italic_text; | ||||
|  | ||||
|             for &(style, region) in ®ions { | ||||
|                 let ansi_iterator = AnsiCodeIterator::new(region); | ||||
|                 let ansi_iterator = EscapeSequenceIterator::new(region); | ||||
|                 for chunk in ansi_iterator { | ||||
|                     match chunk { | ||||
|                         // ANSI escape passthrough. | ||||
|                         (ansi, true) => { | ||||
|                             self.ansi_style.update(ansi); | ||||
|                             write!(handle, "{}", ansi)?; | ||||
|                         } | ||||
|  | ||||
|                         // Regular text. | ||||
|                         (text, false) => { | ||||
|                             let text = &*self.preprocess(text, &mut cursor_total); | ||||
|                         EscapeSequence::Text(text) => { | ||||
|                             let text = self.preprocess(text, &mut cursor_total); | ||||
|                             let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n'); | ||||
|  | ||||
|                             write!( | ||||
|                                 handle, | ||||
|                                 "{}", | ||||
|                                 "{}{}", | ||||
|                                 as_terminal_escaped( | ||||
|                                     style, | ||||
|                                     &format!("{}{}", self.ansi_style, text_trimmed), | ||||
| @@ -572,9 +665,11 @@ impl<'a> Printer for InteractivePrinter<'a> { | ||||
|                                     colored_output, | ||||
|                                     italics, | ||||
|                                     background_color | ||||
|                                 ) | ||||
|                                 ), | ||||
|                                 self.ansi_style.to_reset_sequence(), | ||||
|                             )?; | ||||
|  | ||||
|                             // Pad the rest of the line. | ||||
|                             if text.len() != text_trimmed.len() { | ||||
|                                 if let Some(background_color) = background_color { | ||||
|                                     let ansi_style = Style { | ||||
| @@ -592,6 +687,12 @@ impl<'a> Printer for InteractivePrinter<'a> { | ||||
|                                 write!(handle, "{}", &text[text_trimmed.len()..])?; | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         // ANSI escape passthrough. | ||||
|                         _ => { | ||||
|                             write!(handle, "{}", chunk.raw())?; | ||||
|                             self.ansi_style.update(chunk); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| @@ -601,17 +702,11 @@ impl<'a> Printer for InteractivePrinter<'a> { | ||||
|             } | ||||
|         } else { | ||||
|             for &(style, region) in ®ions { | ||||
|                 let ansi_iterator = AnsiCodeIterator::new(region); | ||||
|                 let ansi_iterator = EscapeSequenceIterator::new(region); | ||||
|                 for chunk in ansi_iterator { | ||||
|                     match chunk { | ||||
|                         // ANSI escape passthrough. | ||||
|                         (ansi, true) => { | ||||
|                             self.ansi_style.update(ansi); | ||||
|                             write!(handle, "{}", ansi)?; | ||||
|                         } | ||||
|  | ||||
|                         // Regular text. | ||||
|                         (text, false) => { | ||||
|                         EscapeSequence::Text(text) => { | ||||
|                             let text = self.preprocess( | ||||
|                                 text.trim_end_matches(|c| c == '\r' || c == '\n'), | ||||
|                                 &mut cursor_total, | ||||
| @@ -654,7 +749,7 @@ impl<'a> Printer for InteractivePrinter<'a> { | ||||
|                                     // It wraps. | ||||
|                                     write!( | ||||
|                                         handle, | ||||
|                                         "{}\n{}", | ||||
|                                         "{}{}\n{}", | ||||
|                                         as_terminal_escaped( | ||||
|                                             style, | ||||
|                                             &format!("{}{}", self.ansi_style, line_buf), | ||||
| @@ -663,6 +758,7 @@ impl<'a> Printer for InteractivePrinter<'a> { | ||||
|                                             self.config.use_italic_text, | ||||
|                                             background_color | ||||
|                                         ), | ||||
|                                         self.ansi_style.to_reset_sequence(), | ||||
|                                         panel_wrap.clone().unwrap() | ||||
|                                     )?; | ||||
|  | ||||
| @@ -691,6 +787,12 @@ impl<'a> Printer for InteractivePrinter<'a> { | ||||
|                                 ) | ||||
|                             )?; | ||||
|                         } | ||||
|  | ||||
|                         // ANSI escape passthrough. | ||||
|                         _ => { | ||||
|                             write!(handle, "{}", chunk.raw())?; | ||||
|                             self.ansi_style.update(chunk); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| @@ -711,8 +813,8 @@ impl<'a> Printer for InteractivePrinter<'a> { | ||||
|         } | ||||
|  | ||||
|         if highlight_this_line && self.config.theme == "ansi" { | ||||
|             self.ansi_style.update("^[24m"); | ||||
|             write!(handle, "\x1B[24m")?; | ||||
|             write!(handle, "{}", ANSI_UNDERLINE_DISABLE.raw())?; | ||||
|             self.ansi_style.update(ANSI_UNDERLINE_DISABLE); | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|   | ||||
| @@ -80,7 +80,7 @@ impl FromStr for StyleComponent { | ||||
|             "full" => Ok(StyleComponent::Full), | ||||
|             "default" => Ok(StyleComponent::Default), | ||||
|             "plain" => Ok(StyleComponent::Plain), | ||||
|             _ => Err(format!("Unknown style '{}'", s).into()), | ||||
|             _ => Err(format!("Unknown style '{s}'").into()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,31 @@ | ||||
| use std::path::Path; | ||||
|  | ||||
| use crate::error::Result; | ||||
| use ignored_suffixes::IgnoredSuffixes; | ||||
| use std::{ | ||||
|     path::Path, | ||||
|     sync::{ | ||||
|         atomic::{AtomicBool, Ordering}, | ||||
|         Arc, | ||||
|     }, | ||||
|     thread, | ||||
| }; | ||||
|  | ||||
| use globset::{Candidate, GlobBuilder, GlobMatcher}; | ||||
| use once_cell::sync::Lazy; | ||||
|  | ||||
| use crate::error::Result; | ||||
| use builtin::BUILTIN_MAPPINGS; | ||||
| use ignored_suffixes::IgnoredSuffixes; | ||||
|  | ||||
| mod builtin; | ||||
| pub mod ignored_suffixes; | ||||
|  | ||||
| fn make_glob_matcher(from: &str) -> Result<GlobMatcher> { | ||||
|     let matcher = GlobBuilder::new(from) | ||||
|         .case_insensitive(true) | ||||
|         .literal_separator(true) | ||||
|         .build()? | ||||
|         .compile_matcher(); | ||||
|     Ok(matcher) | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||||
| #[non_exhaustive] | ||||
| pub enum MappingTarget<'a> { | ||||
| @@ -29,204 +48,108 @@ pub enum MappingTarget<'a> { | ||||
|  | ||||
| #[derive(Debug, Clone, Default)] | ||||
| pub struct SyntaxMapping<'a> { | ||||
|     mappings: Vec<(GlobMatcher, MappingTarget<'a>)>, | ||||
|     /// User-defined mappings at run time. | ||||
|     /// | ||||
|     /// Rules in front have precedence. | ||||
|     custom_mappings: Vec<(GlobMatcher, MappingTarget<'a>)>, | ||||
|  | ||||
|     pub(crate) ignored_suffixes: IgnoredSuffixes<'a>, | ||||
|  | ||||
|     /// A flag to halt glob matcher building, which is offloaded to another thread. | ||||
|     /// | ||||
|     /// We have this so that we can signal the thread to halt early when appropriate. | ||||
|     halt_glob_build: Arc<AtomicBool>, | ||||
| } | ||||
|  | ||||
| impl<'a> Drop for SyntaxMapping<'a> { | ||||
|     fn drop(&mut self) { | ||||
|         // signal the offload thread to halt early | ||||
|         self.halt_glob_build.store(true, Ordering::Relaxed); | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<'a> SyntaxMapping<'a> { | ||||
|     pub fn empty() -> SyntaxMapping<'a> { | ||||
|     pub fn new() -> SyntaxMapping<'a> { | ||||
|         Default::default() | ||||
|     } | ||||
|  | ||||
|     pub fn builtin() -> SyntaxMapping<'a> { | ||||
|         let mut mapping = Self::empty(); | ||||
|         mapping.insert("*.h", MappingTarget::MapTo("C++")).unwrap(); | ||||
|         mapping | ||||
|             .insert(".clang-format", MappingTarget::MapTo("YAML")) | ||||
|             .unwrap(); | ||||
|         mapping.insert("*.fs", MappingTarget::MapTo("F#")).unwrap(); | ||||
|         mapping | ||||
|             .insert("build", MappingTarget::MapToUnknown) | ||||
|             .unwrap(); | ||||
|         mapping | ||||
|             .insert("**/.ssh/config", MappingTarget::MapTo("SSH Config")) | ||||
|             .unwrap(); | ||||
|         mapping | ||||
|             .insert( | ||||
|                 "**/bat/config", | ||||
|                 MappingTarget::MapTo("Bourne Again Shell (bash)"), | ||||
|             ) | ||||
|             .unwrap(); | ||||
|         mapping | ||||
|             .insert( | ||||
|                 "/etc/profile", | ||||
|                 MappingTarget::MapTo("Bourne Again Shell (bash)"), | ||||
|             ) | ||||
|             .unwrap(); | ||||
|         mapping | ||||
|             .insert( | ||||
|                 "os-release", | ||||
|                 MappingTarget::MapTo("Bourne Again Shell (bash)"), | ||||
|             ) | ||||
|             .unwrap(); | ||||
|         mapping | ||||
|             .insert("*.pac", MappingTarget::MapTo("JavaScript (Babel)")) | ||||
|             .unwrap(); | ||||
|         mapping | ||||
|             .insert("fish_history", MappingTarget::MapTo("YAML")) | ||||
|             .unwrap(); | ||||
|  | ||||
|         for glob in ["*.jsonl", "*.sarif"] { | ||||
|             mapping.insert(glob, MappingTarget::MapTo("JSON")).unwrap(); | ||||
|         } | ||||
|  | ||||
|         // See #2151, https://nmap.org/book/nse-language.html | ||||
|         mapping | ||||
|             .insert("*.nse", MappingTarget::MapTo("Lua")) | ||||
|             .unwrap(); | ||||
|  | ||||
|         // See #1008 | ||||
|         mapping | ||||
|             .insert("rails", MappingTarget::MapToUnknown) | ||||
|             .unwrap(); | ||||
|  | ||||
|         mapping | ||||
|             .insert("Containerfile", MappingTarget::MapTo("Dockerfile")) | ||||
|             .unwrap(); | ||||
|  | ||||
|         mapping | ||||
|             .insert("*.ksh", MappingTarget::MapTo("Bourne Again Shell (bash)")) | ||||
|             .unwrap(); | ||||
|  | ||||
|         // Nginx and Apache syntax files both want to style all ".conf" files | ||||
|         // see #1131 and #1137 | ||||
|         mapping | ||||
|             .insert("*.conf", MappingTarget::MapExtensionToUnknown) | ||||
|             .unwrap(); | ||||
|  | ||||
|         for glob in &[ | ||||
|             "/etc/nginx/**/*.conf", | ||||
|             "/etc/nginx/sites-*/**/*", | ||||
|             "nginx.conf", | ||||
|             "mime.types", | ||||
|         ] { | ||||
|             mapping.insert(glob, MappingTarget::MapTo("nginx")).unwrap(); | ||||
|         } | ||||
|  | ||||
|         for glob in &[ | ||||
|             "/etc/apache2/**/*.conf", | ||||
|             "/etc/apache2/sites-*/**/*", | ||||
|             "httpd.conf", | ||||
|         ] { | ||||
|             mapping | ||||
|                 .insert(glob, MappingTarget::MapTo("Apache Conf")) | ||||
|                 .unwrap(); | ||||
|         } | ||||
|  | ||||
|         for glob in &[ | ||||
|             "**/systemd/**/*.conf", | ||||
|             "**/systemd/**/*.example", | ||||
|             "*.automount", | ||||
|             "*.device", | ||||
|             "*.dnssd", | ||||
|             "*.link", | ||||
|             "*.mount", | ||||
|             "*.netdev", | ||||
|             "*.network", | ||||
|             "*.nspawn", | ||||
|             "*.path", | ||||
|             "*.service", | ||||
|             "*.scope", | ||||
|             "*.slice", | ||||
|             "*.socket", | ||||
|             "*.swap", | ||||
|             "*.target", | ||||
|             "*.timer", | ||||
|         ] { | ||||
|             mapping.insert(glob, MappingTarget::MapTo("INI")).unwrap(); | ||||
|         } | ||||
|  | ||||
|         // unix mail spool | ||||
|         for glob in &["/var/spool/mail/*", "/var/mail/*"] { | ||||
|             mapping.insert(glob, MappingTarget::MapTo("Email")).unwrap() | ||||
|         } | ||||
|  | ||||
|         // pacman hooks | ||||
|         mapping | ||||
|             .insert("*.hook", MappingTarget::MapTo("INI")) | ||||
|             .unwrap(); | ||||
|  | ||||
|         mapping | ||||
|             .insert("*.ron", MappingTarget::MapTo("Rust")) | ||||
|             .unwrap(); | ||||
|  | ||||
|         // Global git config files rooted in `$XDG_CONFIG_HOME/git/` or `$HOME/.config/git/` | ||||
|         // See e.g. https://git-scm.com/docs/git-config#FILES | ||||
|         match ( | ||||
|             std::env::var_os("XDG_CONFIG_HOME").filter(|val| !val.is_empty()), | ||||
|             std::env::var_os("HOME") | ||||
|                 .filter(|val| !val.is_empty()) | ||||
|                 .map(|home| Path::new(&home).join(".config")), | ||||
|         ) { | ||||
|             (Some(xdg_config_home), Some(default_config_home)) | ||||
|                 if xdg_config_home == default_config_home => { | ||||
|                 insert_git_config_global(&mut mapping, &xdg_config_home) | ||||
|     /// Start a thread to build the glob matchers for all builtin mappings. | ||||
|     /// | ||||
|     /// The use of this function while not necessary, is useful to speed up startup | ||||
|     /// times by starting this work early in parallel. | ||||
|     /// | ||||
|     /// The thread halts if/when `halt_glob_build` is set to true. | ||||
|     pub fn start_offload_build_all(&self) { | ||||
|         let halt = Arc::clone(&self.halt_glob_build); | ||||
|         thread::spawn(move || { | ||||
|             for (matcher, _) in BUILTIN_MAPPINGS.iter() { | ||||
|                 if halt.load(Ordering::Relaxed) { | ||||
|                     break; | ||||
|                 } | ||||
|                 Lazy::force(matcher); | ||||
|             } | ||||
|             (Some(xdg_config_home), Some(default_config_home)) /* else guard */ => { | ||||
|                 insert_git_config_global(&mut mapping, &xdg_config_home); | ||||
|                 insert_git_config_global(&mut mapping, &default_config_home) | ||||
|             } | ||||
|             (Some(config_home), None) => insert_git_config_global(&mut mapping, &config_home), | ||||
|             (None, Some(config_home)) => insert_git_config_global(&mut mapping, &config_home), | ||||
|             (None, None) => (), | ||||
|         }; | ||||
|  | ||||
|         fn insert_git_config_global(mapping: &mut SyntaxMapping, config_home: impl AsRef<Path>) { | ||||
|             let git_config_path = config_home.as_ref().join("git"); | ||||
|  | ||||
|             mapping | ||||
|                 .insert( | ||||
|                     &git_config_path.join("config").to_string_lossy(), | ||||
|                     MappingTarget::MapTo("Git Config"), | ||||
|                 ) | ||||
|                 .ok(); | ||||
|  | ||||
|             mapping | ||||
|                 .insert( | ||||
|                     &git_config_path.join("ignore").to_string_lossy(), | ||||
|                     MappingTarget::MapTo("Git Ignore"), | ||||
|                 ) | ||||
|                 .ok(); | ||||
|  | ||||
|             mapping | ||||
|                 .insert( | ||||
|                     &git_config_path.join("attributes").to_string_lossy(), | ||||
|                     MappingTarget::MapTo("Git Attributes"), | ||||
|                 ) | ||||
|                 .ok(); | ||||
|         } | ||||
|  | ||||
|         mapping | ||||
|         }); | ||||
|         // Note that this thread is not joined upon completion because there's | ||||
|         // no shared resources that need synchronization to be safely dropped. | ||||
|         // If we later add code into this thread that requires interesting | ||||
|         // resources (e.g. IO), it would be a good idea to store the handle | ||||
|         // and join it on drop. | ||||
|     } | ||||
|  | ||||
|     pub fn insert(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> { | ||||
|         let glob = GlobBuilder::new(from) | ||||
|             .case_insensitive(true) | ||||
|             .literal_separator(true) | ||||
|             .build()?; | ||||
|         self.mappings.push((glob.compile_matcher(), to)); | ||||
|         let matcher = make_glob_matcher(from)?; | ||||
|         self.custom_mappings.push((matcher, to)); | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn mappings(&self) -> &[(GlobMatcher, MappingTarget<'a>)] { | ||||
|         &self.mappings | ||||
|     /// Returns an iterator over all mappings. User-defined mappings are listed | ||||
|     /// before builtin mappings; mappings in front have higher precedence. | ||||
|     /// | ||||
|     /// Builtin mappings' `GlobMatcher`s are lazily compiled. | ||||
|     /// | ||||
|     /// Note that this function only returns mappings that are valid under the | ||||
|     /// current environment. For details see [`Self::builtin_mappings`]. | ||||
|     pub fn all_mappings(&self) -> impl Iterator<Item = (&GlobMatcher, &MappingTarget<'a>)> { | ||||
|         self.custom_mappings() | ||||
|             .iter() | ||||
|             .map(|(matcher, target)| (matcher, target)) // as_ref | ||||
|             .chain( | ||||
|                 // we need a map with a closure to "do" the lifetime variance | ||||
|                 // see: https://discord.com/channels/273534239310479360/1120124565591425034/1170543402870382653 | ||||
|                 // also, clippy false positive: | ||||
|                 // see: https://github.com/rust-lang/rust-clippy/issues/9280 | ||||
|                 #[allow(clippy::map_identity)] | ||||
|                 self.builtin_mappings().map(|rule| rule), | ||||
|             ) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn get_syntax_for(&self, path: impl AsRef<Path>) -> Option<MappingTarget<'a>> { | ||||
|     /// Returns an iterator over all valid builtin mappings. Mappings in front | ||||
|     /// have higher precedence. | ||||
|     /// | ||||
|     /// The `GlabMatcher`s are lazily compiled. | ||||
|     /// | ||||
|     /// Mappings that are invalid under the current environment (i.e. rule | ||||
|     /// requires environment variable(s) that is unset, or the joined string | ||||
|     /// after variable(s) replacement is not a valid glob expression) are | ||||
|     /// ignored. | ||||
|     pub fn builtin_mappings( | ||||
|         &self, | ||||
|     ) -> impl Iterator<Item = (&'static GlobMatcher, &'static MappingTarget<'static>)> { | ||||
|         BUILTIN_MAPPINGS | ||||
|             .iter() | ||||
|             .filter_map(|(matcher, target)| matcher.as_ref().map(|glob| (glob, target))) | ||||
|     } | ||||
|  | ||||
|     /// Returns all user-defined mappings. | ||||
|     pub fn custom_mappings(&self) -> &[(GlobMatcher, MappingTarget<'a>)] { | ||||
|         &self.custom_mappings | ||||
|     } | ||||
|  | ||||
|     pub fn get_syntax_for(&self, path: impl AsRef<Path>) -> Option<MappingTarget<'a>> { | ||||
|         // Try matching on the file name as-is. | ||||
|         let candidate = Candidate::new(&path); | ||||
|         let candidate_filename = path.as_ref().file_name().map(Candidate::new); | ||||
|         for (ref glob, ref syntax) in self.mappings.iter().rev() { | ||||
|         for (glob, syntax) in self.all_mappings() { | ||||
|             if glob.is_match_candidate(&candidate) | ||||
|                 || candidate_filename | ||||
|                     .as_ref() | ||||
| @@ -252,9 +175,46 @@ impl<'a> SyntaxMapping<'a> { | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|  | ||||
|     #[test] | ||||
|     fn basic() { | ||||
|         let mut map = SyntaxMapping::empty(); | ||||
|     fn builtin_mappings_work() { | ||||
|         let map = SyntaxMapping::new(); | ||||
|  | ||||
|         assert_eq!( | ||||
|             map.get_syntax_for("/path/to/build"), | ||||
|             Some(MappingTarget::MapToUnknown) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn all_fixed_builtin_mappings_can_compile() { | ||||
|         let map = SyntaxMapping::new(); | ||||
|  | ||||
|         // collect call evaluates all lazy closures | ||||
|         // fixed builtin mappings will panic if they fail to compile | ||||
|         let _mappings = map.builtin_mappings().collect::<Vec<_>>(); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn builtin_mappings_matcher_only_compile_once() { | ||||
|         let map = SyntaxMapping::new(); | ||||
|  | ||||
|         let two_iterations: Vec<_> = (0..2) | ||||
|             .map(|_| { | ||||
|                 // addresses of every matcher | ||||
|                 map.builtin_mappings() | ||||
|                     .map(|(matcher, _)| matcher as *const _ as usize) | ||||
|                     .collect::<Vec<_>>() | ||||
|             }) | ||||
|             .collect(); | ||||
|  | ||||
|         // if the matchers are only compiled once, their address should remain the same | ||||
|         assert_eq!(two_iterations[0], two_iterations[1]); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn custom_mappings_work() { | ||||
|         let mut map = SyntaxMapping::new(); | ||||
|         map.insert("/path/to/Cargo.lock", MappingTarget::MapTo("TOML")) | ||||
|             .ok(); | ||||
|         map.insert("/path/to/.ignore", MappingTarget::MapTo("Git Ignore")) | ||||
| @@ -273,52 +233,32 @@ mod tests { | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn user_can_override_builtin_mappings() { | ||||
|         let mut map = SyntaxMapping::builtin(); | ||||
|     fn custom_mappings_override_builtin() { | ||||
|         let mut map = SyntaxMapping::new(); | ||||
|  | ||||
|         assert_eq!( | ||||
|             map.get_syntax_for("/etc/profile"), | ||||
|             Some(MappingTarget::MapTo("Bourne Again Shell (bash)")) | ||||
|             map.get_syntax_for("/path/to/httpd.conf"), | ||||
|             Some(MappingTarget::MapTo("Apache Conf")) | ||||
|         ); | ||||
|         map.insert("/etc/profile", MappingTarget::MapTo("My Syntax")) | ||||
|         map.insert("httpd.conf", MappingTarget::MapTo("My Syntax")) | ||||
|             .ok(); | ||||
|         assert_eq!( | ||||
|             map.get_syntax_for("/etc/profile"), | ||||
|             map.get_syntax_for("/path/to/httpd.conf"), | ||||
|             Some(MappingTarget::MapTo("My Syntax")) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn builtin_mappings() { | ||||
|         let map = SyntaxMapping::builtin(); | ||||
|     fn custom_mappings_precedence() { | ||||
|         let mut map = SyntaxMapping::new(); | ||||
|  | ||||
|         map.insert("/path/to/foo", MappingTarget::MapTo("alpha")) | ||||
|             .ok(); | ||||
|         map.insert("/path/to/foo", MappingTarget::MapTo("bravo")) | ||||
|             .ok(); | ||||
|         assert_eq!( | ||||
|             map.get_syntax_for("/path/to/build"), | ||||
|             Some(MappingTarget::MapToUnknown) | ||||
|             map.get_syntax_for("/path/to/foo"), | ||||
|             Some(MappingTarget::MapTo("alpha")) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     /// verifies that SyntaxMapping::builtin() doesn't repeat `Glob`-based keys | ||||
|     fn no_duplicate_builtin_keys() { | ||||
|         let mappings = SyntaxMapping::builtin().mappings; | ||||
|         for i in 0..mappings.len() { | ||||
|             let tail = mappings[i + 1..].into_iter(); | ||||
|             let (dupl, _): (Vec<_>, Vec<_>) = | ||||
|                 tail.partition(|item| item.0.glob() == mappings[i].0.glob()); | ||||
|  | ||||
|             // emit repeats on failure | ||||
|             assert_eq!( | ||||
|                 dupl.len(), | ||||
|                 0, | ||||
|                 "Glob pattern `{}` mapped to multiple: {:?}", | ||||
|                 mappings[i].0.glob().glob(), | ||||
|                 { | ||||
|                     let (_, mut dupl_targets): (Vec<GlobMatcher>, Vec<MappingTarget>) = | ||||
|                         dupl.into_iter().cloned().unzip(); | ||||
|                     dupl_targets.push(mappings[i].1) | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										91
									
								
								src/syntax_mapping/builtin.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								src/syntax_mapping/builtin.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| use std::env; | ||||
|  | ||||
| use globset::GlobMatcher; | ||||
| use once_cell::sync::Lazy; | ||||
|  | ||||
| use crate::syntax_mapping::{make_glob_matcher, MappingTarget}; | ||||
|  | ||||
| // Static syntax mappings generated from /src/syntax_mapping/builtins/ by the | ||||
| // build script (/build/syntax_mapping.rs). | ||||
| include!(concat!( | ||||
|     env!("OUT_DIR"), | ||||
|     "/codegen_static_syntax_mappings.rs" | ||||
| )); | ||||
|  | ||||
| // The defined matcher strings are analysed at compile time and converted into | ||||
| // lazily-compiled `GlobMatcher`s. This is so that the string searches are moved | ||||
| // from run time to compile time, thus improving startup performance. | ||||
| // | ||||
| // To any future maintainer (including possibly myself) wondering why there is | ||||
| // not a `BuiltinMatcher` enum that looks like this: | ||||
| // | ||||
| // ``` | ||||
| // enum BuiltinMatcher { | ||||
| //     Fixed(&'static str), | ||||
| //     Dynamic(Lazy<Option<String>>), | ||||
| // } | ||||
| // ``` | ||||
| // | ||||
| // Because there was. I tried it and threw it out. | ||||
| // | ||||
| // Naively looking at the problem from a distance, this may seem like a good | ||||
| // design (strongly typed etc. etc.). It would also save on compiled size by | ||||
| // extracting out common behaviour into functions. But while actually | ||||
| // implementing the lazy matcher compilation logic, I realised that it's most | ||||
| // convenient for `BUILTIN_MAPPINGS` to have the following type: | ||||
| // | ||||
| // `[(Lazy<Option<GlobMatcher>>, MappingTarget); N]` | ||||
| // | ||||
| // The benefit for this is that operations like listing all builtin mappings | ||||
| // would be effectively memoised. The caller would not have to compile another | ||||
| // `GlobMatcher` for rules that they have previously visited. | ||||
| // | ||||
| // Unfortunately, this means we are going to have to store a distinct closure | ||||
| // for each rule anyway, which makes a `BuiltinMatcher` enum a pointless layer | ||||
| // of indirection. | ||||
| // | ||||
| // In the current implementation, the closure within each generated rule simply | ||||
| // calls either `build_matcher_fixed` or `build_matcher_dynamic`, depending on | ||||
| // whether the defined matcher contains dynamic segments or not. | ||||
|  | ||||
| /// Compile a fixed glob string into a glob matcher. | ||||
| /// | ||||
| /// A failure to compile is a fatal error. | ||||
| /// | ||||
| /// Used internally by `Lazy<Option<GlobMatcher>>`'s lazy evaluation closure. | ||||
| fn build_matcher_fixed(from: &str) -> GlobMatcher { | ||||
|     make_glob_matcher(from).expect("A builtin fixed glob matcher failed to compile") | ||||
| } | ||||
|  | ||||
| /// Join a list of matcher segments to create a glob string, replacing all | ||||
| /// environment variables, then compile to a glob matcher. | ||||
| /// | ||||
| /// Returns `None` if any replacement fails, or if the joined glob string fails | ||||
| /// to compile. | ||||
| /// | ||||
| /// Used internally by `Lazy<Option<GlobMatcher>>`'s lazy evaluation closure. | ||||
| fn build_matcher_dynamic(segs: &[MatcherSegment]) -> Option<GlobMatcher> { | ||||
|     // join segments | ||||
|     let mut buf = String::new(); | ||||
|     for seg in segs { | ||||
|         match seg { | ||||
|             MatcherSegment::Text(s) => buf.push_str(s), | ||||
|             MatcherSegment::Env(var) => { | ||||
|                 let replaced = env::var(var).ok()?; | ||||
|                 buf.push_str(&replaced); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     // compile glob matcher | ||||
|     let matcher = make_glob_matcher(&buf).ok()?; | ||||
|     Some(matcher) | ||||
| } | ||||
|  | ||||
| /// A segment of a dynamic builtin matcher. | ||||
| /// | ||||
| /// Used internally by `Lazy<Option<GlobMatcher>>`'s lazy evaluation closure. | ||||
| #[derive(Clone, Debug)] | ||||
| enum MatcherSegment { | ||||
|     Text(&'static str), | ||||
|     Env(&'static str), | ||||
| } | ||||
							
								
								
									
										116
									
								
								src/syntax_mapping/builtins/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/syntax_mapping/builtins/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| # `/src/syntax_mapping/builtins` | ||||
|  | ||||
| The files in this directory define path/name-based syntax mappings, which amend | ||||
| and take precedence over the extension/content-based syntax mappings provided by | ||||
| [syntect](https://github.com/trishume/syntect). | ||||
|  | ||||
| ## File organisation | ||||
|  | ||||
| Each TOML file should describe the syntax mappings of a single application, or | ||||
| otherwise a set of logically-related rules. | ||||
|  | ||||
| What defines "a single application" here is deliberately vague, since the | ||||
| file-splitting is purely for maintainability reasons. (Technically, we could | ||||
| just as well use a single TOML file.) So just use common sense. | ||||
|  | ||||
| TOML files should reside in the corresponding subdirectory of the platform(s) | ||||
| that they intend to target. At compile time, the build script will go through | ||||
| each subdirectory that is applicable to the compilation target, collect the | ||||
| syntax mappings defined by all TOML files, and embed them into the binary. | ||||
|  | ||||
| ## File syntax | ||||
|  | ||||
| Each TOML file should contain a single section named `mappings`, with each of | ||||
| its keys being a language identifier (first column of `bat -L`; also referred to | ||||
| as "target"). | ||||
|  | ||||
| The value of each key should be an array of strings, with each item being a glob | ||||
| matcher. We will call each of these items a "rule". | ||||
|  | ||||
| For example, if `foo-application` uses both TOML and YAML configuration files, | ||||
| we could write something like this: | ||||
|  | ||||
| ```toml | ||||
| # 30-foo-application.toml | ||||
| [mappings] | ||||
| "TOML" = [ | ||||
|     # rules for TOML syntax go here | ||||
|     "/usr/share/foo-application/toml-config/*.conf", | ||||
|     "/etc/foo-application/toml-config/*.conf", | ||||
| ] | ||||
| "YAML" = [ | ||||
|     # rules for YAML syntax go here | ||||
|     # ... | ||||
| ] | ||||
| ``` | ||||
|  | ||||
| ### Dynamic environment variable replacement | ||||
|  | ||||
| In additional to the standard glob matcher syntax, rules also support dynamic | ||||
| replacement of environment variables at runtime. This allows us to concisely | ||||
| handle things like [XDG](https://specifications.freedesktop.org/basedir-spec/latest/). | ||||
|  | ||||
| All environment variables intended to be replaced at runtime must be enclosed in | ||||
| `${}`, for example `"/foo/*/${YOUR_ENV}-suffix/*.log"`. Note that this is the | ||||
| **only** admissible syntax; other variable substitution syntaxes are not | ||||
| supported and will either cause a compile time error, or be treated as plain | ||||
| text. | ||||
|  | ||||
| For example, if `foo-application` also supports per-user configuration files, we | ||||
| could write something like this: | ||||
|  | ||||
| ```toml | ||||
| # 30-foo-application.toml | ||||
| [mappings] | ||||
| "TOML" = [ | ||||
|     # rules for TOML syntax go here | ||||
|     "/usr/share/foo-application/toml-config/*.conf", | ||||
|     "/etc/foo-application/toml-config/*.conf", | ||||
|     "${XDG_CONFIG_HOME}/foo-application/toml-config/*.conf", | ||||
|     "${HOME}/.config/foo-application/toml-config/*.conf", | ||||
| ] | ||||
| "YAML" = [ | ||||
|     # rules for YAML syntax go here | ||||
|     # ... | ||||
| ] | ||||
| ``` | ||||
|  | ||||
| If any environment variable replacement in a rule fails (for example when a | ||||
| variable is unset), or if the glob string after replacements is invalid, the | ||||
| entire rule will be ignored. | ||||
|  | ||||
| ### Explicitly mapping to unknown | ||||
|  | ||||
| Sometimes it may be necessary to "unset" a particular syntect mapping - perhaps | ||||
| a syntax's matching rules are "too greedy", and is claiming files that it should | ||||
| not. In this case, there are two special identifiers: | ||||
| `MappingTarget::MapToUnknown` and `MappingTarget::MapExtensionToUnknown` | ||||
| (corresponding to the two variants of the `syntax_mapping::MappingTarget` enum). | ||||
|  | ||||
| An example of this would be `*.conf` files in general. So we may write something | ||||
| like this: | ||||
|  | ||||
| ```toml | ||||
| # 99-unset-ambiguous-extensions.toml | ||||
| [mappings] | ||||
| "MappingTarget::MapExtensionToUnknown" = [ | ||||
|     "*.conf", | ||||
| ] | ||||
| ``` | ||||
|  | ||||
| ## Ordering | ||||
|  | ||||
| At compile time, all TOML files applicable to the target are processed in | ||||
| lexicographical filename order. So `00-foo.toml` takes precedence over | ||||
| `10-bar.toml`, which takes precedence over `20-baz.toml`, and so on. Note that | ||||
| **only** the filenames of the TOML files are taken into account; the | ||||
| subdirectories they are placed in have no influence on ordering. | ||||
|  | ||||
| This behaviour can be occasionally useful for creating high/low priority rules, | ||||
| such as in the aforementioned example of explicitly mapping `*.conf` files to | ||||
| unknown. Generally this should not be much of a concern though, since rules | ||||
| should be written as specifically as possible for each application. | ||||
|  | ||||
| Rules within each TOML file are processed (and therefore matched) in the order | ||||
| in which they are defined. At runtime, the syntax selection algorithm will | ||||
| short-circuit and return the target of the first matching rule. | ||||
							
								
								
									
										0
									
								
								src/syntax_mapping/builtins/bsd-family/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/syntax_mapping/builtins/bsd-family/.gitkeep
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| [mappings] | ||||
| "Bourne Again Shell (bash)" = ["/etc/os-release", "/var/run/os-release"] | ||||
							
								
								
									
										0
									
								
								src/syntax_mapping/builtins/common/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/syntax_mapping/builtins/common/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										2
									
								
								src/syntax_mapping/builtins/common/50-apache.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/syntax_mapping/builtins/common/50-apache.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| [mappings] | ||||
| "Apache Conf" = ["httpd.conf"] | ||||
| @@ -0,0 +1,2 @@ | ||||
| [mappings] | ||||
| "INI" = ["**/.aws/credentials", "**/.aws/config"] | ||||
							
								
								
									
										2
									
								
								src/syntax_mapping/builtins/common/50-bat.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/syntax_mapping/builtins/common/50-bat.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| [mappings] | ||||
| "Bourne Again Shell (bash)" = ["**/bat/config"] | ||||
							
								
								
									
										2
									
								
								src/syntax_mapping/builtins/common/50-container.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/syntax_mapping/builtins/common/50-container.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| [mappings] | ||||
| "Dockerfile" = ["Containerfile"] | ||||
							
								
								
									
										6
									
								
								src/syntax_mapping/builtins/common/50-cpp.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/syntax_mapping/builtins/common/50-cpp.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| [mappings] | ||||
| "C++" = [ | ||||
|     # probably better than the default Objective C mapping #877 | ||||
|     "*.h", | ||||
| ] | ||||
| "YAML" = [".clang-format"] | ||||
							
								
								
									
										2
									
								
								src/syntax_mapping/builtins/common/50-f-sharp.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/syntax_mapping/builtins/common/50-f-sharp.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| [mappings] | ||||
| "F#" = ["*.fs"] | ||||
							
								
								
									
										10
									
								
								src/syntax_mapping/builtins/common/50-git.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/syntax_mapping/builtins/common/50-git.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| # Global git config files rooted in `$XDG_CONFIG_HOME/git/` or `$HOME/.config/git/` | ||||
| # See e.g. https://git-scm.com/docs/git-config#FILES | ||||
|  | ||||
| [mappings] | ||||
| "Git Config" = ["${XDG_CONFIG_HOME}/git/config", "${HOME}/.config/git/config"] | ||||
| "Git Ignore" = ["${XDG_CONFIG_HOME}/git/ignore", "${HOME}/.config/git/ignore"] | ||||
| "Git Attributes" = [ | ||||
|     "${XDG_CONFIG_HOME}/git/attributes", | ||||
|     "${HOME}/.config/git/attributes", | ||||
| ] | ||||
							
								
								
									
										3
									
								
								src/syntax_mapping/builtins/common/50-json.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/syntax_mapping/builtins/common/50-json.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # JSON Lines is a simple variation of JSON #2535 | ||||
| [mappings] | ||||
| "JSON" = ["*.jsonl", "*.jsonc"] | ||||
							
								
								
									
										2
									
								
								src/syntax_mapping/builtins/common/50-nginx.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/syntax_mapping/builtins/common/50-nginx.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| [mappings] | ||||
| "nginx" = ["nginx.conf", "mime.types"] | ||||
							
								
								
									
										3
									
								
								src/syntax_mapping/builtins/common/50-nmap.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/syntax_mapping/builtins/common/50-nmap.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| [mappings] | ||||
| # See #2151, https://nmap.org/book/nse-language.html | ||||
| "Lua" = ["*.nse"] | ||||
| @@ -0,0 +1,3 @@ | ||||
| # 1515 | ||||
| [mappings] | ||||
| "JavaScript (Babel)" = ["*.pac"] | ||||
							
								
								
									
										3
									
								
								src/syntax_mapping/builtins/common/50-ron.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/syntax_mapping/builtins/common/50-ron.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # Rusty Object Notation #2427 | ||||
| [mappings] | ||||
| "Rust" = ["*.ron"] | ||||
							
								
								
									
										3
									
								
								src/syntax_mapping/builtins/common/50-sarif.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/syntax_mapping/builtins/common/50-sarif.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # SARIF is a format for reporting static analysis results #2695 | ||||
| [mappings] | ||||
| "JSON" = ["*.sarif"] | ||||
							
								
								
									
										2
									
								
								src/syntax_mapping/builtins/common/50-ssh.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/syntax_mapping/builtins/common/50-ssh.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| [mappings] | ||||
| "SSH Config" = ["**/.ssh/config"] | ||||
| @@ -0,0 +1,5 @@ | ||||
| [mappings] | ||||
| "MappingTarget::MapExtensionToUnknown" = [ | ||||
|     # common extension used for all kinds of formats | ||||
|     "*.conf", | ||||
| ] | ||||
| @@ -0,0 +1,7 @@ | ||||
| [mappings] | ||||
| "MappingTarget::MapToUnknown" = [ | ||||
|     # "NAnt Build File" should only match *.build files, not files named "build" | ||||
|     "build", | ||||
|     # "bin/rails" scripts in a Ruby project misidentified as HTML (Rails) #1008 | ||||
|     "rails", | ||||
| ] | ||||
							
								
								
									
										3
									
								
								src/syntax_mapping/builtins/common/xonsh.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/syntax_mapping/builtins/common/xonsh.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # Xonsh shell (https://xon.sh/) | ||||
| [mappings] | ||||
| "Python" = ["*.xsh", "*.xonshrc"] | ||||
							
								
								
									
										0
									
								
								src/syntax_mapping/builtins/linux/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/syntax_mapping/builtins/linux/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										8
									
								
								src/syntax_mapping/builtins/linux/50-containers.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/syntax_mapping/builtins/linux/50-containers.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| # see https://github.com/containers/image/tree/main/docs | ||||
| [mappings] | ||||
| "TOML" = [ | ||||
|     "/usr/share/containers/**/*.conf", | ||||
|     "/etc/containers/**/*.conf", | ||||
|     "${HOME}/.config/containers/**/*.conf", | ||||
|     "${XDG_CONFIG_HOME}/containers/**/*.conf", | ||||
| ] | ||||
							
								
								
									
										7
									
								
								src/syntax_mapping/builtins/linux/50-os-release.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/syntax_mapping/builtins/linux/50-os-release.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| [mappings] | ||||
| "Bourne Again Shell (bash)" = [ | ||||
|     "/etc/os-release", | ||||
|     "/usr/lib/os-release", | ||||
|     "/etc/initrd-release", | ||||
|     "/usr/lib/extension-release.d/extension-release.*", | ||||
| ] | ||||
							
								
								
									
										3
									
								
								src/syntax_mapping/builtins/linux/50-pacman.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/syntax_mapping/builtins/linux/50-pacman.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| [mappings] | ||||
| # pacman hooks | ||||
| "INI" = ["/usr/share/libalpm/hooks/*.hook", "/etc/pacman.d/hooks/*.hook"] | ||||
							
								
								
									
										7
									
								
								src/syntax_mapping/builtins/linux/50-podman-quadlet.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/syntax_mapping/builtins/linux/50-podman-quadlet.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| # see `man quadlet` | ||||
| [mappings] | ||||
| "INI" = [ | ||||
|     "**/containers/systemd/*.{container,volume,network,kube,image}", | ||||
|     "**/containers/systemd/users/*.{container,volume,network,kube,image}", | ||||
|     "**/containers/systemd/users/*/*.{container,volume,network,kube,image}", | ||||
| ] | ||||
							
								
								
									
										21
									
								
								src/syntax_mapping/builtins/linux/50-systemd.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/syntax_mapping/builtins/linux/50-systemd.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| [mappings] | ||||
| "INI" = [ | ||||
|     "**/systemd/**/*.conf", | ||||
|     "**/systemd/**/*.example", | ||||
|     "*.automount", | ||||
|     "*.device", | ||||
|     "*.dnssd", | ||||
|     "*.link", | ||||
|     "*.mount", | ||||
|     "*.netdev", | ||||
|     "*.network", | ||||
|     "*.nspawn", | ||||
|     "*.path", | ||||
|     "*.service", | ||||
|     "*.scope", | ||||
|     "*.slice", | ||||
|     "*.socket", | ||||
|     "*.swap", | ||||
|     "*.target", | ||||
|     "*.timer", | ||||
| ] | ||||
							
								
								
									
										0
									
								
								src/syntax_mapping/builtins/macos/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/syntax_mapping/builtins/macos/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								src/syntax_mapping/builtins/unix-family/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/syntax_mapping/builtins/unix-family/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										2
									
								
								src/syntax_mapping/builtins/unix-family/50-apache.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/syntax_mapping/builtins/unix-family/50-apache.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| [mappings] | ||||
| "Apache Conf" = ["/etc/apache2/**/*.conf", "/etc/apache2/sites-*/**/*"] | ||||
| @@ -0,0 +1,2 @@ | ||||
| [mappings] | ||||
| "YAML" = ["fish_history"] | ||||
| @@ -0,0 +1,3 @@ | ||||
| # KornShell is backward-compatible with the Bourne shell #2633 | ||||
| [mappings] | ||||
| "Bourne Again Shell (bash)" = ["*.ksh"] | ||||
| @@ -0,0 +1,2 @@ | ||||
| [mappings] | ||||
| "Email" = ["/var/spool/mail/*", "/var/mail/*"] | ||||
							
								
								
									
										2
									
								
								src/syntax_mapping/builtins/unix-family/50-nginx.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/syntax_mapping/builtins/unix-family/50-nginx.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| [mappings] | ||||
| "nginx" = ["/etc/nginx/**/*.conf", "/etc/nginx/sites-*/**/*"] | ||||
							
								
								
									
										5
									
								
								src/syntax_mapping/builtins/unix-family/50-shell.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/syntax_mapping/builtins/unix-family/50-shell.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| [mappings] | ||||
| "Bourne Again Shell (bash)" = [ | ||||
|     # used by lots of shells | ||||
|     "/etc/profile", | ||||
| ] | ||||
| @@ -0,0 +1,3 @@ | ||||
| # see `man wg-quick` | ||||
| [mappings] | ||||
| "INI" = ["/etc/wireguard/*.conf"] | ||||
							
								
								
									
										0
									
								
								src/syntax_mapping/builtins/windows/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/syntax_mapping/builtins/windows/.gitkeep
									
									
									
									
									
										Normal file
									
								
							| @@ -73,7 +73,7 @@ fn internal_suffixes() { | ||||
|     let file_names = ignored_suffixes | ||||
|         .values | ||||
|         .iter() | ||||
|         .map(|suffix| format!("test.json{}", suffix)); | ||||
|         .map(|suffix| format!("test.json{suffix}")); | ||||
|     for file_name_str in file_names { | ||||
|         let file_name = OsStr::new(&file_name_str); | ||||
|         let expected_stripped_file_name = OsStr::new("test.json"); | ||||
| @@ -95,7 +95,7 @@ fn external_suffixes() { | ||||
|     let file_names = ignored_suffixes | ||||
|         .values | ||||
|         .iter() | ||||
|         .map(|suffix| format!("test.json{}", suffix)); | ||||
|         .map(|suffix| format!("test.json{suffix}")); | ||||
|     for file_name_str in file_names { | ||||
|         let file_name = OsStr::new(&file_name_str); | ||||
|         let expected_stripped_file_name = OsStr::new("test.json"); | ||||
|   | ||||
							
								
								
									
										783
									
								
								src/vscreen.rs
									
									
									
									
									
								
							
							
						
						
									
										783
									
								
								src/vscreen.rs
									
									
									
									
									
								
							| @@ -1,4 +1,8 @@ | ||||
| use std::fmt::{Display, Formatter}; | ||||
| use std::{ | ||||
|     fmt::{Display, Formatter}, | ||||
|     iter::Peekable, | ||||
|     str::CharIndices, | ||||
| }; | ||||
|  | ||||
| // Wrapper to avoid unnecessary branching when input doesn't have ANSI escape sequences. | ||||
| pub struct AnsiStyle { | ||||
| @@ -10,7 +14,7 @@ impl AnsiStyle { | ||||
|         AnsiStyle { attributes: None } | ||||
|     } | ||||
|  | ||||
|     pub fn update(&mut self, sequence: &str) -> bool { | ||||
|     pub fn update(&mut self, sequence: EscapeSequence) -> bool { | ||||
|         match &mut self.attributes { | ||||
|             Some(a) => a.update(sequence), | ||||
|             None => { | ||||
| @@ -19,6 +23,13 @@ impl AnsiStyle { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn to_reset_sequence(&self) -> String { | ||||
|         match self.attributes { | ||||
|             Some(ref a) => a.to_reset_sequence(), | ||||
|             None => String::new(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Display for AnsiStyle { | ||||
| @@ -31,6 +42,8 @@ impl Display for AnsiStyle { | ||||
| } | ||||
|  | ||||
| struct Attributes { | ||||
|     has_sgr_sequences: bool, | ||||
|  | ||||
|     foreground: String, | ||||
|     background: String, | ||||
|     underlined: String, | ||||
| @@ -61,11 +74,20 @@ struct Attributes { | ||||
|     /// ON:  ^[9m | ||||
|     /// OFF: ^[29m | ||||
|     strike: String, | ||||
|  | ||||
|     /// The hyperlink sequence. | ||||
|     /// FORMAT: \x1B]8;{ID};{URL}\e\\ | ||||
|     /// | ||||
|     /// `\e\\` may be replaced with BEL `\x07`. | ||||
|     /// Setting both {ID} and {URL} to an empty string represents no hyperlink. | ||||
|     hyperlink: String, | ||||
| } | ||||
|  | ||||
| impl Attributes { | ||||
|     pub fn new() -> Self { | ||||
|         Attributes { | ||||
|             has_sgr_sequences: false, | ||||
|  | ||||
|             foreground: "".to_owned(), | ||||
|             background: "".to_owned(), | ||||
|             underlined: "".to_owned(), | ||||
| @@ -76,34 +98,56 @@ impl Attributes { | ||||
|             underline: "".to_owned(), | ||||
|             italic: "".to_owned(), | ||||
|             strike: "".to_owned(), | ||||
|             hyperlink: "".to_owned(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Update the attributes with an escape sequence. | ||||
|     /// Returns `false` if the sequence is unsupported. | ||||
|     pub fn update(&mut self, sequence: &str) -> bool { | ||||
|         let mut chars = sequence.char_indices().skip(1); | ||||
|  | ||||
|         if let Some((_, t)) = chars.next() { | ||||
|             match t { | ||||
|                 '(' => self.update_with_charset('(', chars.map(|(_, c)| c)), | ||||
|                 ')' => self.update_with_charset(')', chars.map(|(_, c)| c)), | ||||
|                 '[' => { | ||||
|                     if let Some((i, last)) = chars.last() { | ||||
|                         // SAFETY: Always starts with ^[ and ends with m. | ||||
|                         self.update_with_csi(last, &sequence[2..i]) | ||||
|                     } else { | ||||
|                         false | ||||
|     pub fn update(&mut self, sequence: EscapeSequence) -> bool { | ||||
|         use EscapeSequence::*; | ||||
|         match sequence { | ||||
|             Text(_) => return false, | ||||
|             Unknown(_) => { /* defer to update_with_unsupported */ } | ||||
|             OSC { | ||||
|                 raw_sequence, | ||||
|                 command, | ||||
|                 .. | ||||
|             } => { | ||||
|                 if command.starts_with("8;") { | ||||
|                     return self.update_with_hyperlink(raw_sequence); | ||||
|                 } | ||||
|                 /* defer to update_with_unsupported */ | ||||
|             } | ||||
|             CSI { | ||||
|                 final_byte, | ||||
|                 parameters, | ||||
|                 .. | ||||
|             } => { | ||||
|                 match final_byte { | ||||
|                     "m" => return self.update_with_sgr(parameters), | ||||
|                     _ => { | ||||
|                         // NOTE(eth-p): We might want to ignore these, since they involve cursor or buffer manipulation. | ||||
|                         /* defer to update_with_unsupported */ | ||||
|                     } | ||||
|                 } | ||||
|                 _ => self.update_with_unsupported(sequence), | ||||
|             } | ||||
|         } else { | ||||
|             false | ||||
|             NF { nf_sequence, .. } => { | ||||
|                 let mut iter = nf_sequence.chars(); | ||||
|                 match iter.next() { | ||||
|                     Some('(') => return self.update_with_charset('(', iter), | ||||
|                     Some(')') => return self.update_with_charset(')', iter), | ||||
|                     _ => { /* defer to update_with_unsupported */ } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         self.update_with_unsupported(sequence.raw()) | ||||
|     } | ||||
|  | ||||
|     fn sgr_reset(&mut self) { | ||||
|         self.has_sgr_sequences = false; | ||||
|  | ||||
|         self.foreground.clear(); | ||||
|         self.background.clear(); | ||||
|         self.underlined.clear(); | ||||
| @@ -121,13 +165,14 @@ impl Attributes { | ||||
|             .map(|p| p.parse::<u16>()) | ||||
|             .map(|p| p.unwrap_or(0)); // Treat errors as 0. | ||||
|  | ||||
|         self.has_sgr_sequences = true; | ||||
|         while let Some(p) = iter.next() { | ||||
|             match p { | ||||
|                 0 => self.sgr_reset(), | ||||
|                 1 => self.bold = format!("\x1B[{}m", parameters), | ||||
|                 2 => self.dim = format!("\x1B[{}m", parameters), | ||||
|                 3 => self.italic = format!("\x1B[{}m", parameters), | ||||
|                 4 => self.underline = format!("\x1B[{}m", parameters), | ||||
|                 1 => self.bold = "\x1B[1m".to_owned(), | ||||
|                 2 => self.dim = "\x1B[2m".to_owned(), | ||||
|                 3 => self.italic = "\x1B[3m".to_owned(), | ||||
|                 4 => self.underline = "\x1B[4m".to_owned(), | ||||
|                 23 => self.italic.clear(), | ||||
|                 24 => self.underline.clear(), | ||||
|                 22 => { | ||||
| @@ -138,7 +183,7 @@ impl Attributes { | ||||
|                 40..=49 => self.background = Self::parse_color(p, &mut iter), | ||||
|                 58..=59 => self.underlined = Self::parse_color(p, &mut iter), | ||||
|                 90..=97 => self.foreground = Self::parse_color(p, &mut iter), | ||||
|                 100..=107 => self.foreground = Self::parse_color(p, &mut iter), | ||||
|                 100..=107 => self.background = Self::parse_color(p, &mut iter), | ||||
|                 _ => { | ||||
|                     // Unsupported SGR sequence. | ||||
|                     // Be compatible and pretend one just wasn't was provided. | ||||
| @@ -149,19 +194,23 @@ impl Attributes { | ||||
|         true | ||||
|     } | ||||
|  | ||||
|     fn update_with_csi(&mut self, finalizer: char, sequence: &str) -> bool { | ||||
|         if finalizer == 'm' { | ||||
|             self.update_with_sgr(sequence) | ||||
|         } else { | ||||
|             false | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn update_with_unsupported(&mut self, sequence: &str) -> bool { | ||||
|         self.unknown_buffer.push_str(sequence); | ||||
|         false | ||||
|     } | ||||
|  | ||||
|     fn update_with_hyperlink(&mut self, sequence: &str) -> bool { | ||||
|         if sequence == "8;;" { | ||||
|             // Empty hyperlink ID and HREF -> end of hyperlink. | ||||
|             self.hyperlink.clear(); | ||||
|         } else { | ||||
|             self.hyperlink.clear(); | ||||
|             self.hyperlink.push_str(sequence); | ||||
|         } | ||||
|  | ||||
|         true | ||||
|     } | ||||
|  | ||||
|     fn update_with_charset(&mut self, kind: char, set: impl Iterator<Item = char>) -> bool { | ||||
|         self.charset = format!("\x1B{}{}", kind, set.take(1).collect::<String>()); | ||||
|         true | ||||
| @@ -172,20 +221,42 @@ impl Attributes { | ||||
|             8 => match parameters.next() { | ||||
|                 Some(5) /* 256-color */ => format!("\x1B[{};5;{}m", color, join(";", 1, parameters)), | ||||
|                 Some(2) /* 24-bit color */ => format!("\x1B[{};2;{}m", color, join(";", 3, parameters)), | ||||
|                 Some(c) => format!("\x1B[{};{}m", color, c), | ||||
|                 Some(c) => format!("\x1B[{color};{c}m"), | ||||
|                 _ => "".to_owned(), | ||||
|             }, | ||||
|             9 => "".to_owned(), | ||||
|             _ => format!("\x1B[{}m", color), | ||||
|             _ => format!("\x1B[{color}m"), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Gets an ANSI escape sequence to reset all the known attributes. | ||||
|     pub fn to_reset_sequence(&self) -> String { | ||||
|         let mut buf = String::with_capacity(17); | ||||
|  | ||||
|         // TODO: Enable me in a later pull request. | ||||
|         // if self.has_sgr_sequences { | ||||
|         //     buf.push_str("\x1B[m"); | ||||
|         // } | ||||
|  | ||||
|         if !self.hyperlink.is_empty() { | ||||
|             buf.push_str("\x1B]8;;\x1B\\"); // Disable hyperlink. | ||||
|         } | ||||
|  | ||||
|         // TODO: Enable me in a later pull request. | ||||
|         // if !self.charset.is_empty() { | ||||
|         //     // https://espterm.github.io/docs/VT100%20escape%20codes.html | ||||
|         //     buf.push_str("\x1B(B\x1B)B"); // setusg0 and setusg1 | ||||
|         // } | ||||
|  | ||||
|         buf | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Display for Attributes { | ||||
|     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||||
|         write!( | ||||
|             f, | ||||
|             "{}{}{}{}{}{}{}{}{}", | ||||
|             "{}{}{}{}{}{}{}{}{}{}", | ||||
|             self.foreground, | ||||
|             self.background, | ||||
|             self.underlined, | ||||
| @@ -195,6 +266,7 @@ impl Display for Attributes { | ||||
|             self.underline, | ||||
|             self.italic, | ||||
|             self.strike, | ||||
|             self.hyperlink, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -210,3 +282,646 @@ fn join( | ||||
|         .collect::<Vec<String>>() | ||||
|         .join(delimiter) | ||||
| } | ||||
|  | ||||
| /// A range of indices for a raw ANSI escape sequence. | ||||
| #[derive(Debug, PartialEq)] | ||||
| enum EscapeSequenceOffsets { | ||||
|     Text { | ||||
|         start: usize, | ||||
|         end: usize, | ||||
|     }, | ||||
|     Unknown { | ||||
|         start: usize, | ||||
|         end: usize, | ||||
|     }, | ||||
|     #[allow(clippy::upper_case_acronyms)] | ||||
|     NF { | ||||
|         // https://en.wikipedia.org/wiki/ANSI_escape_code#nF_Escape_sequences | ||||
|         start_sequence: usize, | ||||
|         start: usize, | ||||
|         end: usize, | ||||
|     }, | ||||
|     #[allow(clippy::upper_case_acronyms)] | ||||
|     OSC { | ||||
|         // https://en.wikipedia.org/wiki/ANSI_escape_code#OSC_(Operating_System_Command)_sequences | ||||
|         start_sequence: usize, | ||||
|         start_command: usize, | ||||
|         start_terminator: usize, | ||||
|         end: usize, | ||||
|     }, | ||||
|     #[allow(clippy::upper_case_acronyms)] | ||||
|     CSI { | ||||
|         // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences | ||||
|         start_sequence: usize, | ||||
|         start_parameters: usize, | ||||
|         start_intermediates: usize, | ||||
|         start_final_byte: usize, | ||||
|         end: usize, | ||||
|     }, | ||||
| } | ||||
|  | ||||
| /// An iterator over the offests of ANSI/VT escape sequences within a string. | ||||
| /// | ||||
| /// ## Example | ||||
| /// | ||||
| /// ```ignore | ||||
| /// let iter = EscapeSequenceOffsetsIterator::new("\x1B[33mThis is yellow text.\x1B[m"); | ||||
| /// ``` | ||||
| struct EscapeSequenceOffsetsIterator<'a> { | ||||
|     text: &'a str, | ||||
|     chars: Peekable<CharIndices<'a>>, | ||||
| } | ||||
|  | ||||
| impl<'a> EscapeSequenceOffsetsIterator<'a> { | ||||
|     pub fn new(text: &'a str) -> EscapeSequenceOffsetsIterator<'a> { | ||||
|         return EscapeSequenceOffsetsIterator { | ||||
|             text, | ||||
|             chars: text.char_indices().peekable(), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /// Takes values from the iterator while the predicate returns true. | ||||
|     /// If the predicate returns false, that value is left. | ||||
|     fn chars_take_while(&mut self, pred: impl Fn(char) -> bool) -> Option<(usize, usize)> { | ||||
|         self.chars.peek()?; | ||||
|  | ||||
|         let start = self.chars.peek().unwrap().0; | ||||
|         let mut end: usize = start; | ||||
|         while let Some((i, c)) = self.chars.peek() { | ||||
|             if !pred(*c) { | ||||
|                 break; | ||||
|             } | ||||
|  | ||||
|             end = *i + c.len_utf8(); | ||||
|             self.chars.next(); | ||||
|         } | ||||
|  | ||||
|         Some((start, end)) | ||||
|     } | ||||
|  | ||||
|     fn next_text(&mut self) -> Option<EscapeSequenceOffsets> { | ||||
|         self.chars_take_while(|c| c != '\x1B') | ||||
|             .map(|(start, end)| EscapeSequenceOffsets::Text { start, end }) | ||||
|     } | ||||
|  | ||||
|     fn next_sequence(&mut self) -> Option<EscapeSequenceOffsets> { | ||||
|         let (start_sequence, c) = self.chars.next().expect("to not be finished"); | ||||
|         match self.chars.peek() { | ||||
|             None => Some(EscapeSequenceOffsets::Unknown { | ||||
|                 start: start_sequence, | ||||
|                 end: start_sequence + c.len_utf8(), | ||||
|             }), | ||||
|  | ||||
|             Some((_, ']')) => self.next_osc(start_sequence), | ||||
|             Some((_, '[')) => self.next_csi(start_sequence), | ||||
|             Some((i, c)) => match c { | ||||
|                 '\x20'..='\x2F' => self.next_nf(start_sequence), | ||||
|                 c => Some(EscapeSequenceOffsets::Unknown { | ||||
|                     start: start_sequence, | ||||
|                     end: i + c.len_utf8(), | ||||
|                 }), | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn next_osc(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> { | ||||
|         let (osc_open_index, osc_open_char) = self.chars.next().expect("to not be finished"); | ||||
|         debug_assert_eq!(osc_open_char, ']'); | ||||
|  | ||||
|         let mut start_terminator: usize; | ||||
|         let mut end_sequence: usize; | ||||
|  | ||||
|         loop { | ||||
|             match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B')) { | ||||
|                 None => { | ||||
|                     start_terminator = self.text.len(); | ||||
|                     end_sequence = start_terminator; | ||||
|                     break; | ||||
|                 } | ||||
|  | ||||
|                 Some((_, end)) => { | ||||
|                     start_terminator = end; | ||||
|                     end_sequence = end; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             match self.chars.next() { | ||||
|                 Some((ti, '\x07')) => { | ||||
|                     end_sequence = ti + '\x07'.len_utf8(); | ||||
|                     break; | ||||
|                 } | ||||
|  | ||||
|                 Some((ti, '\x1B')) => { | ||||
|                     match self.chars.next() { | ||||
|                         Some((i, '\\')) => { | ||||
|                             end_sequence = i + '\\'.len_utf8(); | ||||
|                             break; | ||||
|                         } | ||||
|  | ||||
|                         None => { | ||||
|                             end_sequence = ti + '\x1B'.len_utf8(); | ||||
|                             break; | ||||
|                         } | ||||
|  | ||||
|                         _ => { | ||||
|                             // Repeat, since `\\`(anything) isn't a valid ST. | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 None => { | ||||
|                     // Prematurely ends. | ||||
|                     break; | ||||
|                 } | ||||
|  | ||||
|                 Some((_, tc)) => { | ||||
|                     panic!("this should not be reached: char {tc:?}") | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Some(EscapeSequenceOffsets::OSC { | ||||
|             start_sequence, | ||||
|             start_command: osc_open_index + osc_open_char.len_utf8(), | ||||
|             start_terminator, | ||||
|             end: end_sequence, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn next_csi(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> { | ||||
|         let (csi_open_index, csi_open_char) = self.chars.next().expect("to not be finished"); | ||||
|         debug_assert_eq!(csi_open_char, '['); | ||||
|  | ||||
|         let start_parameters: usize = csi_open_index + csi_open_char.len_utf8(); | ||||
|  | ||||
|         // Keep iterating while within the range of `0x30-0x3F`. | ||||
|         let mut start_intermediates: usize = start_parameters; | ||||
|         if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x30'..='\x3F')) { | ||||
|             start_intermediates = end; | ||||
|         } | ||||
|  | ||||
|         // Keep iterating while within the range of `0x20-0x2F`. | ||||
|         let mut start_final_byte: usize = start_intermediates; | ||||
|         if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x20'..='\x2F')) { | ||||
|             start_final_byte = end; | ||||
|         } | ||||
|  | ||||
|         // Take the last char. | ||||
|         let end_of_sequence = match self.chars.next() { | ||||
|             None => start_final_byte, | ||||
|             Some((i, c)) => i + c.len_utf8(), | ||||
|         }; | ||||
|  | ||||
|         Some(EscapeSequenceOffsets::CSI { | ||||
|             start_sequence, | ||||
|             start_parameters, | ||||
|             start_intermediates, | ||||
|             start_final_byte, | ||||
|             end: end_of_sequence, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn next_nf(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> { | ||||
|         let (nf_open_index, nf_open_char) = self.chars.next().expect("to not be finished"); | ||||
|         debug_assert!(matches!(nf_open_char, '\x20'..='\x2F')); | ||||
|  | ||||
|         let start: usize = nf_open_index; | ||||
|         let mut end: usize = start; | ||||
|  | ||||
|         // Keep iterating while within the range of `0x20-0x2F`. | ||||
|         match self.chars_take_while(|c| matches!(c, '\x20'..='\x2F')) { | ||||
|             Some((_, i)) => end = i, | ||||
|             None => { | ||||
|                 return Some(EscapeSequenceOffsets::NF { | ||||
|                     start_sequence, | ||||
|                     start, | ||||
|                     end, | ||||
|                 }) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Get the final byte. | ||||
|         if let Some((i, c)) = self.chars.next() { | ||||
|             end = i + c.len_utf8() | ||||
|         } | ||||
|  | ||||
|         Some(EscapeSequenceOffsets::NF { | ||||
|             start_sequence, | ||||
|             start, | ||||
|             end, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<'a> Iterator for EscapeSequenceOffsetsIterator<'a> { | ||||
|     type Item = EscapeSequenceOffsets; | ||||
|     fn next(&mut self) -> Option<Self::Item> { | ||||
|         match self.chars.peek() { | ||||
|             Some((_, '\x1B')) => self.next_sequence(), | ||||
|             Some((_, _)) => self.next_text(), | ||||
|             None => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// An iterator over ANSI/VT escape sequences within a string. | ||||
| /// | ||||
| /// ## Example | ||||
| /// | ||||
| /// ```ignore | ||||
| /// let iter = EscapeSequenceIterator::new("\x1B[33mThis is yellow text.\x1B[m"); | ||||
| /// ``` | ||||
| pub struct EscapeSequenceIterator<'a> { | ||||
|     text: &'a str, | ||||
|     offset_iter: EscapeSequenceOffsetsIterator<'a>, | ||||
| } | ||||
|  | ||||
| impl<'a> EscapeSequenceIterator<'a> { | ||||
|     pub fn new(text: &'a str) -> EscapeSequenceIterator<'a> { | ||||
|         return EscapeSequenceIterator { | ||||
|             text, | ||||
|             offset_iter: EscapeSequenceOffsetsIterator::new(text), | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<'a> Iterator for EscapeSequenceIterator<'a> { | ||||
|     type Item = EscapeSequence<'a>; | ||||
|     fn next(&mut self) -> Option<Self::Item> { | ||||
|         use EscapeSequenceOffsets::*; | ||||
|         self.offset_iter.next().map(|offsets| match offsets { | ||||
|             Unknown { start, end } => EscapeSequence::Unknown(&self.text[start..end]), | ||||
|             Text { start, end } => EscapeSequence::Text(&self.text[start..end]), | ||||
|             NF { | ||||
|                 start_sequence, | ||||
|                 start, | ||||
|                 end, | ||||
|             } => EscapeSequence::NF { | ||||
|                 raw_sequence: &self.text[start_sequence..end], | ||||
|                 nf_sequence: &self.text[start..end], | ||||
|             }, | ||||
|             OSC { | ||||
|                 start_sequence, | ||||
|                 start_command, | ||||
|                 start_terminator, | ||||
|                 end, | ||||
|             } => EscapeSequence::OSC { | ||||
|                 raw_sequence: &self.text[start_sequence..end], | ||||
|                 command: &self.text[start_command..start_terminator], | ||||
|                 terminator: &self.text[start_terminator..end], | ||||
|             }, | ||||
|             CSI { | ||||
|                 start_sequence, | ||||
|                 start_parameters, | ||||
|                 start_intermediates, | ||||
|                 start_final_byte, | ||||
|                 end, | ||||
|             } => EscapeSequence::CSI { | ||||
|                 raw_sequence: &self.text[start_sequence..end], | ||||
|                 parameters: &self.text[start_parameters..start_intermediates], | ||||
|                 intermediates: &self.text[start_intermediates..start_final_byte], | ||||
|                 final_byte: &self.text[start_final_byte..end], | ||||
|             }, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// A parsed ANSI/VT100 escape sequence. | ||||
| #[derive(Debug, PartialEq)] | ||||
| pub enum EscapeSequence<'a> { | ||||
|     Text(&'a str), | ||||
|     Unknown(&'a str), | ||||
|     #[allow(clippy::upper_case_acronyms)] | ||||
|     NF { | ||||
|         raw_sequence: &'a str, | ||||
|         nf_sequence: &'a str, | ||||
|     }, | ||||
|     #[allow(clippy::upper_case_acronyms)] | ||||
|     OSC { | ||||
|         raw_sequence: &'a str, | ||||
|         command: &'a str, | ||||
|         terminator: &'a str, | ||||
|     }, | ||||
|     #[allow(clippy::upper_case_acronyms)] | ||||
|     CSI { | ||||
|         raw_sequence: &'a str, | ||||
|         parameters: &'a str, | ||||
|         intermediates: &'a str, | ||||
|         final_byte: &'a str, | ||||
|     }, | ||||
| } | ||||
|  | ||||
| impl<'a> EscapeSequence<'a> { | ||||
|     pub fn raw(&self) -> &'a str { | ||||
|         use EscapeSequence::*; | ||||
|         match *self { | ||||
|             Text(raw) => raw, | ||||
|             Unknown(raw) => raw, | ||||
|             NF { raw_sequence, .. } => raw_sequence, | ||||
|             OSC { raw_sequence, .. } => raw_sequence, | ||||
|             CSI { raw_sequence, .. } => raw_sequence, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use crate::vscreen::{ | ||||
|         EscapeSequence, EscapeSequenceIterator, EscapeSequenceOffsets, | ||||
|         EscapeSequenceOffsetsIterator, | ||||
|     }; | ||||
|  | ||||
|     #[test] | ||||
|     fn test_escape_sequence_offsets_iterator_parses_text() { | ||||
|         let mut iter = EscapeSequenceOffsetsIterator::new("text"); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::Text { start: 0, end: 4 }) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_escape_sequence_offsets_iterator_parses_text_stops_at_esc() { | ||||
|         let mut iter = EscapeSequenceOffsetsIterator::new("text\x1B[ming"); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::Text { start: 0, end: 4 }) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_escape_sequence_offsets_iterator_parses_osc_with_bel() { | ||||
|         let mut iter = EscapeSequenceOffsetsIterator::new("\x1B]abc\x07"); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::OSC { | ||||
|                 start_sequence: 0, | ||||
|                 start_command: 2, | ||||
|                 start_terminator: 5, | ||||
|                 end: 6, | ||||
|             }) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_escape_sequence_offsets_iterator_parses_osc_with_st() { | ||||
|         let mut iter = EscapeSequenceOffsetsIterator::new("\x1B]abc\x1B\\"); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::OSC { | ||||
|                 start_sequence: 0, | ||||
|                 start_command: 2, | ||||
|                 start_terminator: 5, | ||||
|                 end: 7, | ||||
|             }) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_escape_sequence_offsets_iterator_parses_osc_thats_broken() { | ||||
|         let mut iter = EscapeSequenceOffsetsIterator::new("\x1B]ab"); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::OSC { | ||||
|                 start_sequence: 0, | ||||
|                 start_command: 2, | ||||
|                 start_terminator: 4, | ||||
|                 end: 4, | ||||
|             }) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_escape_sequence_offsets_iterator_parses_csi() { | ||||
|         let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[m"); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::CSI { | ||||
|                 start_sequence: 0, | ||||
|                 start_parameters: 2, | ||||
|                 start_intermediates: 2, | ||||
|                 start_final_byte: 2, | ||||
|                 end: 3 | ||||
|             }) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_escape_sequence_offsets_iterator_parses_csi_with_parameters() { | ||||
|         let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1;34m"); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::CSI { | ||||
|                 start_sequence: 0, | ||||
|                 start_parameters: 2, | ||||
|                 start_intermediates: 6, | ||||
|                 start_final_byte: 6, | ||||
|                 end: 7 | ||||
|             }) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_escape_sequence_offsets_iterator_parses_csi_with_intermediates() { | ||||
|         let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[$m"); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::CSI { | ||||
|                 start_sequence: 0, | ||||
|                 start_parameters: 2, | ||||
|                 start_intermediates: 2, | ||||
|                 start_final_byte: 3, | ||||
|                 end: 4 | ||||
|             }) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_escape_sequence_offsets_iterator_parses_csi_with_parameters_and_intermediates() { | ||||
|         let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1$m"); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::CSI { | ||||
|                 start_sequence: 0, | ||||
|                 start_parameters: 2, | ||||
|                 start_intermediates: 3, | ||||
|                 start_final_byte: 4, | ||||
|                 end: 5 | ||||
|             }) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_escape_sequence_offsets_iterator_parses_csi_thats_broken() { | ||||
|         let mut iter = EscapeSequenceOffsetsIterator::new("\x1B["); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::CSI { | ||||
|                 start_sequence: 0, | ||||
|                 start_parameters: 2, | ||||
|                 start_intermediates: 2, | ||||
|                 start_final_byte: 2, | ||||
|                 end: 2 | ||||
|             }) | ||||
|         ); | ||||
|  | ||||
|         let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1"); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::CSI { | ||||
|                 start_sequence: 0, | ||||
|                 start_parameters: 2, | ||||
|                 start_intermediates: 3, | ||||
|                 start_final_byte: 3, | ||||
|                 end: 3 | ||||
|             }) | ||||
|         ); | ||||
|  | ||||
|         let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1$"); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::CSI { | ||||
|                 start_sequence: 0, | ||||
|                 start_parameters: 2, | ||||
|                 start_intermediates: 3, | ||||
|                 start_final_byte: 4, | ||||
|                 end: 4 | ||||
|             }) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_escape_sequence_offsets_iterator_parses_nf() { | ||||
|         let mut iter = EscapeSequenceOffsetsIterator::new("\x1B($0"); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::NF { | ||||
|                 start_sequence: 0, | ||||
|                 start: 1, | ||||
|                 end: 4 | ||||
|             }) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_escape_sequence_offsets_iterator_parses_nf_thats_broken() { | ||||
|         let mut iter = EscapeSequenceOffsetsIterator::new("\x1B("); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::NF { | ||||
|                 start_sequence: 0, | ||||
|                 start: 1, | ||||
|                 end: 1 | ||||
|             }) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_escape_sequence_offsets_iterator_iterates() { | ||||
|         let mut iter = EscapeSequenceOffsetsIterator::new("text\x1B[33m\x1B]OSC\x07\x1B(0"); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::Text { start: 0, end: 4 }) | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::CSI { | ||||
|                 start_sequence: 4, | ||||
|                 start_parameters: 6, | ||||
|                 start_intermediates: 8, | ||||
|                 start_final_byte: 8, | ||||
|                 end: 9 | ||||
|             }) | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::OSC { | ||||
|                 start_sequence: 9, | ||||
|                 start_command: 11, | ||||
|                 start_terminator: 14, | ||||
|                 end: 15 | ||||
|             }) | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequenceOffsets::NF { | ||||
|                 start_sequence: 15, | ||||
|                 start: 16, | ||||
|                 end: 18 | ||||
|             }) | ||||
|         ); | ||||
|         assert_eq!(iter.next(), None); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_escape_sequence_iterator_iterates() { | ||||
|         let mut iter = EscapeSequenceIterator::new("text\x1B[33m\x1B]OSC\x07\x1B]OSC\x1B\\\x1B(0"); | ||||
|         assert_eq!(iter.next(), Some(EscapeSequence::Text("text"))); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequence::CSI { | ||||
|                 raw_sequence: "\x1B[33m", | ||||
|                 parameters: "33", | ||||
|                 intermediates: "", | ||||
|                 final_byte: "m", | ||||
|             }) | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequence::OSC { | ||||
|                 raw_sequence: "\x1B]OSC\x07", | ||||
|                 command: "OSC", | ||||
|                 terminator: "\x07", | ||||
|             }) | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequence::OSC { | ||||
|                 raw_sequence: "\x1B]OSC\x1B\\", | ||||
|                 command: "OSC", | ||||
|                 terminator: "\x1B\\", | ||||
|             }) | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             iter.next(), | ||||
|             Some(EscapeSequence::NF { | ||||
|                 raw_sequence: "\x1B(0", | ||||
|                 nf_sequence: "(0", | ||||
|             }) | ||||
|         ); | ||||
|         assert_eq!(iter.next(), None); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_sgr_attributes_do_not_leak_into_wrong_field() { | ||||
|         let mut attrs = crate::vscreen::Attributes::new(); | ||||
|  | ||||
|         // Bold, Dim, Italic, Underline, Foreground, Background | ||||
|         attrs.update(EscapeSequence::CSI { | ||||
|             raw_sequence: "\x1B[1;2;3;4;31;41m", | ||||
|             parameters: "1;2;3;4;31;41", | ||||
|             intermediates: "", | ||||
|             final_byte: "m", | ||||
|         }); | ||||
|  | ||||
|         assert_eq!(attrs.bold, "\x1B[1m"); | ||||
|         assert_eq!(attrs.dim, "\x1B[2m"); | ||||
|         assert_eq!(attrs.italic, "\x1B[3m"); | ||||
|         assert_eq!(attrs.underline, "\x1B[4m"); | ||||
|         assert_eq!(attrs.foreground, "\x1B[31m"); | ||||
|         assert_eq!(attrs.background, "\x1B[41m"); | ||||
|  | ||||
|         // Bold, Bright Foreground, Bright Background | ||||
|         attrs.sgr_reset(); | ||||
|         attrs.update(EscapeSequence::CSI { | ||||
|             raw_sequence: "\x1B[1;94;103m", | ||||
|             parameters: "1;94;103", | ||||
|             intermediates: "", | ||||
|             final_byte: "m", | ||||
|         }); | ||||
|  | ||||
|         assert_eq!(attrs.bold, "\x1B[1m"); | ||||
|         assert_eq!(attrs.foreground, "\x1B[94m"); | ||||
|         assert_eq!(attrs.background, "\x1B[103m"); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										62
									
								
								tests/benchmarks/run-benchmarks.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										62
									
								
								tests/benchmarks/run-benchmarks.sh
									
									
									
									
										vendored
									
									
								
							| @@ -9,6 +9,13 @@ if ! command -v hyperfine > /dev/null 2>&1; then | ||||
| 	exit 1 | ||||
| fi | ||||
|  | ||||
| # Check that jq is installed. | ||||
| if ! command -v jq > /dev/null 2>&1; then | ||||
| 	echo "'jq' does not seem to be installed." | ||||
| 	echo "You can get it here: https://jqlang.github.io/jq/download/" | ||||
| 	exit 1 | ||||
| fi | ||||
|  | ||||
| # Check that python3 is installed. | ||||
| if ! command -v python3 > /dev/null 2>&1; then | ||||
| 	echo "'python3' does not seem to be installed." | ||||
| @@ -49,7 +56,8 @@ REPORT="$RESULT_DIR/report.md" | ||||
| TARGET_DIR="$(get_cargo_target_dir)" | ||||
| TARGET_RELEASE="${TARGET_DIR}/release/bat" | ||||
|  | ||||
| WARMUP_COUNT=3 | ||||
| : ${WARMUP_COUNT:=3} | ||||
| : ${RUN_COUNT:=10} | ||||
|  | ||||
| # Determine which target to benchmark. | ||||
| BAT='' | ||||
| @@ -88,16 +96,28 @@ hyperfine \ | ||||
| 	"$(printf "%q" "$BAT") --no-config" \ | ||||
| 	--command-name "bat" \ | ||||
| 	--warmup "$WARMUP_COUNT" \ | ||||
|     --runs "$RUN_COUNT" \ | ||||
|     --export-markdown "$RESULT_DIR/startup-time.md" \ | ||||
|     --export-json "$RESULT_DIR/startup-time.json" | ||||
| cat "$RESULT_DIR/startup-time.md" >> "$REPORT" | ||||
|  | ||||
|  | ||||
| heading "Startup time without syntax highlighting" | ||||
| hyperfine \ | ||||
| 	"$(printf "%q" "$BAT") --no-config startup-time-src/small-CpuInfo-file.cpuinfo" \ | ||||
| 	--command-name "bat … small-CpuInfo-file.cpuinfo" \ | ||||
| 	--warmup "$WARMUP_COUNT" \ | ||||
|     --runs "$RUN_COUNT" \ | ||||
|     --export-markdown "$RESULT_DIR/startup-time-without-syntax-highlighting.md" \ | ||||
|     --export-json "$RESULT_DIR/startup-time-without-syntax-highlighting.json" | ||||
| cat "$RESULT_DIR/startup-time-without-syntax-highlighting.md" >> "$REPORT" | ||||
|  | ||||
| heading "Startup time with syntax highlighting" | ||||
| hyperfine \ | ||||
| 	"$(printf "%q" "$BAT") --no-config --color=always startup-time-src/small-CpuInfo-file.cpuinfo" \ | ||||
| 	--command-name "bat … small-CpuInfo-file.cpuinfo" \ | ||||
| 	--command-name "bat … --color=always small-CpuInfo-file.cpuinfo" \ | ||||
| 	--warmup "$WARMUP_COUNT" \ | ||||
|     --runs "$RUN_COUNT" \ | ||||
|     --export-markdown "$RESULT_DIR/startup-time-with-syntax-highlighting.md" \ | ||||
|     --export-json "$RESULT_DIR/startup-time-with-syntax-highlighting.json" | ||||
| cat "$RESULT_DIR/startup-time-with-syntax-highlighting.md" >> "$REPORT" | ||||
| @@ -108,16 +128,52 @@ hyperfine \ | ||||
| 	"$(printf "%q" "$BAT") --no-config --color=always startup-time-src/small-Markdown-file.md" \ | ||||
| 	--command-name "bat … small-Markdown-file.md" \ | ||||
| 	--warmup "$WARMUP_COUNT" \ | ||||
|     --runs "$RUN_COUNT" \ | ||||
|     --export-markdown "$RESULT_DIR/startup-time-with-syntax-with-dependencies.md" \ | ||||
|     --export-json "$RESULT_DIR/startup-time-with-syntax-with-dependencies.json" | ||||
| cat "$RESULT_DIR/startup-time-with-syntax-with-dependencies.md" >> "$REPORT" | ||||
|  | ||||
|  | ||||
| heading "Startup time with indeterminant syntax" | ||||
| hyperfine \ | ||||
| 	"$(printf "%q" "$BAT") --no-config --color=always startup-time-src/mystery-file" \ | ||||
| 	--shell none \ | ||||
| 	--command-name 'bat … mystery-file' \ | ||||
| 	--warmup "$WARMUP_COUNT" \ | ||||
|     --runs "$RUN_COUNT" \ | ||||
|     --export-markdown "$RESULT_DIR/startup-time-with-indeterminant-syntax.md" \ | ||||
|     --export-json "$RESULT_DIR/startup-time-with-indeterminant-syntax.json" | ||||
| cat "$RESULT_DIR/startup-time-with-indeterminant-syntax.md" >> "$REPORT" | ||||
|  | ||||
| heading "Startup time with manually set syntax" | ||||
| hyperfine \ | ||||
| 	"$(printf "%q" "$BAT") --no-config --color=always --language=Dockerfile startup-time-src/mystery-file" \ | ||||
| 	--shell none \ | ||||
| 	--command-name 'bat … --language=Dockerfile mystery-file' \ | ||||
| 	--warmup "$WARMUP_COUNT" \ | ||||
|     --runs "$RUN_COUNT" \ | ||||
|     --export-markdown "$RESULT_DIR/startup-time-with-manually-set-syntax.md" \ | ||||
|     --export-json "$RESULT_DIR/startup-time-with-manually-set-syntax.json" | ||||
| cat "$RESULT_DIR/startup-time-with-manually-set-syntax.md" >> "$REPORT" | ||||
|  | ||||
| heading "Startup time with mapped syntax" | ||||
| hyperfine \ | ||||
| 	"$(printf "%q" "$BAT") --no-config --color=always startup-time-src/Containerfile" \ | ||||
| 	--shell none \ | ||||
| 	--command-name 'bat … Containerfile' \ | ||||
| 	--warmup "$WARMUP_COUNT" \ | ||||
|     --runs "$RUN_COUNT" \ | ||||
|     --export-markdown "$RESULT_DIR/startup-time-with-mapped-syntax.md" \ | ||||
|     --export-json "$RESULT_DIR/startup-time-with-mapped-syntax.json" | ||||
| cat "$RESULT_DIR/startup-time-with-mapped-syntax.md" >> "$REPORT" | ||||
|  | ||||
|  | ||||
| heading "Plain-text speed" | ||||
| hyperfine \ | ||||
| 	"$(printf "%q" "$BAT") --no-config --language=txt --style=plain highlighting-speed-src/numpy_test_multiarray.py" \ | ||||
| 	--command-name 'bat … --language=txt numpy_test_multiarray.py' \ | ||||
| 	--warmup "$WARMUP_COUNT" \ | ||||
|     --runs "$RUN_COUNT" \ | ||||
|     --export-markdown "$RESULT_DIR/plain-text-speed.md" \ | ||||
|     --export-json "$RESULT_DIR/plain-text-speed.json" | ||||
| cat "$RESULT_DIR/plain-text-speed.md" >> "$REPORT" | ||||
| @@ -129,6 +185,7 @@ for wrap in character never; do | ||||
|  | ||||
| 		heading "Syntax highlighting speed --wrap=${wrap}: \`$filename\`" | ||||
| 		hyperfine --warmup "$WARMUP_COUNT" \ | ||||
|     		--runs "$RUN_COUNT" \ | ||||
| 			"$(printf "%q" "$BAT") --no-config --style=full --color=always --wrap=${wrap} --terminal-width=80 '$SRC'" \ | ||||
| 			--command-name "bat … ${filename}" \ | ||||
| 			--export-markdown "$RESULT_DIR/syntax-highlighting-speed-${filename}.md" \ | ||||
| @@ -143,6 +200,7 @@ hyperfine \ | ||||
| 	"$(printf "%q" "$BAT") --no-config --language=txt --style=plain many-small-files/*.txt" \ | ||||
| 	--command-name 'bat … --language=txt *.txt' \ | ||||
| 	--warmup "$WARMUP_COUNT" \ | ||||
|     --runs "$RUN_COUNT" \ | ||||
|     --export-markdown "$RESULT_DIR/many-small-files-speed.md" \ | ||||
|     --export-json "$RESULT_DIR/many-small-files-speed.json" | ||||
| cat "$RESULT_DIR/many-small-files-speed.md" >> "$REPORT" | ||||
|   | ||||
							
								
								
									
										3
									
								
								tests/benchmarks/startup-time-src/Containerfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								tests/benchmarks/startup-time-src/Containerfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| FROM docker.io/alpine:latest | ||||
| COPY foo /root/bar | ||||
| RUN sleep 60 | ||||
							
								
								
									
										3
									
								
								tests/benchmarks/startup-time-src/mystery-file
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								tests/benchmarks/startup-time-src/mystery-file
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| FROM docker.io/alpine:latest | ||||
| COPY foo /root/bar | ||||
| RUN sleep 60 | ||||
							
								
								
									
										
											BIN
										
									
								
								tests/examples/control_characters.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tests/examples/control_characters.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										30
									
								
								tests/examples/empty_lines.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								tests/examples/empty_lines.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| line 1 | ||||
|  | ||||
|  | ||||
|  | ||||
| line 5 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| line 20 | ||||
| line 21 | ||||
|  | ||||
|  | ||||
| line 24 | ||||
|  | ||||
| line 26 | ||||
|  | ||||
|  | ||||
|  | ||||
| line 30 | ||||
							
								
								
									
										1
									
								
								tests/examples/regression_tests/issue_2541.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/examples/regression_tests/issue_2541.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| ]8;;http://example.com\This is a link]8;;\n | ||||
| @@ -0,0 +1 @@ | ||||
| The header is not broken | ||||
							
								
								
									
										53
									
								
								tests/github-actions.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								tests/github-actions.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| #[test] | ||||
| fn all_jobs_not_missing_any_jobs() { | ||||
|     let yaml: serde_yaml::Value = | ||||
|         serde_yaml::from_reader(std::fs::File::open(".github/workflows/CICD.yml").unwrap()) | ||||
|             .unwrap(); | ||||
|     let jobs = yaml.get("jobs").unwrap(); | ||||
|  | ||||
|     // Get all jobs that all-jobs depends on: | ||||
|     // | ||||
|     //   jobs: | ||||
|     //     all-jobs: | ||||
|     //       needs: | ||||
|     //         - this | ||||
|     //         - list | ||||
|     //         - ... | ||||
|     let actual = jobs | ||||
|         .get("all-jobs") | ||||
|         .unwrap() | ||||
|         .get("needs") | ||||
|         .unwrap() | ||||
|         .as_sequence() | ||||
|         .unwrap(); | ||||
|  | ||||
|     // Get all jobs used in CI, except the ones we want to ignore: | ||||
|     // | ||||
|     //   jobs: | ||||
|     //     this: ... | ||||
|     //     list: ... | ||||
|     //     ... | ||||
|     let exceptions = [ | ||||
|         "all-jobs", // 'all-jobs' should not reference itself | ||||
|         "winget",   // only used when publishing a release | ||||
|     ]; | ||||
|     let expected = jobs | ||||
|         .as_mapping() | ||||
|         .unwrap() | ||||
|         .keys() | ||||
|         .filter_map(|k| { | ||||
|             if exceptions.contains(&k.as_str().unwrap_or_default()) { | ||||
|                 None | ||||
|             } else { | ||||
|                 Some(k) | ||||
|             } | ||||
|         }) | ||||
|         .map(ToOwned::to_owned) | ||||
|         .collect::<Vec<_>>(); | ||||
|  | ||||
|     // Make sure they match | ||||
|     assert_eq!( | ||||
|         *actual, expected, | ||||
|         "`all-jobs` should depend on all other jobs" | ||||
|     ); | ||||
| } | ||||
| @@ -208,6 +208,70 @@ fn line_range_multiple() { | ||||
|         .stdout("line 1\nline 2\nline 4\n"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn squeeze_blank() { | ||||
|     bat() | ||||
|         .arg("empty_lines.txt") | ||||
|         .arg("--squeeze-blank") | ||||
|         .assert() | ||||
|         .success() | ||||
|         .stdout("line 1\n\nline 5\n\nline 20\nline 21\n\nline 24\n\nline 26\n\nline 30\n"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn squeeze_blank_line_numbers() { | ||||
|     bat() | ||||
|         .arg("empty_lines.txt") | ||||
|         .arg("--squeeze-blank") | ||||
|         .arg("--decorations=always") | ||||
|         .arg("--number") | ||||
|         .assert() | ||||
|         .success() | ||||
|         .stdout("   1 line 1\n   2 \n   5 line 5\n   6 \n  20 line 20\n  21 line 21\n  22 \n  24 line 24\n  25 \n  26 line 26\n  27 \n  30 line 30\n"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn squeeze_limit() { | ||||
|     bat() | ||||
|         .arg("empty_lines.txt") | ||||
|         .arg("--squeeze-blank") | ||||
|         .arg("--squeeze-limit=2") | ||||
|         .assert() | ||||
|         .success() | ||||
|         .stdout("line 1\n\n\nline 5\n\n\nline 20\nline 21\n\n\nline 24\n\nline 26\n\n\nline 30\n"); | ||||
|  | ||||
|     bat() | ||||
|         .arg("empty_lines.txt") | ||||
|         .arg("--squeeze-blank") | ||||
|         .arg("--squeeze-limit=5") | ||||
|         .assert() | ||||
|         .success() | ||||
|         .stdout("line 1\n\n\n\nline 5\n\n\n\n\n\nline 20\nline 21\n\n\nline 24\n\nline 26\n\n\n\nline 30\n"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn squeeze_limit_line_numbers() { | ||||
|     bat() | ||||
|         .arg("empty_lines.txt") | ||||
|         .arg("--squeeze-blank") | ||||
|         .arg("--squeeze-limit=2") | ||||
|         .arg("--decorations=always") | ||||
|         .arg("--number") | ||||
|         .assert() | ||||
|         .success() | ||||
|         .stdout("   1 line 1\n   2 \n   3 \n   5 line 5\n   6 \n   7 \n  20 line 20\n  21 line 21\n  22 \n  23 \n  24 line 24\n  25 \n  26 line 26\n  27 \n  28 \n  30 line 30\n"); | ||||
|  | ||||
|     bat() | ||||
|         .arg("empty_lines.txt") | ||||
|         .arg("--squeeze-blank") | ||||
|         .arg("--squeeze-limit=5") | ||||
|         .arg("--decorations=always") | ||||
|         .arg("--number") | ||||
|         .assert() | ||||
|         .success() | ||||
|         .stdout("   1 line 1\n   2 \n   3 \n   4 \n   5 line 5\n   6 \n   7 \n   8 \n   9 \n  10 \n  20 line 20\n  21 line 21\n  22 \n  23 \n  24 line 24\n  25 \n  26 line 26\n  27 \n  28 \n  29 \n  30 line 30\n"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| #[cfg_attr(any(not(feature = "git"), target_os = "windows"), ignore)] | ||||
| fn short_help() { | ||||
| @@ -936,6 +1000,18 @@ fn env_var_bat_paging() { | ||||
|     }); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn basic_set_terminal_title() { | ||||
|     bat() | ||||
|         .arg("--paging=always") | ||||
|         .arg("--set-terminal-title") | ||||
|         .arg("test.txt") | ||||
|         .assert() | ||||
|         .success() | ||||
|         .stdout("\u{1b}]0;bat: test.txt\x07hello world\n") | ||||
|         .stderr(""); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn diagnostic_sanity_check() { | ||||
|     bat() | ||||
| @@ -1163,6 +1239,20 @@ fn bom_stripped_when_no_color_and_not_loop_through() { | ||||
|         ); | ||||
| } | ||||
|  | ||||
| // Regression test for https://github.com/sharkdp/bat/issues/2541 | ||||
| #[test] | ||||
| fn no_broken_osc_emit_with_line_wrapping() { | ||||
|     bat() | ||||
|         .arg("--color=always") | ||||
|         .arg("--decorations=never") | ||||
|         .arg("--wrap=character") | ||||
|         .arg("--terminal-width=40") | ||||
|         .arg("regression_tests/issue_2541.txt") | ||||
|         .assert() | ||||
|         .success() | ||||
|         .stdout(predicate::function(|s: &str| s.lines().count() == 1)); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn can_print_file_named_cache() { | ||||
|     bat_with_config() | ||||
| @@ -1381,6 +1471,61 @@ fn header_full_binary() { | ||||
|         .stderr(""); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| #[cfg(not(feature = "git"))] | ||||
| fn header_narrow_terminal() { | ||||
|     bat() | ||||
|         .arg("--terminal-width=30") | ||||
|         .arg("--decorations=always") | ||||
|         .arg("this-file-path-is-really-long-and-would-have-broken-the-layout-of-the-header.txt") | ||||
|         .assert() | ||||
|         .success() | ||||
|         .stdout( | ||||
|             "\ | ||||
| ─────┬──────────────────────── | ||||
|      │ File: this-file-path-is | ||||
|      │ -really-long-and-would- | ||||
|      │ have-broken-the-layout- | ||||
|      │ of-the-header.txt | ||||
| ─────┼──────────────────────── | ||||
|    1 │ The header is not broke | ||||
|      │ n | ||||
| ─────┴──────────────────────── | ||||
| ", | ||||
|         ) | ||||
|         .stderr(""); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn header_very_narrow_terminal() { | ||||
|     bat() | ||||
|         .arg("--terminal-width=10") | ||||
|         .arg("--decorations=always") | ||||
|         .arg("this-file-path-is-really-long-and-would-have-broken-the-layout-of-the-header.txt") | ||||
|         .assert() | ||||
|         .success() | ||||
|         .stdout( | ||||
|             "\ | ||||
| ────────── | ||||
| File: this | ||||
| -file-path | ||||
| -is-really | ||||
| -long-and- | ||||
| would-have | ||||
| -broken-th | ||||
| e-layout-o | ||||
| f-the-head | ||||
| er.txt | ||||
| ────────── | ||||
| The header | ||||
|  is not br | ||||
| oken | ||||
| ────────── | ||||
| ", | ||||
|         ) | ||||
|         .stderr(""); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| #[cfg(feature = "git")] // Expected output assumes git is enabled | ||||
| fn header_default() { | ||||
| @@ -1613,7 +1758,7 @@ fn do_not_panic_regression_tests() { | ||||
|     ] { | ||||
|         bat() | ||||
|             .arg("--color=always") | ||||
|             .arg(&format!("regression_tests/{}", filename)) | ||||
|             .arg(&format!("regression_tests/{filename}")) | ||||
|             .assert() | ||||
|             .success(); | ||||
|     } | ||||
| @@ -1626,7 +1771,7 @@ fn do_not_detect_different_syntax_for_stdin_and_files() { | ||||
|     let cmd_for_file = bat() | ||||
|         .arg("--color=always") | ||||
|         .arg("--map-syntax=*.js:Markdown") | ||||
|         .arg(&format!("--file-name={}", file)) | ||||
|         .arg(&format!("--file-name={file}")) | ||||
|         .arg("--style=plain") | ||||
|         .arg(file) | ||||
|         .assert() | ||||
| @@ -1636,7 +1781,7 @@ fn do_not_detect_different_syntax_for_stdin_and_files() { | ||||
|         .arg("--color=always") | ||||
|         .arg("--map-syntax=*.js:Markdown") | ||||
|         .arg("--style=plain") | ||||
|         .arg(&format!("--file-name={}", file)) | ||||
|         .arg(&format!("--file-name={file}")) | ||||
|         .pipe_stdin(Path::new(EXAMPLES_DIR).join(file)) | ||||
|         .unwrap() | ||||
|         .assert() | ||||
| @@ -1655,7 +1800,7 @@ fn no_first_line_fallback_when_mapping_to_invalid_syntax() { | ||||
|     bat() | ||||
|         .arg("--color=always") | ||||
|         .arg("--map-syntax=*.invalid-syntax:InvalidSyntax") | ||||
|         .arg(&format!("--file-name={}", file)) | ||||
|         .arg(&format!("--file-name={file}")) | ||||
|         .arg("--style=plain") | ||||
|         .arg(file) | ||||
|         .assert() | ||||
| @@ -1728,6 +1873,25 @@ fn show_all_with_caret_notation() { | ||||
|         .assert() | ||||
|         .stdout("hello·world^J\n├──┤^M^@^G^H^[") | ||||
|         .stderr(""); | ||||
|  | ||||
|     bat() | ||||
|         .arg("--show-all") | ||||
|         .arg("--nonprintable-notation=caret") | ||||
|         .arg("control_characters.txt") | ||||
|         .assert() | ||||
|         .stdout("^@^A^B^C^D^E^F^G^H├─┤^J\n^K^L^M^N^O^P^Q^R^S^T^U^V^W^X^Y^Z^[^\\^]^^^_^?") | ||||
|         .stderr(""); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn show_all_with_unicode() { | ||||
|     bat() | ||||
|         .arg("--show-all") | ||||
|         .arg("--nonprintable-notation=unicode") | ||||
|         .arg("control_characters.txt") | ||||
|         .assert() | ||||
|         .stdout("␀␁␂␃␄␅␆␇␈├─┤␊\n␋␌␍␎␏␐␑␒␓␔␕␖␗␘␙␚␛␜␝␞␟␡") | ||||
|         .stderr(""); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| @@ -1834,7 +1998,7 @@ fn ansi_passthrough_emit() { | ||||
|             .arg("--paging=never") | ||||
|             .arg("--color=never") | ||||
|             .arg("--terminal-width=80") | ||||
|             .arg(format!("--wrap={}", wrapping)) | ||||
|             .arg(format!("--wrap={wrapping}")) | ||||
|             .arg("--decorations=always") | ||||
|             .arg("--style=plain") | ||||
|             .write_stdin("\x1B[33mColor\nColor \x1B[m\nPlain\n") | ||||
| @@ -1845,6 +2009,62 @@ fn ansi_passthrough_emit() { | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Ensure that a simple ANSI sequence passthrough is emitted properly on wrapped lines. | ||||
| // This also helps ensure that escape sequences are counted as part of the visible characters when wrapping. | ||||
| #[test] | ||||
| fn ansi_sgr_emitted_when_wrapped() { | ||||
|     bat() | ||||
|         .arg("--paging=never") | ||||
|         .arg("--color=never") | ||||
|         .arg("--terminal-width=20") | ||||
|         .arg("--wrap=character") | ||||
|         .arg("--decorations=always") | ||||
|         .arg("--style=plain") | ||||
|         .write_stdin("\x1B[33mColor...............Also color.\n") | ||||
|         .assert() | ||||
|         .success() | ||||
|         .stdout("\x1B[33m\x1B[33mColor...............\n\x1B[33mAlso color.\n") | ||||
|         // FIXME:              ~~~~~~~~ should not be emitted twice. | ||||
|         .stderr(""); | ||||
| } | ||||
|  | ||||
| // Ensure that a simple ANSI sequence passthrough is emitted properly on wrapped lines. | ||||
| // This also helps ensure that escape sequences are counted as part of the visible characters when wrapping. | ||||
| #[test] | ||||
| fn ansi_hyperlink_emitted_when_wrapped() { | ||||
|     bat() | ||||
|         .arg("--paging=never") | ||||
|         .arg("--color=never") | ||||
|         .arg("--terminal-width=20") | ||||
|         .arg("--wrap=character") | ||||
|         .arg("--decorations=always") | ||||
|         .arg("--style=plain") | ||||
|         .write_stdin("\x1B]8;;http://example.com/\x1B\\Hyperlinks..........Wrap across lines.\n") | ||||
|         .assert() | ||||
|         .success() | ||||
|         .stdout("\x1B]8;;http://example.com/\x1B\\\x1B]8;;http://example.com/\x1B\\Hyperlinks..........\x1B]8;;\x1B\\\n\x1B]8;;http://example.com/\x1B\\Wrap across lines.\n") | ||||
|         // FIXME:                                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ should not be emitted twice. | ||||
|         .stderr(""); | ||||
| } | ||||
|  | ||||
| // Ensure that multiple ANSI sequence SGR attributes are combined when emitted on wrapped lines. | ||||
| #[test] | ||||
| fn ansi_sgr_joins_attributes_when_wrapped() { | ||||
|     bat() | ||||
|             .arg("--paging=never") | ||||
|             .arg("--color=never") | ||||
|             .arg("--terminal-width=20") | ||||
|             .arg("--wrap=character") | ||||
|             .arg("--decorations=always") | ||||
|             .arg("--style=plain") | ||||
|             .write_stdin("\x1B[33mColor. \x1B[1mBold.........Also bold and color.\n") | ||||
|             .assert() | ||||
|             .success() | ||||
|             .stdout("\x1B[33m\x1B[33mColor. \x1B[1m\x1B[33m\x1B[1mBold.........\n\x1B[33m\x1B[1mAlso bold and color.\n") | ||||
|             // FIXME:              ~~~~~~~~       ~~~~~~~~~~~~~~~ should not be emitted twice. | ||||
|             .stderr(""); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn ignored_suffix_arg() { | ||||
|     bat() | ||||
|   | ||||
| @@ -30,8 +30,7 @@ fn no_duplicate_extensions() { | ||||
|         for extension in &syntax.file_extensions { | ||||
|             assert!( | ||||
|                 KNOWN_EXCEPTIONS.contains(&extension.as_str()) || extensions.insert(extension), | ||||
|                 "File extension / pattern \"{}\" appears twice in the syntax set", | ||||
|                 extension | ||||
|                 "File extension / pattern \"{extension}\" appears twice in the syntax set" | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|   | ||||
							
								
								
									
										32
									
								
								tests/syntax-tests/highlighted/JQ/sample.jq
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										32
									
								
								tests/syntax-tests/highlighted/JQ/sample.jq
									
									
									
									
										vendored
									
									
								
							| @@ -1,31 +1,31 @@ | ||||
| [38;2;249;38;114mimport[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116m../imported-file[0m[38;2;230;219;116m"[0m[38;2;248;248;242m [0m[38;2;248;248;242m;[0m | ||||
|  | ||||
| [38;2;117;113;94m#[0m[38;2;117;113;94m With Comments ![0m | ||||
| [38;2;249;38;114mdef[0m[38;2;248;248;242m [0m[38;2;166;226;46mweird[0m[38;2;248;248;242m([0m[3;38;2;253;151;31m$a[0m[38;2;248;248;242m; [0m[3;38;2;253;151;31m$b[0m[38;2;248;248;242m; [0m[3;38;2;253;151;31m$c[0m[38;2;248;248;242m)[0m[38;2;248;248;242m:[0m | ||||
| [38;2;248;248;242m	[0m[38;2;248;248;242m[ [0m[38;2;255;255;255m$a[0m[38;2;248;248;242m, [0m[38;2;255;255;255m$b[0m[38;2;248;248;242m, [0m[38;2;255;255;255m$c[0m[38;2;248;248;242m ][0m[38;2;248;248;242m | [0m[38;2;102;217;239mtranspose[0m[38;2;248;248;242m | [0m[38;2;249;38;114mreduce[0m[38;2;248;248;242m .[0m[38;2;248;248;242m[][0m[38;2;248;248;242m[][0m[38;2;248;248;242m [0m[38;2;249;38;114mas[0m[38;2;248;248;242m [0m[38;2;255;255;255m$item[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m | ||||
| [38;2;248;248;242m		[0m[38;2;248;248;242m[][0m[38;2;248;248;242m;[0m | ||||
| [38;2;248;248;242m		. + [0m[38;2;255;255;255m$item[0m[38;2;248;248;242m.property[0m | ||||
| [38;2;248;248;242m	)[0m | ||||
| [38;2;249;38;114mdef[0m[38;2;248;248;242m [0m[38;2;166;226;46mweird[0m[38;2;248;248;242m([0m[3;38;2;253;151;31m$a[0m[38;2;248;248;242m;[0m[38;2;248;248;242m [0m[3;38;2;253;151;31m$b[0m[38;2;248;248;242m;[0m[38;2;248;248;242m [0m[3;38;2;253;151;31m$c[0m[38;2;248;248;242m)[0m[38;2;248;248;242m:[0m | ||||
| [38;2;248;248;242m	[0m[38;2;248;248;242m[[0m[38;2;248;248;242m [0m[38;2;255;255;255m$[0m[38;2;255;255;255ma[0m[38;2;248;248;242m, [0m[38;2;255;255;255m$[0m[38;2;255;255;255mb[0m[38;2;248;248;242m, [0m[38;2;255;255;255m$[0m[38;2;255;255;255mc[0m[38;2;248;248;242m [0m[38;2;248;248;242m][0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m[38;2;248;248;242m [0m[38;2;102;217;239mtranspose[0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m[38;2;248;248;242m [0m[38;2;249;38;114mreduce[0m[38;2;248;248;242m [0m[38;2;248;248;242m.[0m[38;2;248;248;242m[[0m[38;2;248;248;242m][0m[38;2;248;248;242m[[0m[38;2;248;248;242m][0m[38;2;248;248;242m [0m[38;2;249;38;114mas[0m[38;2;248;248;242m [0m[38;2;255;255;255m$[0m[38;2;255;255;255mitem[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m | ||||
| [38;2;248;248;242m		[0m[38;2;248;248;242m[[0m[38;2;248;248;242m][0m[38;2;248;248;242m;[0m | ||||
| [38;2;248;248;242m		[0m[38;2;248;248;242m.[0m[38;2;248;248;242m [0m[38;2;249;38;114m+[0m[38;2;248;248;242m [0m[38;2;255;255;255m$[0m[38;2;255;255;255mitem[0m[38;2;248;248;242m.[0m[38;2;248;248;242mproperty[0m | ||||
| [38;2;248;248;242m	[0m[38;2;248;248;242m)[0m | ||||
| [38;2;248;248;242m;[0m | ||||
|  | ||||
| [38;2;248;248;242m. | weird [0m[38;2;248;248;242m(.a; .b; .c)[0m[38;2;248;248;242m |[0m | ||||
| [38;2;248;248;242m.[0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m[38;2;248;248;242m weird [0m[38;2;248;248;242m([0m[38;2;248;248;242m.[0m[38;2;248;248;242ma[0m[38;2;248;248;242m;[0m[38;2;248;248;242m [0m[38;2;248;248;242m.[0m[38;2;248;248;242mb[0m[38;2;248;248;242m;[0m[38;2;248;248;242m [0m[38;2;248;248;242m.[0m[38;2;248;248;242mc[0m[38;2;248;248;242m)[0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m | ||||
|  | ||||
| [38;2;248;248;242m([0m | ||||
|  | ||||
| [38;2;249;38;114mif[0m[38;2;248;248;242m [0m[38;2;248;248;242m(. | [0m[38;2;102;217;239mcontains[0m[38;2;248;248;242m([0m[38;2;230;219;116m"[0m[38;2;230;219;116mnever[0m[38;2;230;219;116m"[0m[38;2;248;248;242m)[0m[38;2;248;248;242m )[0m[38;2;248;248;242m [0m[38;2;249;38;114mthen[0m | ||||
| [38;2;249;38;114mif[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m[38;2;248;248;242m.[0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m[38;2;248;248;242m [0m[38;2;102;217;239mcontains[0m[38;2;248;248;242m([0m[38;2;230;219;116m"[0m[38;2;230;219;116mnever[0m[38;2;230;219;116m"[0m[38;2;248;248;242m)[0m[38;2;248;248;242m [0m[38;2;248;248;242m)[0m[38;2;248;248;242m [0m[38;2;249;38;114mthen[0m | ||||
| [38;2;248;248;242m	[0m[38;2;230;219;116m"[0m[38;2;230;219;116mWhy yes[0m[38;2;230;219;116m"[0m | ||||
| [38;2;249;38;114melse[0m | ||||
| [38;2;248;248;242m	[0m[38;2;190;132;255m12.23[0m | ||||
| [38;2;249;38;114mend[0m | ||||
|  | ||||
| [38;2;248;248;242m)[0m[38;2;248;248;242m [0m[38;2;249;38;114mas[0m[38;2;248;248;242m [0m[38;2;255;255;255m$never[0m[38;2;248;248;242m |[0m | ||||
| [38;2;248;248;242m)[0m[38;2;248;248;242m [0m[38;2;249;38;114mas[0m[38;2;248;248;242m [0m[38;2;255;255;255m$[0m[38;2;255;255;255mnever[0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m | ||||
|  | ||||
| [38;2;248;248;242m{[0m | ||||
| [38;2;248;248;242m	[0m[38;2;166;226;46mhello[0m[38;2;248;248;242m,[0m | ||||
| [38;2;248;248;242m	[0m[38;2;166;226;46mwhy[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mbecause[0m[38;2;230;219;116m"[0m[38;2;248;248;242m,[0m | ||||
| [38;2;248;248;242m	[0m[38;2;166;226;46mhello[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m( weird | [0m[38;2;102;217;239mascii_upcase[0m[38;2;248;248;242m )[0m[38;2;248;248;242m,[0m | ||||
| [38;2;248;248;242m	[0m[38;2;166;226;46mformat_eg[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m( . | [0m[38;2;190;132;255m@json[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mMy json string [0m[38;2;190;132;255m\([0m[38;2;248;248;242m . | this | part | just | white | ascii_upcase | transpose[0m[38;2;190;132;255m)[0m[38;2;230;219;116m"[0m[38;2;248;248;242m )[0m[38;2;248;248;242m,[0m | ||||
| [38;2;248;248;242m	[0m[38;2;166;226;46mnever[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;255;255;255m$never[0m[38;2;248;248;242m,[0m | ||||
| [38;2;248;248;242m	[0m[38;2;166;226;46mhello[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m[38;2;248;248;242m weird [0m[38;2;249;38;114m|[0m[38;2;248;248;242m [0m[38;2;102;217;239mascii_upcase[0m[38;2;248;248;242m [0m[38;2;248;248;242m)[0m[38;2;248;248;242m,[0m | ||||
| [38;2;248;248;242m	[0m[38;2;166;226;46mformat_eg[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m[38;2;248;248;242m [0m[38;2;248;248;242m.[0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m[38;2;248;248;242m [0m[38;2;190;132;255m@json[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mMy json string [0m[38;2;190;132;255m\([0m[38;2;248;248;242m . | this | part | just | white | ascii_upcase | transpose[0m[38;2;190;132;255m)[0m[38;2;230;219;116m"[0m[38;2;248;248;242m [0m[38;2;248;248;242m)[0m[38;2;248;248;242m,[0m | ||||
| [38;2;248;248;242m	[0m[38;2;166;226;46mnever[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;255;255;255m$[0m[38;2;255;255;255mnever[0m[38;2;248;248;242m,[0m | ||||
| [38;2;248;248;242m	[0m[38;2;230;219;116m"[0m[38;2;230;219;116mliteral_key[0m[38;2;230;219;116m"[0m[38;2;248;248;242m:[0m[38;2;248;248;242m literal_value[0m[38;2;248;248;242m,[0m | ||||
| [38;2;248;248;242m	[0m[38;2;230;219;116m"[0m[38;2;230;219;116mthis[0m[38;2;230;219;116m"[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;190;132;255m12.1e12[0m[38;2;248;248;242m,[0m | ||||
| [38;2;248;248;242m	[0m[38;2;230;219;116m"[0m[38;2;230;219;116mpart[0m[38;2;230;219;116m"[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116malmost[0m[38;2;230;219;116m"[0m | ||||
| @@ -38,8 +38,8 @@ | ||||
| [38;2;248;248;242m				[0m[38;2;166;226;46msimilar[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;230;219;116m"[0m[38;2;230;219;116mbut not quite[0m[38;2;230;219;116m"[0m | ||||
| [38;2;248;248;242m			[0m[38;2;248;248;242m}[0m | ||||
| [38;2;248;248;242m		[0m[38;2;248;248;242m}[0m | ||||
| [38;2;248;248;242m	][0m[38;2;248;248;242m,[0m | ||||
| [38;2;248;248;242m}[0m[38;2;248;248;242m | [0m[38;2;248;248;242m([0m | ||||
| [38;2;248;248;242m	[0m[38;2;248;248;242m][0m[38;2;248;248;242m,[0m | ||||
| [38;2;248;248;242m}[0m[38;2;248;248;242m [0m[38;2;249;38;114m|[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m | ||||
| [38;2;248;248;242m	[0m | ||||
| [38;2;248;248;242m	[0m[38;2;117;113;94m#[0m[38;2;117;113;94m And with very basic brace matching[0m | ||||
| [38;2;248;248;242m	[0m | ||||
| @@ -47,13 +47,13 @@ | ||||
| [38;2;248;248;242m	[0m[38;2;248;248;240m][0m[38;2;248;248;242m	[0m | ||||
| [38;2;248;248;242m	[0m | ||||
| [38;2;248;248;242m	[0m[38;2;117;113;94m#[0m[38;2;117;113;94m Other invalid ends[0m | ||||
| [38;2;248;248;242m	[0m[38;2;248;248;242m( [0m[38;2;248;248;242m[ [0m[38;2;248;248;240m}[0m[38;2;248;248;242m [0m[38;2;248;248;240m][0m[38;2;248;248;242m )[0m | ||||
| [38;2;248;248;242m	[0m[38;2;248;248;242m([0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m [0m[38;2;248;248;240m}[0m[38;2;248;248;242m [0m[38;2;248;248;240m][0m[38;2;248;248;242m )[0m | ||||
|  | ||||
| [38;2;248;248;242m	[0m[38;2;117;113;94m#[0m[38;2;117;113;94m A "valid" sequence[0m | ||||
| [38;2;248;248;242m	[0m[38;2;248;248;242m( [0m[38;2;248;248;242m[  [0m[38;2;248;248;242m{ [0m[38;2;166;226;46mkey[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m()[0m[38;2;248;248;242m [0m[38;2;248;248;242m,[0m[38;2;248;248;242m [0m[38;2;166;226;46mother_key[0m[38;2;248;248;242m:[0m[38;2;248;248;242m( [0m[38;2;248;248;242m[ [0m[38;2;248;248;242m[][0m[38;2;248;248;242m  [0m[38;2;248;248;242m[[0m[38;2;248;248;242m[][0m[38;2;248;248;242m][0m[38;2;248;248;242m ][0m[38;2;248;248;242m  )[0m[38;2;248;248;242m,[0m[38;2;248;248;242m [0m[38;2;166;226;46mgaga[0m[38;2;248;248;242m }[0m[38;2;248;248;242m  ][0m[38;2;248;248;242m )[0m | ||||
| [38;2;248;248;242m	[0m[38;2;248;248;242m([0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m  [0m[38;2;248;248;242m{[0m[38;2;248;248;242m [0m[38;2;166;226;46mkey[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m[38;2;248;248;242m)[0m[38;2;248;248;242m [0m[38;2;248;248;242m,[0m[38;2;248;248;242m [0m[38;2;166;226;46mother_key[0m[38;2;248;248;242m:[0m[38;2;248;248;242m([0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m][0m[38;2;248;248;242m  [0m[38;2;248;248;242m[[0m[38;2;248;248;242m[[0m[38;2;248;248;242m][0m[38;2;248;248;242m][0m[38;2;248;248;242m [0m[38;2;248;248;242m][0m[38;2;248;248;242m  [0m[38;2;248;248;242m)[0m[38;2;248;248;242m,[0m[38;2;248;248;242m [0m[38;2;166;226;46mgaga[0m[38;2;248;248;242m [0m[38;2;248;248;242m}[0m[38;2;248;248;242m  [0m[38;2;248;248;242m][0m[38;2;248;248;242m [0m[38;2;248;248;242m)[0m | ||||
|  | ||||
| [38;2;248;248;242m	[0m[38;2;117;113;94m#[0m[38;2;117;113;94m A "invalid" sequence[0m | ||||
| [38;2;248;248;242m	[0m[38;2;248;248;242m( [0m[38;2;248;248;242m[  [0m[38;2;248;248;242m{ [0m[38;2;166;226;46mkey[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m()[0m[38;2;248;248;242m [0m[38;2;248;248;242m,[0m[38;2;248;248;242m [0m[38;2;166;226;46mother_key[0m[38;2;248;248;242m:[0m[38;2;248;248;242m( [0m[38;2;248;248;242m[ [0m[38;2;248;248;242m[][0m[38;2;248;248;242m  [0m[38;2;248;248;242m[[0m[38;2;248;248;242m[][0m[38;2;248;248;242m ][0m[38;2;248;248;242m  [0m[38;2;248;248;240m)[0m[38;2;248;248;242m, gaga [0m[38;2;248;248;240m}[0m[38;2;248;248;242m  ] )[0m | ||||
| [38;2;248;248;242m	[0m[38;2;248;248;242m([0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m  [0m[38;2;248;248;242m{[0m[38;2;248;248;242m [0m[38;2;166;226;46mkey[0m[38;2;248;248;242m:[0m[38;2;248;248;242m [0m[38;2;248;248;242m([0m[38;2;248;248;242m)[0m[38;2;248;248;242m [0m[38;2;248;248;242m,[0m[38;2;248;248;242m [0m[38;2;166;226;46mother_key[0m[38;2;248;248;242m:[0m[38;2;248;248;242m([0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m [0m[38;2;248;248;242m[[0m[38;2;248;248;242m][0m[38;2;248;248;242m  [0m[38;2;248;248;242m[[0m[38;2;248;248;242m[[0m[38;2;248;248;242m][0m[38;2;248;248;242m [0m[38;2;248;248;242m][0m[38;2;248;248;242m  [0m[38;2;248;248;240m)[0m[38;2;248;248;242m, gaga [0m[38;2;248;248;240m}[0m[38;2;248;248;242m  ] )[0m | ||||
|  | ||||
| [38;2;248;248;242m	[0m[38;2;230;219;116m"[0m[38;2;230;219;116mA string[0m[38;2;190;132;255m\[0m[38;2;190;132;255mn[0m[38;2;230;219;116m whith escaped characters [0m[38;2;190;132;255m\[0m[38;2;190;132;255m"[0m[38;2;230;219;116m because we can[0m[38;2;230;219;116m"[0m | ||||
| [38;2;248;248;242m)[0m | ||||
|   | ||||
| @@ -1,36 +1,36 @@ | ||||
| [38;2;166;226;46m␀[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{1}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{2}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{3}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{4}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{5}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{6}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␁[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␂[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␃[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␄[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␅[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␆[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;166;226;46m␇[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;166;226;46m␈[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;190;132;255m├──┤[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;249;38;114m␊[0m | ||||
| [38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{b}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{c}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␋[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␌[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{e}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{f}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{10}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{11}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{12}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{13}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{14}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{15}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{16}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{17}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{18}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{19}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{1a}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␎[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␏[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␐[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␑[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␒[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␓[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␔[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␕[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␖[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␗[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␘[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␙[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␚[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;166;226;46m␛[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{1c}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{1d}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{1e}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{1f}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␜[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␝[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␞[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␟[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;102;217;239m·[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m![0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m"[0m[38;2;249;38;114m␊[0m | ||||
| @@ -126,7 +126,7 @@ | ||||
| [38;2;248;248;242m|[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m~[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{7f}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;248;248;242m␡[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{80}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{81}[0m[38;2;249;38;114m␊[0m | ||||
| [38;2;117;113;94m\u{82}[0m[38;2;249;38;114m␊[0m | ||||
|   | ||||
| @@ -4,8 +4,8 @@ | ||||
| [38;2;248;248;242mUse '--help' instead of '-h' to see a more detailed version of the help text.[0m | ||||
|  | ||||
| [38;2;246;170;17mUSAGE:[0m | ||||
| [38;2;248;248;242m    bat [OPTIONS] [FILE]...[0m | ||||
| [38;2;248;248;242m    bat <SUBCOMMAND>[0m | ||||
| [38;2;248;248;242m    [0m[38;2;190;132;255mbat[0m[38;2;248;248;242m [OPTIONS] [FILE]...[0m | ||||
| [38;2;248;248;242m    [0m[38;2;190;132;255mbat[0m[38;2;248;248;242m <SUBCOMMAND>[0m | ||||
|  | ||||
| [38;2;246;170;17mARGS:[0m | ||||
| [38;2;248;248;242m    [0m[38;2;249;38;114m<FILE>...[0m[38;2;248;248;242m    File(s) to print / concatenate. Use '-' for standard input.[0m | ||||
|   | ||||
| @@ -41,7 +41,7 @@ fail_test() { | ||||
|  | ||||
| echo_step "TEST: Make sure 'BatTestCustomAssets' is not part of integrated syntaxes" | ||||
| bat -f "${custom_syntax_args[@]}" && | ||||
|     fail_test "EXPECTED: 'unknown syntax' error ACTUAL: no error occured" | ||||
|     fail_test "EXPECTED: 'unknown syntax' error ACTUAL: no error occurred" | ||||
|  | ||||
| echo_step "PREPARE: Install custom syntax 'BatTestCustomAssets'" | ||||
| custom_syntaxes_dir="$(bat --config-dir)/syntaxes" | ||||
| @@ -62,7 +62,7 @@ bat -f "${integrated_syntax_args[@]}" || | ||||
|  | ||||
| echo_step "TEST: 'BatTestCustomAssets' is an unknown syntax with --no-custom-assets" | ||||
| bat -f --no-custom-assets "${custom_syntax_args[@]}" && | ||||
|     fail_test "EXPECTED: 'unknown syntax' error because of --no-custom-assets ACTUAL: no error occured" | ||||
|     fail_test "EXPECTED: 'unknown syntax' error because of --no-custom-assets ACTUAL: no error occurred" | ||||
|  | ||||
| echo_step "TEST: 'bat cache --clear' removes all files" | ||||
| bat cache --clear | ||||
|   | ||||
| @@ -22,14 +22,14 @@ impl BatTester { | ||||
|     pub fn test_snapshot(&self, name: &str, style: &str) { | ||||
|         let output = Command::new(&self.exe) | ||||
|             .current_dir(self.temp_dir.path()) | ||||
|             .args(&[ | ||||
|             .args([ | ||||
|                 "sample.rs", | ||||
|                 "--no-config", | ||||
|                 "--paging=never", | ||||
|                 "--color=never", | ||||
|                 "--decorations=always", | ||||
|                 "--terminal-width=80", | ||||
|                 &format!("--style={}", style), | ||||
|                 &format!("--style={style}"), | ||||
|             ]) | ||||
|             .output() | ||||
|             .expect("bat failed"); | ||||
| @@ -40,7 +40,7 @@ impl BatTester { | ||||
|             .replace("tests/snapshots/", ""); | ||||
|  | ||||
|         let mut expected = String::new(); | ||||
|         let mut file = File::open(format!("tests/snapshots/output/{}.snapshot.txt", name)) | ||||
|         let mut file = File::open(format!("tests/snapshots/output/{name}.snapshot.txt")) | ||||
|             .expect("snapshot file missing"); | ||||
|         file.read_to_string(&mut expected) | ||||
|             .expect("could not read snapshot file"); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user