mirror of
				https://github.com/sharkdp/bat.git
				synced 2025-10-30 22:54:07 +00:00 
			
		
		
		
	Compare commits
	
		
			176 Commits
		
	
	
		
			dependabot
			...
			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 | ||
|  | c261b41578 | ||
|  | 6f1cc80d68 | ||
|  | 3b0ade9cb8 | ||
|  | 57016f4e04 | ||
|  | d7503bfc09 | ||
|  | 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 | 
| @@ -29,5 +29,5 @@ jobs: | ||||
|           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:" | ||||
|           echo "Grepping for PR info (see CONTRIBUTING.md):" | ||||
|           grep "#${PR_NUMBER}\\b.*@${PR_SUBMITTER}\\b" <<< "$ADDED" | ||||
|   | ||||
							
								
								
									
										28
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -2,9 +2,18 @@ | ||||
|  | ||||
| ## 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 | ||||
|  | ||||
| @@ -15,18 +24,37 @@ | ||||
| - 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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										263
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										263
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -129,6 +129,8 @@ dependencies = [ | ||||
|  "globset", | ||||
|  "grep-cli", | ||||
|  "home", | ||||
|  "indexmap", | ||||
|  "itertools 0.12.1", | ||||
|  "nix", | ||||
|  "nu-ansi-term", | ||||
|  "once_cell", | ||||
| @@ -140,12 +142,15 @@ dependencies = [ | ||||
|  "run_script", | ||||
|  "semver", | ||||
|  "serde", | ||||
|  "serde_derive", | ||||
|  "serde_with", | ||||
|  "serde_yaml", | ||||
|  "serial_test", | ||||
|  "shell-words", | ||||
|  "syntect", | ||||
|  "tempfile", | ||||
|  "thiserror", | ||||
|  "toml", | ||||
|  "unicode-width", | ||||
|  "wait-timeout", | ||||
|  "walkdir", | ||||
| @@ -224,11 +229,12 @@ checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" | ||||
|  | ||||
| [[package]] | ||||
| name = "cc" | ||||
| version = "1.0.73" | ||||
| version = "1.0.83" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" | ||||
| checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" | ||||
| dependencies = [ | ||||
|  "jobserver", | ||||
|  "libc", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -267,13 +273,14 @@ checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" | ||||
|  | ||||
| [[package]] | ||||
| name = "clircle" | ||||
| version = "0.4.0" | ||||
| version = "0.5.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "c8e87cbed5354f17bd8ca8821a097fb62599787fe8f611743fad7ee156a0a600" | ||||
| checksum = "ec0b92245ea62a7a751db4b0e4a583f8978e508077ef6de24fcc0d0dc5311a8d" | ||||
| dependencies = [ | ||||
|  "cfg-if", | ||||
|  "libc", | ||||
|  "serde", | ||||
|  "serde_derive", | ||||
|  "winapi", | ||||
| ] | ||||
|  | ||||
| @@ -314,6 +321,41 @@ dependencies = [ | ||||
|  "cfg-if", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "darling" | ||||
| version = "0.20.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" | ||||
| dependencies = [ | ||||
|  "darling_core", | ||||
|  "darling_macro", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "darling_core" | ||||
| version = "0.20.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" | ||||
| dependencies = [ | ||||
|  "fnv", | ||||
|  "ident_case", | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "strsim", | ||||
|  "syn", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "darling_macro" | ||||
| version = "0.20.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" | ||||
| dependencies = [ | ||||
|  "darling_core", | ||||
|  "quote", | ||||
|  "syn", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "dashmap" | ||||
| version = "5.4.0" | ||||
| @@ -327,6 +369,15 @@ dependencies = [ | ||||
|  "parking_lot_core", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "deranged" | ||||
| version = "0.3.11" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" | ||||
| dependencies = [ | ||||
|  "powerfmt", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "difflib" | ||||
| version = "0.4.0" | ||||
| @@ -513,9 +564,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "git2" | ||||
| version = "0.18.1" | ||||
| version = "0.18.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "fbf97ba92db08df386e10c8ede66a2a0369bd277090afd8710e19e38de9ec0cd" | ||||
| checksum = "1b3ba52851e73b46a4c3df1d89343741112003f0f6f13beb0dfac9e457c3fdcd" | ||||
| dependencies = [ | ||||
|  "bitflags 2.4.0", | ||||
|  "libc", | ||||
| @@ -540,7 +591,7 @@ dependencies = [ | ||||
|  "bstr", | ||||
|  "log", | ||||
|  "regex-automata", | ||||
|  "regex-syntax 0.8.2", | ||||
|  "regex-syntax", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -578,6 +629,12 @@ dependencies = [ | ||||
|  "windows-sys 0.52.0", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "ident_case" | ||||
| version = "1.0.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" | ||||
|  | ||||
| [[package]] | ||||
| name = "idna" | ||||
| version = "0.3.0" | ||||
| @@ -590,22 +647,13 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "indexmap" | ||||
| version = "1.9.1" | ||||
| version = "2.2.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" | ||||
| dependencies = [ | ||||
|  "autocfg", | ||||
|  "hashbrown 0.12.3", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "indexmap" | ||||
| version = "2.0.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" | ||||
| checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" | ||||
| dependencies = [ | ||||
|  "equivalent", | ||||
|  "hashbrown 0.14.1", | ||||
|  "serde", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -617,6 +665,15 @@ dependencies = [ | ||||
|  "either", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "itertools" | ||||
| version = "0.12.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" | ||||
| dependencies = [ | ||||
|  "either", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "itoa" | ||||
| version = "1.0.3" | ||||
| @@ -646,9 +703,9 @@ checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" | ||||
|  | ||||
| [[package]] | ||||
| name = "libgit2-sys" | ||||
| version = "0.16.1+1.7.1" | ||||
| version = "0.16.2+1.7.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "f2a2bb3680b094add03bb3732ec520ece34da31a8cd2d633d1389d0f0fb60d0c" | ||||
| checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" | ||||
| dependencies = [ | ||||
|  "cc", | ||||
|  "libc", | ||||
| @@ -739,13 +796,19 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" | ||||
|  | ||||
| [[package]] | ||||
| name = "nu-ansi-term" | ||||
| version = "0.49.0" | ||||
| version = "0.50.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" | ||||
| checksum = "dd2800e1520bdc966782168a627aa5d1ad92e33b984bf7c7615d31280c83ff14" | ||||
| dependencies = [ | ||||
|  "windows-sys 0.48.0", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "num-conv" | ||||
| version = "0.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" | ||||
|  | ||||
| [[package]] | ||||
| name = "num-traits" | ||||
| version = "0.2.15" | ||||
| @@ -755,15 +818,6 @@ dependencies = [ | ||||
|  "autocfg", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "num_threads" | ||||
| version = "0.1.6" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "once_cell" | ||||
| version = "1.19.0" | ||||
| @@ -847,18 +901,24 @@ checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" | ||||
|  | ||||
| [[package]] | ||||
| name = "plist" | ||||
| version = "1.5.1" | ||||
| version = "1.6.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "9a4a0cfc5fb21a09dc6af4bf834cf10d4a32fccd9e2ea468c4b1751a097487aa" | ||||
| checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" | ||||
| dependencies = [ | ||||
|  "base64", | ||||
|  "indexmap 1.9.1", | ||||
|  "indexmap", | ||||
|  "line-wrap", | ||||
|  "quick-xml", | ||||
|  "serde", | ||||
|  "time", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "powerfmt" | ||||
| version = "0.2.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" | ||||
|  | ||||
| [[package]] | ||||
| name = "ppv-lite86" | ||||
| version = "0.2.17" | ||||
| @@ -874,7 +934,7 @@ dependencies = [ | ||||
|  "anstyle", | ||||
|  "difflib", | ||||
|  "float-cmp", | ||||
|  "itertools", | ||||
|  "itertools 0.11.0", | ||||
|  "normalize-line-endings", | ||||
|  "predicates-core", | ||||
|  "regex", | ||||
| @@ -907,9 +967,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "quick-xml" | ||||
| version = "0.30.0" | ||||
| version = "0.31.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" | ||||
| checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" | ||||
| dependencies = [ | ||||
|  "memchr", | ||||
| ] | ||||
| @@ -980,7 +1040,7 @@ dependencies = [ | ||||
|  "aho-corasick", | ||||
|  "memchr", | ||||
|  "regex-automata", | ||||
|  "regex-syntax 0.8.2", | ||||
|  "regex-syntax", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -991,15 +1051,9 @@ checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" | ||||
| dependencies = [ | ||||
|  "aho-corasick", | ||||
|  "memchr", | ||||
|  "regex-syntax 0.8.2", | ||||
|  "regex-syntax", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "regex-syntax" | ||||
| version = "0.7.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" | ||||
|  | ||||
| [[package]] | ||||
| name = "regex-syntax" | ||||
| version = "0.8.2" | ||||
| @@ -1066,9 +1120,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" | ||||
|  | ||||
| [[package]] | ||||
| name = "semver" | ||||
| version = "1.0.20" | ||||
| version = "1.0.21" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" | ||||
| checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" | ||||
|  | ||||
| [[package]] | ||||
| name = "serde" | ||||
| @@ -1101,13 +1155,45 @@ dependencies = [ | ||||
|  "serde", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "serde_spanned" | ||||
| version = "0.6.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" | ||||
| dependencies = [ | ||||
|  "serde", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "serde_with" | ||||
| version = "3.6.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270" | ||||
| dependencies = [ | ||||
|  "serde", | ||||
|  "serde_derive", | ||||
|  "serde_with_macros", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "serde_with_macros" | ||||
| version = "3.6.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d" | ||||
| dependencies = [ | ||||
|  "darling", | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "serde_yaml" | ||||
| version = "0.9.29" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a15e0ef66bf939a7c890a0bf6d5a733c70202225f9888a89ed5c62298b019129" | ||||
| dependencies = [ | ||||
|  "indexmap 2.0.2", | ||||
|  "indexmap", | ||||
|  "itoa", | ||||
|  "ryu", | ||||
|  "serde", | ||||
| @@ -1180,9 +1266,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "syntect" | ||||
| version = "5.1.0" | ||||
| version = "5.2.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e02b4b303bf8d08bfeb0445cba5068a3d306b6baece1d5582171a9bf49188f91" | ||||
| checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" | ||||
| dependencies = [ | ||||
|  "bincode", | ||||
|  "bitflags 1.3.2", | ||||
| @@ -1192,8 +1278,9 @@ dependencies = [ | ||||
|  "once_cell", | ||||
|  "onig", | ||||
|  "plist", | ||||
|  "regex-syntax 0.7.5", | ||||
|  "regex-syntax", | ||||
|  "serde", | ||||
|  "serde_derive", | ||||
|  "serde_json", | ||||
|  "thiserror", | ||||
|  "walkdir", | ||||
| @@ -1270,13 +1357,33 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "time" | ||||
| version = "0.3.14" | ||||
| version = "0.3.34" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" | ||||
| checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" | ||||
| dependencies = [ | ||||
|  "deranged", | ||||
|  "itoa", | ||||
|  "libc", | ||||
|  "num_threads", | ||||
|  "num-conv", | ||||
|  "powerfmt", | ||||
|  "serde", | ||||
|  "time-core", | ||||
|  "time-macros", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "time-core" | ||||
| version = "0.1.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" | ||||
|  | ||||
| [[package]] | ||||
| name = "time-macros" | ||||
| version = "0.2.17" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" | ||||
| dependencies = [ | ||||
|  "num-conv", | ||||
|  "time-core", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -1294,6 +1401,41 @@ version = "0.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" | ||||
|  | ||||
| [[package]] | ||||
| name = "toml" | ||||
| version = "0.8.9" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "c6a4b9e8023eb94392d3dca65d717c53abc5dad49c07cb65bb8fcd87115fa325" | ||||
| dependencies = [ | ||||
|  "indexmap", | ||||
|  "serde", | ||||
|  "serde_spanned", | ||||
|  "toml_datetime", | ||||
|  "toml_edit", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "toml_datetime" | ||||
| version = "0.6.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" | ||||
| dependencies = [ | ||||
|  "serde", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "toml_edit" | ||||
| version = "0.21.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" | ||||
| dependencies = [ | ||||
|  "indexmap", | ||||
|  "serde", | ||||
|  "serde_spanned", | ||||
|  "toml_datetime", | ||||
|  "winnow", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "unicode-bidi" | ||||
| version = "0.3.8" | ||||
| @@ -1613,6 +1755,15 @@ version = "0.52.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" | ||||
|  | ||||
| [[package]] | ||||
| name = "winnow" | ||||
| version = "0.5.18" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "176b6138793677221d420fd2f0aeeced263f197688b36484660da767bca2fa32" | ||||
| dependencies = [ | ||||
|  "memchr", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "yaml-rust" | ||||
| version = "0.4.5" | ||||
|   | ||||
							
								
								
									
										21
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -41,7 +41,7 @@ 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.7" | ||||
| @@ -53,11 +53,12 @@ content_inspector = "0.2.4" | ||||
| shell-words = { version = "1.1.0", optional = true } | ||||
| unicode-width = "0.1.11" | ||||
| globset = "0.4" | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
| 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.10", optional = true } | ||||
| @@ -74,7 +75,7 @@ optional = true | ||||
| default-features = false | ||||
|  | ||||
| [dependencies.syntect] | ||||
| version = "5.1.0" | ||||
| version = "5.2.0" | ||||
| default-features = false | ||||
| features = ["parsing"] | ||||
|  | ||||
| @@ -85,7 +86,7 @@ features = ["wrap_help", "cargo"] | ||||
|  | ||||
| [target.'cfg(target_os = "macos")'.dependencies] | ||||
| home = "0.5.9" | ||||
| plist = "1.5.1" | ||||
| plist = "1.6.0" | ||||
|  | ||||
| [dev-dependencies] | ||||
| assert_cmd = "2.0.12" | ||||
| @@ -94,12 +95,22 @@ serial_test = { version = "2.0.0", default-features = false } | ||||
| predicates = "3.0.4" | ||||
| wait-timeout = "0.2.0" | ||||
| tempfile = "3.8.1" | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
|  | ||||
| [target.'cfg(unix)'.dev-dependencies] | ||||
| 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.12" | ||||
|   | ||||
							
								
								
									
										44
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								README.md
									
									
									
									
									
								
							| @@ -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 | ||||
|  | ||||
|   | ||||
							
								
								
									
										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: b150d84534...209559b72f
									
								
							
							
								
								
									
										2
									
								
								assets/themes/zenburn
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
									
										2
									
								
								assets/themes/zenburn
									
									
									
									
										vendored
									
									
								
							 Submodule assets/themes/zenburn updated: e627f1cb22...86d4ee7a1f
									
								
							| @@ -1,5 +1,6 @@ | ||||
| #[cfg(feature = "application")] | ||||
| mod application; | ||||
| mod syntax_mapping; | ||||
| mod util; | ||||
|  | ||||
| fn main() -> anyhow::Result<()> { | ||||
| @@ -7,6 +8,8 @@ fn main() -> anyhow::Result<()> { | ||||
|     // 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()?; | ||||
|  | ||||
|   | ||||
							
								
								
									
										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(()) | ||||
| } | ||||
| @@ -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') | ||||
|  | ||||
|   | ||||
| @@ -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(), | ||||
|   | ||||
| @@ -121,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 { | ||||
| @@ -130,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 { | ||||
| @@ -287,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)"); | ||||
|   | ||||
| @@ -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()); | ||||
|                     } | ||||
|   | ||||
| @@ -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"); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										53
									
								
								tests/benchmarks/run-benchmarks.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										53
									
								
								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." | ||||
| @@ -95,10 +102,20 @@ hyperfine \ | ||||
| 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" \ | ||||
| @@ -117,6 +134,40 @@ hyperfine \ | ||||
| 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" \ | ||||
|   | ||||
							
								
								
									
										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 | ||||
							
								
								
									
										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 | ||||
| @@ -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() | ||||
| @@ -1853,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") | ||||
| @@ -1864,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 | ||||
|   | ||||
| @@ -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