diff --git a/.gitignore b/.gitignore index a3ea8cff..fbfe6ac6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ **/*.rs.bk # Generated files +/assets/completions/_bat.ps1 /assets/completions/bat.bash /assets/completions/bat.fish /assets/completions/bat.zsh diff --git a/.gitmodules b/.gitmodules index 7c8a7724..fe159da8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -260,3 +260,6 @@ [submodule "assets/syntaxes/02_Extra/vscode-wgsl"] path = assets/syntaxes/02_Extra/vscode-wgsl url = https://github.com/PolyMeilex/vscode-wgsl.git +[submodule "assets/syntaxes/02_Extra/CFML"] + path = assets/syntaxes/02_Extra/CFML + url = https://github.com/jcberquist/sublimetext-cfml.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e361ddb..8c0f591b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ - `PrettyPrinter::squeeze_empty_lines` to support line squeezing for bat as a library, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815) - Syntax highlighting for JavaScript files that start with `#!/usr/bin/env bun` #2913 (@sharunkumar) - `bat --strip-ansi={never,always,auto}` to remove ANSI escape sequences from bat's input, see #2999 (@eth-p) +- Add or remove individual style components without replacing all styles #2929 (@eth-p) +- Automatically choose theme based on the terminal's color scheme, see #2896 (@bash) +- Add option `--binary=as-text` for printing binary content, see issue #2974 and PR #2976 (@einfachIrgendwer0815) ## Bugfixes @@ -16,6 +19,8 @@ - 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) +- Fix compile issue of `time` dependency caused by standard library regression #3045 (@cyqsimon) +- Fix override behavior of --plain and --paging, see issue #2731 and PR #3108 (@einfachIrgendwer0815) ## Other @@ -41,6 +46,8 @@ - Update the Lisp syntax, see #2970 (@ccqpein) - Use bat's ANSI iterator during tab expansion, see #2998 (@eth-p) - Support 'statically linked binary' for aarch64 in 'Release' page, see #2992 (@tzq0301) +- Update options in shell completions and the man page of `bat`, see #2995 (@akinomyoga) +- Update nix dev-dependency to v0.29.0, see #3112 (@decathorpe) ## Syntaxes @@ -48,10 +55,19 @@ - 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) +- Associate `.xsh` files with `xonsh` syntax that is Python, see #2840 (@anki-code) +- Associate JSON with Comments `.jsonc` with `json` syntax, see #2795 (@mxaddict) +- Associate JSON-LD `.jsonld` files with `json` syntax, see #3037 (@vorburger) +- Associate `.textproto` files with `ProtoBuf` syntax, see #3038 (@vorburger) +- Associate GeoJSON `.geojson` files with `json` syntax, see #3084 (@mvaaltola) +- Associate `.aws/{config,credentials}`, see #2795 (@mxaddict) +- Associate Wireguard config `/etc/wireguard/*.conf`, see #2874 (@cyqsimon) +- Add support for [CFML](https://www.adobe.com/products/coldfusion-family.html), see #3031 (@brenton-at-pieces) +- Map `*.mkd` files to `Markdown` syntax, see issue #3060 and PR #3061 (@einfachIrgendwer0815) +- Add syntax mapping for CITATION.cff, see #3103 (@Ugzuzg) +- Add syntax mapping for kubernetes config files #3049 (@cyqsimon) +- Adds support for pipe delimiter for CSV #3115 (@pratik-m) +- Add syntax mapping for `/etc/pacman.conf` #2961 (@cyqsimon) ## Themes @@ -63,6 +79,10 @@ - [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) +- Add `theme::theme` for choosing an appropriate theme based on the + terminal's color scheme, see #2896 (@bash) + - [BREAKING] Remove `HighlightingAssets::default_theme`. Use `theme::default_theme` instead. +- Add `PrettyPrinter::print_with_writer` for custom output destinations, see #3070 (@kojix2) # v0.24.0 @@ -101,6 +121,7 @@ - Update `Julia` syntax, see #2553 (@dependabot) - add `NSIS` support, see #2577 (@idleberg) - Update `ssh-config`, see #2697 (@mrmeszaros) +- Add syntax mapping `*.debdiff` => `diff`, see #2947 (@jacg) ## `bat` as a library diff --git a/Cargo.lock b/Cargo.lock index 5fc3e1fd..a6d47a19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,10 +3,10 @@ version = 3 [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" @@ -19,9 +19,9 @@ dependencies = [ [[package]] name = "ansi_colours" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a1558bd2075d341b9ca698ec8eb6fcc55a746b1fc4255585aad5b141d918a80" +checksum = "14eec43e0298190790f41679fe69ef7a829d2a2ddd78c8c00339e84710e435fe" dependencies = [ "rgb", ] @@ -103,9 +103,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" -version = "0.21.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bat" @@ -149,6 +149,7 @@ dependencies = [ "shell-words", "syntect", "tempfile", + "terminal-colorsaurus", "thiserror", "toml", "unicode-width", @@ -243,6 +244,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "4.4.12" @@ -273,12 +280,11 @@ checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "clircle" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0b92245ea62a7a751db4b0e4a583f8978e508077ef6de24fcc0d0dc5311a8d" +checksum = "e136d50bd652710f1d86259a8977263d46bef0ab782a8bfc3887e44338517015" dependencies = [ "cfg-if", - "libc", "serde", "serde_derive", "winapi", @@ -416,9 +422,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -463,9 +469,9 @@ dependencies = [ [[package]] name = "expect-test" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d9eafeadd538e68fb28016364c9732d78e420b9ff8853fa5e4058861e9f8d3" +checksum = "9e0be0a561335815e06dab7c62e50353134c796e7a6155402a64bcff66b6a5e0" dependencies = [ "dissimilar", "once_cell", @@ -489,9 +495,9 @@ checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide", @@ -564,9 +570,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.18.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" dependencies = [ "bitflags 2.4.0", "libc", @@ -583,9 +589,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" dependencies = [ "aho-corasick", "bstr", @@ -596,9 +602,9 @@ dependencies = [ [[package]] name = "grep-cli" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea40788c059ab8b622c4d074732750bfb3bd2912e2dd58eabc11798a4d5ad725" +checksum = "47f1288f0e06f279f84926fa4c17e3fcd2a22b357927a82f2777f7be26e4cec0" dependencies = [ "bstr", "globset", @@ -620,6 +626,12 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "home" version = "0.5.9" @@ -647,9 +659,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown 0.14.1", @@ -688,15 +700,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.149" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libgit2-sys" -version = "0.16.2+1.7.2" +version = "0.17.0+1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" dependencies = [ "cc", "libc", @@ -716,15 +728,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "line-wrap" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" -dependencies = [ - "safemem", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -755,27 +758,40 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4929e1f84c5e54c3ec6141cd5d8b5a5c055f031f80cf78f2072920173cb4d880" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", ] [[package]] name = "nix" -version = "0.26.4" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "cfg-if", + "cfg_aliases", "libc", ] @@ -811,9 +827,12 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" +dependencies = [ + "portable-atomic", +] [[package]] name = "onig" @@ -892,18 +911,23 @@ checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "plist" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64", "indexmap", - "line-wrap", "quick-xml", "serde", "time", ] +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -957,9 +981,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" dependencies = [ "memchr", ] @@ -1087,12 +1111,6 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" - [[package]] name = "same-file" version = "1.0.6" @@ -1116,18 +1134,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.199" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.199" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", @@ -1147,9 +1165,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -1309,6 +1327,30 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal-colorsaurus" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f99bb1dc5cde9eada5a8f466641240f9d5b9f55291d675df4160b097fbfa42e" +dependencies = [ + "cfg-if", + "libc", + "memchr", + "mio", + "terminal-trx", +] + +[[package]] +name = "terminal-trx" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d4c86910e10c782a02d3b7606de43cf7ebd80e1fafdca8e49a0db2b0d4611f0" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "terminal_size" version = "0.3.0" @@ -1347,9 +1389,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -1368,9 +1410,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -1393,9 +1435,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "toml" -version = "0.8.9" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6a4b9e8023eb94392d3dca65d717c53abc5dad49c07cb65bb8fcd87115fa325" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "indexmap", "serde", @@ -1406,18 +1448,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.21.1" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", @@ -1747,9 +1789,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176b6138793677221d420fd2f0aeeced263f197688b36484660da767bca2fa32" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 6170905a..cd59aef7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ minimal-application = [ ] git = ["git2"] # Support indicating git modifications paging = ["shell-words", "grep-cli"] # Support applying a pager on the output -lessopen = ["run_script", "os_str_bytes"] # Support $LESSOPEN preprocessor +lessopen = ["run_script", "os_str_bytes/conversions"] # Support $LESSOPEN preprocessor build-assets = ["syntect/yaml-load", "syntect/plist-load", "regex", "walkdir"] # You need to use one of these if you depend on bat as a library: @@ -46,7 +46,7 @@ ansi_colours = "^1.2" bincode = "1.0" console = "0.15.8" flate2 = "1.0" -once_cell = "1.19" +once_cell = "1.20" thiserror = "1.0" wild = { version = "2.2", optional = true } content_inspector = "0.2.4" @@ -58,19 +58,20 @@ serde_derive = "1.0" serde_yaml = "0.9.28" semver = "1.0" path_abs = { version = "0.5", default-features = false } -clircle = "0.5" +clircle = "0.6" bugreport = { version = "0.5.0", optional = true } etcetera = { version = "0.8.0", optional = true } -grep-cli = { version = "0.1.10", optional = true } +grep-cli = { version = "0.1.11", optional = true } regex = { version = "1.10.2", optional = true } walkdir = { version = "2.5", optional = true } bytesize = { version = "1.3.0" } -encoding_rs = "0.8.34" +encoding_rs = "0.8.35" os_str_bytes = { version = "~7.0", optional = true } run_script = { version = "^0.10.1", optional = true} +terminal-colorsaurus = "0.4" [dependencies.git2] -version = "0.18" +version = "0.19" optional = true default-features = false @@ -86,11 +87,11 @@ features = ["wrap_help", "cargo"] [target.'cfg(target_os = "macos")'.dependencies] home = "0.5.9" -plist = "1.6.0" +plist = "1.7.0" [dev-dependencies] assert_cmd = "2.0.12" -expect-test = "1.4.1" +expect-test = "1.5.0" serial_test = { version = "2.0.0", default-features = false } predicates = "3.1.0" wait-timeout = "0.2.0" @@ -98,18 +99,18 @@ 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"] } +nix = { version = "0.29", default-features = false, features = ["term"] } [build-dependencies] anyhow = "1.0.86" -indexmap = { version = "2.2.6", features = ["serde"] } +indexmap = { version = "2.3.0", features = ["serde"] } itertools = "0.13.0" -once_cell = "1.18" +once_cell = "1.20" regex = "1.10.2" serde = "1.0" serde_derive = "1.0" serde_with = { version = "3.8.1", default-features = false, features = ["macros"] } -toml = { version = "0.8.9", features = ["preserve_order"] } +toml = { version = "0.8.19", features = ["preserve_order"] } walkdir = "2.5" [build-dependencies.clap] diff --git a/README.md b/README.md index 1e42e501..c09d0e36 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,11 @@ A special *thank you* goes to our biggest sponsors Warp
- Warp is a modern, Rust-based terminal with AI built in
so you and your team can build great software, faster.
+ Warp, the intelligent terminal
- Feel more productive on the command line with parameterized commands, + Run commands like a power user with AI and your dev team’s
- autosuggestions, and an IDE-like text editor. + knowledge in one fast, intuitive terminal. For MacOS or Linux.
### Syntax highlighting @@ -482,8 +482,10 @@ the following command (you need [`fzf`](https://github.com/junegunn/fzf) for thi bat --list-themes | fzf --preview="bat --theme={} --color=always /path/to/file" ``` -`bat` looks good on a dark background by default. However, if your terminal uses a -light background, some themes like `GitHub` or `OneHalfLight` will work better for you. +`bat` automatically picks a fitting theme depending on your terminal's background color. +You can use the `--theme-light` / `--theme-light` options or the `BAT_THEME_DARK` / `BAT_THEME_LIGHT` environment variables +to customize the themes used. This is especially useful if you frequently switch between dark and light mode. + You can also use a custom theme by following the ['Adding new themes' section below](https://github.com/sharkdp/bat#adding-new-themes). @@ -515,6 +517,16 @@ and line numbers but no grid and no file header. Set the `BAT_STYLE` environment variable to make these changes permanent or use `bat`s [configuration file](https://github.com/sharkdp/bat#configuration-file). +>[!tip] +> If you specify a default style in `bat`'s config file, you can change which components +> are displayed during a single run of `bat` using the `--style` command-line argument. +> By prefixing a component with `+` or `-`, it can be added or removed from the current style. +> +> For example, if your config contains `--style=full,-snip`, you can run bat with +> `--style=-grid,+snip` to remove the grid and add back the `snip` component. +> Or, if you want to override the styles completely, you use `--style=numbers` to +> only show the line numbers. + ### Adding new syntaxes / language definitions Should you find that a particular syntax is not available within `bat`, you can follow these @@ -683,10 +695,11 @@ on your operating system. To get the default path for your system, call bat --config-file ``` -Alternatively, you can use the `BAT_CONFIG_PATH` environment variable to point `bat` to a -non-default location of the configuration file: +Alternatively, you can use `BAT_CONFIG_PATH` or `BAT_CONFIG_DIR` environment variables to point `bat` +to a non-default location of the configuration file or the configuration directory respectively: ```bash -export BAT_CONFIG_PATH="/path/to/bat.conf" +export BAT_CONFIG_PATH="/path/to/bat/bat.conf" +export BAT_CONFIG_DIR="/path/to/bat" ``` A default configuration file can be created with the `--generate-config-file` option. diff --git a/assets/completions/_bat.ps1.in b/assets/completions/_bat.ps1.in index c0c151e1..b6f62aae 100644 --- a/assets/completions/_bat.ps1.in +++ b/assets/completions/_bat.ps1.in @@ -37,6 +37,8 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script [CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Use the specified syntax for files matching the glob pattern (''*.cpp:C++'').') [CompletionResult]::new('--map-syntax', 'map-syntax', [CompletionResultType]::ParameterName, 'Use the specified syntax for files matching the glob pattern (''*.cpp:C++'').') [CompletionResult]::new('--theme', 'theme', [CompletionResultType]::ParameterName, 'Set the color theme for syntax highlighting.') + [CompletionResult]::new('--theme-dark', 'theme', [CompletionResultType]::ParameterName, 'Set the color theme for syntax highlighting for dark backgrounds.') + [CompletionResult]::new('--theme-light', 'theme', [CompletionResultType]::ParameterName, 'Set the color theme for syntax highlighting for light backgrounds.') [CompletionResult]::new('--style', 'style', [CompletionResultType]::ParameterName, 'Comma-separated list of style elements to display (*default*, auto, full, plain, changes, header, header-filename, header-filesize, grid, rule, numbers, snip).') [CompletionResult]::new('-r', 'r', [CompletionResultType]::ParameterName, 'Only print the lines from N to M.') [CompletionResult]::new('--line-range', 'line-range', [CompletionResultType]::ParameterName, 'Only print the lines from N to M.') diff --git a/assets/completions/bat.bash.in b/assets/completions/bat.bash.in index de8651a8..90931f24 100644 --- a/assets/completions/bat.bash.in +++ b/assets/completions/bat.bash.in @@ -76,6 +76,7 @@ _bat() { -m | --map-syntax | \ --ignored-suffix | \ --list-themes | \ + --squeeze-limit | \ --line-range | \ -L | --list-languages | \ --lessopen | \ @@ -112,6 +113,13 @@ _bat() { return 0 ;; --theme) + local IFS=$'\n' + COMPREPLY=($(compgen -W "auto${IFS}auto:always${IFS}auto:system${IFS}dark${IFS}light${IFS}$("$1" --list-themes)" -- "$cur")) + __bat_escape_completions + return 0 + ;; + --theme-dark | \ + --theme-light) local IFS=$'\n' COMPREPLY=($(compgen -W "$("$1" --list-themes)" -- "$cur")) __bat_escape_completions @@ -157,6 +165,7 @@ _bat() { --diff-context --tabs --wrap + --chop-long-lines --terminal-width --number --color @@ -168,19 +177,27 @@ _bat() { --map-syntax --ignored-suffix --theme + --theme-dark + --theme-light --list-themes + --squeeze-blank + --squeeze-limit --style --line-range --list-languages --lessopen --diagnostic --acknowledgements + --set-terminal-title --help --version --cache-dir --config-dir --config-file --generate-config-file + --no-config + --no-custom-assets + --no-lessopen " -- "$cur")) return 0 fi diff --git a/assets/completions/bat.fish.in b/assets/completions/bat.fish.in index d86ebfe6..e2712706 100644 --- a/assets/completions/bat.fish.in +++ b/assets/completions/bat.fish.in @@ -129,10 +129,20 @@ set -l tabs_opts ' 8\t ' +set -l special_themes ' + auto\tdefault,\ Choose\ a\ theme\ based\ on\ dark\ or\ light\ mode + auto:always\tChoose\ a\ theme\ based\ on\ dark\ or\ light\ mode + auto:system\tChoose\ a\ theme\ based\ on\ dark\ or\ light\ mode + dark\tUse\ the\ theme\ specified\ by\ --theme-dark + light\tUse\ the\ theme\ specified\ by\ --theme-light +' + # Completions: complete -c $bat -l acknowledgements -d "Print acknowledgements" -n __fish_is_first_arg +complete -c $bat -l cache-dir -f -d "Show bat's cache directory" -n __fish_is_first_arg + complete -c $bat -l color -x -a "$color_opts" -d "When to use colored output" -n __bat_no_excl_args complete -c $bat -l config-dir -f -d "Display location of configuration directory" -n __fish_is_first_arg @@ -201,7 +211,11 @@ complete -c $bat -l tabs -x -a "$tabs_opts" -d "Set tab width" -n __bat_no_excl_ complete -c $bat -l terminal-width -x -d "Set terminal , +, or -" -n __bat_no_excl_args -complete -c $bat -l theme -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme" -n __bat_no_excl_args +complete -c $bat -l theme -x -a "$special_themes(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme" -n __bat_no_excl_args + +complete -c $bat -l theme-dark -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme for dark backgrounds" -n __bat_no_excl_args + +complete -c $bat -l theme-light -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme for light backgrounds" -n __bat_no_excl_args complete -c $bat -s V -l version -f -d "Show version information" -n __fish_is_first_arg diff --git a/assets/completions/bat.zsh.in b/assets/completions/bat.zsh.in index 69caceed..dfe1e1b9 100644 --- a/assets/completions/bat.zsh.in +++ b/assets/completions/bat.zsh.in @@ -26,7 +26,7 @@ _{{PROJECT_EXECUTABLE}}_main() { args=( '(-A --show-all)'{-A,--show-all}'[show non-printable characters (space, tab, newline, ..)]' --nonprintable-notation='[specify how to display non-printable characters when using --show-all]:notation:(caret unicode)' - \*{-p,--plain}'[show plain style (alias for `--style=plain`), repeat twice to disable disable automatic paging (alias for `--paging=never`)]' + \*{-p,--plain}'[show plain style (alias for `--style=plain`), repeat twice to disable automatic paging (alias for `--paging=never`)]' '(-l --language)'{-l+,--language=}'[set the language for syntax highlighting]:language:->languages' \*{-H+,--highlight-line=}'[highlight specified block of lines]:start\:end' \*--file-name='[specify the name to display for a file]:name:_files' @@ -42,12 +42,15 @@ _{{PROJECT_EXECUTABLE}}_main() { --decorations='[specify when to show the decorations]:when:(auto never always)' --paging='[specify when to use the pager]:when:(auto never always)' '(-m --map-syntax)'{-m+,--map-syntax=}'[map a glob pattern to an existing syntax name]: :->syntax-maps' - '(--theme)'--theme='[set the color theme for syntax highlighting]:theme:->themes' + '(--theme)'--theme='[set the color theme for syntax highlighting]:theme:->theme_preferences' + '(--theme-dark)'--theme-dark='[set the color theme for syntax highlighting for dark backgrounds]:theme:->themes' + '(--theme-light)'--theme-light='[set the color theme for syntax highlighting for light backgrounds]:theme:->themes' '(: --list-themes --list-languages -L)'--list-themes'[show all supported highlighting themes]' --style='[comma-separated list of style elements to display]: : _values "style [default]" default auto full plain changes header header-filename header-filesize grid rule numbers snip' \*{-r+,--line-range=}'[only print the specified line range]:start\:end' '(* -)'{-L,--list-languages}'[display all supported languages]' + -P'[disable paging]' "--no-config[don't use the configuration file]" "--no-custom-assets[don't load custom assets]" '(--no-lessopen)'--lessopen'[enable the $LESSOPEN preprocessor]' @@ -81,7 +84,13 @@ _{{PROJECT_EXECUTABLE}}_main() { themes) local -a themes expl - themes=( ${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} ) + themes=(${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} ) + + _wanted themes expl 'theme' compadd -a themes && ret=0 + ;; + theme_preferences) + local -a themes expl + themes=(auto dark light auto:always auto:system ${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} ) _wanted themes expl 'theme' compadd -a themes && ret=0 ;; diff --git a/assets/manual/bat.1.in b/assets/manual/bat.1.in index b85520da..ccc70629 100644 --- a/assets/manual/bat.1.in +++ b/assets/manual/bat.1.in @@ -87,6 +87,10 @@ Set the tab width to T spaces. Use a width of 0 to pass tabs through directly Specify the text\-wrapping mode (*auto*, never, character). The '\-\-terminal\-width' option can be used in addition to control the output width. .HP +\fB\-S\fR, \fB\-\-chop\-long\-lines\fR +.IP +Truncate all lines longer than screen width. Alias for '\-\-wrap=never'. +.HP \fB\-\-terminal\-width\fR .IP Explicitly set the width of the terminal instead of determining it automatically. If @@ -141,16 +145,58 @@ use -m '*.build:Python'. To highlight files named '.myignore' with the Git Ignor syntax, use -m '.myignore:Git Ignore'. Note that the right-hand side is the *name* of the syntax, not a file extension. .HP +\fB\-\-ignored\-suffix\fR +.IP +Ignore extension. For example: 'bat \-\-ignored-suffix ".dev" my_file.json.dev' +will use JSON syntax, and ignore '.dev' +.HP \fB\-\-theme\fR .IP -Set the theme for syntax highlighting. Use '\-\-list\-themes' to see all available themes. -To set a default theme, add the '\-\-theme="..."' option to the configuration file or -export the BAT_THEME environment variable (e.g.: export BAT_THEME="..."). +Set the theme for syntax highlighting. Use \fB\-\-list\-themes\fP to see all available themes. +To set a default theme, add the \fB\-\-theme="..."\fP option to the configuration file or +export the \fBBAT_THEME\fP environment variable (e.g.: \fBexport BAT_THEME="..."\fP). + +Special values: +.RS +.IP "auto (\fIdefault\fR)" +Picks a dark or light theme depending on the terminal's colors. +Use \fB-\-theme\-light\fR and \fB-\-theme\-dark\fR to customize the selected theme. +.IP "auto:always" +Variation of \fBauto\fR where where the terminal's colors are detected even when the output is redirected. +.IP "auto:system (macOS only)" +Variation of \fBauto\fR where the color scheme is detected from the system-wide preference instead. +.IP "dark" +Use the dark theme specified by \fB-\-theme-dark\fR. +.IP "light" +Use the light theme specified by \fB-\-theme-light\fR. +.RE +.HP +\fB\-\-theme\-dark\fR +.IP +Sets the theme name for syntax highlighting used when the terminal uses a dark background. +To set a default theme, add the \fB\-\-theme-dark="..."\fP option to the configuration file or +export the \fBBAT_THEME_DARK\fP environment variable (e.g. \fBexport BAT_THEME_DARK="..."\fP). +This option only has an effect when \fB\-\-theme\fP option is set to \fBauto\fR or \fBdark\fR. +.HP +\fB\-\-theme\-light\fR +.IP +Sets the theme name for syntax highlighting used when the terminal uses a dark background. +To set a default theme, add the \fB\-\-theme-dark="..."\fP option to the configuration file or +export the \fBBAT_THEME_LIGHT\fP environment variable (e.g. \fBexport BAT_THEME_LIGHT="..."\fP). +This option only has an effect when \fB\-\-theme\fP option is set to \fBauto\fR or \fBlight\fR. .HP \fB\-\-list\-themes\fR .IP Display a list of supported themes for syntax highlighting. .HP +\fB\-s\fR, \fB\-\-squeeze\-blank\fR +.IP +Squeeze consecutive empty lines into a single empty line. +.HP +\fB\-\-squeeze\-limit\fR +.IP +Set the maximum number of consecutive empty lines to be printed. +.HP \fB\-\-style\fR .IP Configure which elements (line numbers, file headers, grid borders, Git modifications, @@ -184,6 +230,30 @@ Display a list of supported languages for syntax highlighting. This option exists for POSIX\-compliance reasons ('u' is for 'unbuffered'). The output is always unbuffered \- this option is simply ignored. .HP +\fB\-\-no\-custom\-assets\fR +.IP +Do not load custom assets. +.HP +\fB\-\-config\-dir\fR +.IP +Show bat's configuration directory. +.HP +\fB\-\-cache\-dir\fR +.IP +Show bat's cache directory. +.HP +\fB\-\-diagnostic\fR +.IP +Show diagnostic information for bug reports. +.HP +\fB\-\-acknowledgements\fR +.IP +Show acknowledgements. +.HP +\fB\-\-set\-terminal\-title\fR +.IP +Sets terminal title to filenames when using a pager. +.HP \fB\-h\fR, \fB\-\-help\fR .IP Print this help message. @@ -212,6 +282,20 @@ location of the configuration file. To generate a default configuration file, call: \fB{{PROJECT_EXECUTABLE}} --generate-config-file\fR + +These are related options: +.HP +\fB\-\-config\-file\fR +.IP +Show path to the configuration file. +.HP +\fB\-\-generate-config\-file\fR +.IP +Generates a default configuration file. +.HP +\fB\-\-no\-config\fR +.IP +Do not use the configuration file. .SH "ADDING CUSTOM LANGUAGES" {{PROJECT_EXECUTABLE}} supports Sublime Text \fB.sublime-syntax\fR language files, and can be customized to add additional languages to your local installation. To do this, add the \fB.sublime-syntax\fR language @@ -252,13 +336,23 @@ To use the preprocessor, call: \fB{{PROJECT_EXECUTABLE}} --lessopen\fR -Alternatively, the preprocessor may be enabled by default by adding the '\-\-lessopen' option to the configuration file. +Alternatively, the preprocessor may be enabled by default by adding the '\-\-lessopen' option to the configuration file. To temporarily disable the preprocessor if it is enabled by default, call: \fB{{PROJECT_EXECUTABLE}} --no-lessopen\fR -For more information, see the "INPUT PREPROCESSOR" section of less(1). +These are related options: +.HP +\fB\-\-lessopen\fR +.IP +Enable the $LESSOPEN preprocessor. +.HP +\fB\-\-no\-lessopen\fR +.IP +Disable the $LESSOPEN preprocessor if enabled (overrides --lessopen) +.PP +For more information, see the "INPUT PREPROCESSOR" section of less(1). .SH "MORE INFORMATION" diff --git a/assets/syntaxes/02_Extra/CFML b/assets/syntaxes/02_Extra/CFML new file mode 160000 index 00000000..b91c44a3 --- /dev/null +++ b/assets/syntaxes/02_Extra/CFML @@ -0,0 +1 @@ +Subproject commit b91c44a32e251c20c6359a8d9232287e1b408e6c diff --git a/assets/syntaxes/02_Extra/CSV.sublime-syntax b/assets/syntaxes/02_Extra/CSV.sublime-syntax index cca7cd2c..0ad17834 100644 --- a/assets/syntaxes/02_Extra/CSV.sublime-syntax +++ b/assets/syntaxes/02_Extra/CSV.sublime-syntax @@ -7,14 +7,14 @@ file_extensions: - tsv scope: text.csv variables: - field_separator: (?:[,;\t]) + field_separator: (?:[,;|\t]) record_separator: (?:$\n?) contexts: prototype: - match: (?={{record_separator}}) pop: true fields: - - match: '' + - match: "" push: - field_or_record_separator - field4 @@ -26,15 +26,15 @@ contexts: - field1 main: - meta_include_prototype: false - - match: '^' + - match: "^" set: fields field_or_record_separator: - meta_include_prototype: false - - match: '{{record_separator}}' + - match: "{{record_separator}}" scope: punctuation.terminator.record.csv pop: true - - match: '{{field_separator}}' + - match: "{{field_separator}}" scope: punctuation.separator.sequence.csv pop: true @@ -56,23 +56,22 @@ contexts: pop: true field1: - - match: '' + - match: "" set: - meta_content_scope: meta.field-1.csv support.type - include: field_contents field2: - - match: '' + - match: "" set: - meta_content_scope: meta.field-2.csv support.function - include: field_contents field3: - - match: '' + - match: "" set: - meta_content_scope: meta.field-3.csv constant.numeric - include: field_contents field4: - - match: '' + - match: "" set: - meta_content_scope: meta.field-4.csv keyword.operator - include: field_contents - diff --git a/assets/syntaxes/02_Extra/Protobuf b/assets/syntaxes/02_Extra/Protobuf index 726e21d7..13653315 160000 --- a/assets/syntaxes/02_Extra/Protobuf +++ b/assets/syntaxes/02_Extra/Protobuf @@ -1 +1 @@ -Subproject commit 726e21d74dac23cbb036f2fbbd626decdc954060 +Subproject commit 1365331580b0e4bb86f74d0c599dccc87e7bdacb diff --git a/assets/themes/Nord-sublime b/assets/themes/Nord-sublime index 0d655b23..bf92a9e4 160000 --- a/assets/themes/Nord-sublime +++ b/assets/themes/Nord-sublime @@ -1 +1 @@ -Subproject commit 0d655b23d6b300e691676d9b90a68d92b267f7ec +Subproject commit bf92a9e4457dc2f97efebc59bbeac95933ec6515 diff --git a/doc/README-zh.md b/doc/README-zh.md index 007afb3e..9f2d202e 100644 --- a/doc/README-zh.md +++ b/doc/README-zh.md @@ -616,63 +616,59 @@ iconv -f ISO-8859-1 -t UTF-8 my-file.php | bat 注意: 当`bat`无法识别语言时你可能会需要`-l`/`--language`参数。 -## Development +## 开发 ```bash -# Recursive clone to retrieve all submodules +# 递归 clone 以获取所有子模块 git clone --recursive https://github.com/sharkdp/bat -# Build (debug version) +# 构建(调试版本) cd bat cargo build --bins -# Run unit tests and integration tests +# 运行单元测试和集成测试 cargo test -# Install (release version) +# 安装(发布版本) cargo install --path . --locked -# Build a bat binary with modified syntaxes and themes +# 使用修改后的语法和主题构建一个 bat 二进制文件 bash assets/create.sh cargo install --path . --locked --force ``` -If you want to build an application that uses `bat`s pretty-printing -features as a library, check out the [the API documentation](https://docs.rs/bat/). -Note that you have to use either `regex-onig` or `regex-fancy` as a feature -when you depend on `bat` as a library. +如果你想构建一个使用 `bat` 美化打印功能的应用程序,请查看 [API 文档](https://docs.rs/bat/)。请注意,当你依赖 `bat` 作为库时,必须使用 `regex-onig` 或 `regex-fancy` 作为特性。 -## Contributing +## 贡献指南 -Take a look at the [`CONTRIBUTING.md`](CONTRIBUTING.md) guide. +请查看 [`CONTRIBUTING.md`](CONTRIBUTING.md) 指南。 -## Maintainers +## 维护者 - [sharkdp](https://github.com/sharkdp) - [eth-p](https://github.com/eth-p) - [keith-hall](https://github.com/keith-hall) - [Enselic](https://github.com/Enselic) -## Security vulnerabilities +## 安全漏洞 -Please contact [David Peter](https://david-peter.de/) via email if you want to report a vulnerability in `bat`. +如果你想报告 `bat` 中的漏洞,请通过邮件联系 [David Peter](https://david-peter.de/)。 -## Project goals and alternatives +## 项目目标和替代方案 -`bat` tries to achieve the following goals: +`bat` 试图实现以下目标: -- Provide beautiful, advanced syntax highlighting -- Integrate with Git to show file modifications -- Be a drop-in replacement for (POSIX) `cat` -- Offer a user-friendly command-line interface +- 提供美观的高级语法高亮 +- 与 Git 集成以显示文件修改 +- 成为 (POSIX) `cat` 的替代品 +- 提供用户友好的命令行界面 -There are a lot of alternatives, if you are looking for similar programs. See -[this document](doc/alternatives.md) for a comparison. +如果你在寻找类似的程序,有很多替代方案。请参阅[本文档](doc/alternatives.md)进行比较。 -## License +## 许可证 -Copyright (c) 2018-2021 [bat-developers](https://github.com/sharkdp/bat). +版权所有 (c) 2018-2021 [bat-developers](https://github.com/sharkdp/bat)。 -`bat` is made available under the terms of either the MIT License or the Apache License 2.0, at your option. +`bat` 可根据 MIT 许可证或 Apache 许可证 2.0 的条款使用,任选其一。 -See the [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) files for license details. +有关许可证的详细信息,请参阅 [LICENSE-APACHE](LICENSE-APACHE) 和 [LICENSE-MIT](LICENSE-MIT) 文件。 diff --git a/doc/long-help.txt b/doc/long-help.txt index d9cdce39..85d595b9 100644 --- a/doc/long-help.txt +++ b/doc/long-help.txt @@ -20,6 +20,13 @@ Options: * unicode (␇, ␊, ␀, ..) * caret (^G, ^J, ^@, ..) + --binary + How to treat binary content. (default: no-printing) + + Possible values: + * no-printing: do not print any binary content + * as-text: treat binary content as normal text + -p, --plain... Only show plain style, no decorations. This is an alias for '--style=plain'. When '-p' is used twice ('-pp'), it also disables automatic paging (alias for '--style=plain @@ -112,6 +119,27 @@ Options: Set the theme for syntax highlighting. Use '--list-themes' to see all available themes. To set a default theme, add the '--theme="..."' option to the configuration file or export the BAT_THEME environment variable (e.g.: export BAT_THEME="..."). + + Special values: + + * auto: Picks a dark or light theme depending on the terminal's colors (default). + Use '--theme-light' and '--theme-dark' to customize the selected theme. + * auto:always: Detect the terminal's colors even when the output is redirected. + * auto:system: Detect the color scheme from the system-wide preference (macOS only). + * dark: Use the dark theme specified by '--theme-dark'. + * light: Use the light theme specified by '--theme-light'. + + --theme-light + Sets the theme name for syntax highlighting used when the terminal uses a light + background. Use '--list-themes' to see all available themes. To set a default theme, add + the '--theme-light="..." option to the configuration file or export the BAT_THEME_LIGHT + environment variable (e.g. export BAT_THEME_LIGHT="..."). + + --theme-dark + Sets the theme name for syntax highlighting used when the terminal uses a dark background. + Use '--list-themes' to see all available themes. To set a default theme, add the + '--theme-dark="..." option to the configuration file or export the BAT_THEME_DARK + environment variable (e.g. export BAT_THEME_DARK="..."). --list-themes Display a list of supported themes for syntax highlighting. @@ -134,6 +162,12 @@ 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=".."). + When styles are specified in multiple places, the "nearest" set of styles take precedence. + The command-line arguments are the highest priority, followed by the BAT_STYLE environment + variable, and then the configuration file. If any set of styles consists entirely of + components prefixed with "+" or "-", it will modify the previous set of styles instead of + replacing them. + By default, the following components are enabled: changes, grid, header-filename, numbers, snip diff --git a/doc/short-help.txt b/doc/short-help.txt index 305bbf3d..ba06ef30 100644 --- a/doc/short-help.txt +++ b/doc/short-help.txt @@ -11,6 +11,8 @@ Options: Show non-printable characters (space, tab, newline, ..). --nonprintable-notation Set notation for non-printable characters. + --binary + How to treat binary content. (default: no-printing) -p, --plain... Show plain style (alias for '--style=plain'). -l, --language @@ -41,6 +43,10 @@ Options: Use the specified syntax for files matching the glob pattern ('*.cpp:C++'). --theme Set the color theme for syntax highlighting. + --theme-light + Sets the color theme for syntax highlighting used for light backgrounds. + --theme-dark + Sets the color theme for syntax highlighting used for dark backgrounds. --list-themes Display all supported highlighting themes. -s, --squeeze-blank diff --git a/doc/sponsors/warp-logo.png b/doc/sponsors/warp-logo.png index 4795a2b9..f99dd38c 100644 Binary files a/doc/sponsors/warp-logo.png and b/doc/sponsors/warp-logo.png differ diff --git a/src/assets.rs b/src/assets.rs index 9655553d..d32ccbd4 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -13,6 +13,7 @@ use crate::error::*; use crate::input::{InputReader, OpenedInput}; use crate::syntax_mapping::ignored_suffixes::IgnoredSuffixes; use crate::syntax_mapping::MappingTarget; +use crate::theme::{default_theme, ColorScheme}; use crate::{bat_warning, SyntaxMapping}; use lazy_theme_set::LazyThemeSet; @@ -69,57 +70,6 @@ impl HighlightingAssets { } } - /// The default theme. - /// - /// ### Windows and Linux - /// - /// Windows and most Linux distributions has a dark terminal theme by - /// default. On these platforms, this function always returns a theme that - /// looks good on a dark background. - /// - /// ### macOS - /// - /// On macOS the default terminal background is light, but it is common that - /// Dark Mode is active, which makes the terminal background dark. On this - /// platform, the default theme depends on - /// ```bash - /// defaults read -globalDomain AppleInterfaceStyle - /// ``` - /// To avoid the overhead of the check on macOS, simply specify a theme - /// explicitly via `--theme`, `BAT_THEME`, or `~/.config/bat`. - /// - /// See and - /// for more context. - pub fn default_theme() -> &'static str { - #[cfg(not(target_os = "macos"))] - { - Self::default_dark_theme() - } - #[cfg(target_os = "macos")] - { - if macos_dark_mode_active() { - Self::default_dark_theme() - } else { - Self::default_light_theme() - } - } - } - - /** - * The default theme that looks good on a dark background. - */ - fn default_dark_theme() -> &'static str { - "Monokai Extended" - } - - /** - * The default theme that looks good on a light background. - */ - #[cfg(target_os = "macos")] - fn default_light_theme() -> &'static str { - "Monokai Extended Light" - } - pub fn from_cache(cache_path: &Path) -> Result { Ok(HighlightingAssets::new( SerializedSyntaxSet::FromFile(cache_path.join("syntaxes.bin")), @@ -248,7 +198,10 @@ impl HighlightingAssets { bat_warning!("Unknown theme '{}', using default.", theme) } self.get_theme_set() - .get(self.fallback_theme.unwrap_or_else(Self::default_theme)) + .get( + self.fallback_theme + .unwrap_or_else(|| default_theme(ColorScheme::Dark)), + ) .expect("something is very wrong if the default theme is missing") } } @@ -399,26 +352,6 @@ fn asset_from_cache( .map_err(|_| format!("Could not parse cached {description}").into()) } -#[cfg(target_os = "macos")] -fn macos_dark_mode_active() -> bool { - const PREFERENCES_FILE: &str = "Library/Preferences/.GlobalPreferences.plist"; - const STYLE_KEY: &str = "AppleInterfaceStyle"; - - let preferences_file = home::home_dir() - .map(|home| home.join(PREFERENCES_FILE)) - .expect("Could not get home directory"); - - match plist::Value::from_file(preferences_file).map(|file| file.into_dictionary()) { - Ok(Some(preferences)) => match preferences.get(STYLE_KEY).and_then(|val| val.as_string()) { - Some(value) => value == "Dark", - // If the key does not exist, then light theme is currently in use. - None => false, - }, - // Unreachable, in theory. All macOS users have a home directory and preferences file setup. - Ok(None) | Err(_) => true, - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 0d2600b2..8f69870f 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -2,11 +2,15 @@ use std::collections::HashSet; use std::env; use std::io::IsTerminal; use std::path::{Path, PathBuf}; +use std::str::FromStr; use crate::{ clap_app, config::{get_args_from_config_file, get_args_from_env_opts_var, get_args_from_env_vars}, }; +use bat::style::StyleComponentList; +use bat::theme::{theme, ThemeName, ThemeOptions, ThemePreference}; +use bat::BinaryBehavior; use bat::StripAnsiMode; use clap::ArgMatches; @@ -14,7 +18,6 @@ use console::Term; use crate::input::{new_file_input, new_stdin_input}; use bat::{ - assets::HighlightingAssets, bat_warning, config::{Config, VisibleLines}, error::*, @@ -86,7 +89,6 @@ impl App { // .. and the rest at the end cli_args.for_each(|a| args.push(a)); - args }; @@ -96,12 +98,30 @@ impl App { pub fn config(&self, inputs: &[Input]) -> Result { let style_components = self.style_components()?; + let extra_plain = self.matches.get_count("plain") > 1; + let plain_last_index = self + .matches + .indices_of("plain") + .and_then(Iterator::max) + .unwrap_or_default(); + let paging_last_index = self + .matches + .indices_of("paging") + .and_then(Iterator::max) + .unwrap_or_default(); + let paging_mode = match self.matches.get_one::("paging").map(|s| s.as_str()) { - Some("always") => PagingMode::Always, + Some("always") => { + // Disable paging if the second -p (or -pp) is specified after --paging=always + if extra_plain && plain_last_index > paging_last_index { + PagingMode::Never + } else { + PagingMode::Always + } + } Some("never") => PagingMode::Never, Some("auto") | None => { // If we have -pp as an option when in auto mode, the pager should be disabled. - let extra_plain = self.matches.get_count("plain") > 1; if extra_plain || self.matches.get_flag("no-paging") { PagingMode::Never } else if inputs.iter().any(Input::is_stdin) { @@ -192,6 +212,11 @@ impl App { Some("caret") => NonprintableNotation::Caret, _ => unreachable!("other values for --nonprintable-notation are not allowed"), }, + binary: match self.matches.get_one::("binary").map(|s| s.as_str()) { + Some("as-text") => BinaryBehavior::AsText, + Some("no-printing") => BinaryBehavior::NoPrinting, + _ => unreachable!("other values for --binary are not allowed"), + }, wrapping_mode: if self.interactive_output || maybe_term_width.is_some() { if !self.matches.get_flag("chop-long-lines") { match self.matches.get_one::("wrap").map(|s| s.as_str()) { @@ -253,18 +278,7 @@ impl App { Some("auto") => StripAnsiMode::Auto, _ => unreachable!("other values for --strip-ansi are not allowed"), }, - theme: self - .matches - .get_one::("theme") - .map(String::from) - .map(|s| { - if s == "default" { - String::from(HighlightingAssets::default_theme()) - } else { - s - } - }) - .unwrap_or_else(|| String::from(HighlightingAssets::default_theme())), + theme: theme(self.theme_options()).to_string(), visible_lines: match self.matches.try_contains_id("diff").unwrap_or_default() && self.matches.get_flag("diff") { @@ -364,34 +378,57 @@ impl App { Ok(file_input) } + fn forced_style_components(&self) -> Option { + // No components if `--decorations=never``. + if self + .matches + .get_one::("decorations") + .map(|s| s.as_str()) + == Some("never") + { + return Some(StyleComponents(HashSet::new())); + } + + // Only line numbers if `--number`. + if self.matches.get_flag("number") { + return Some(StyleComponents(HashSet::from([ + StyleComponent::LineNumbers, + ]))); + } + + // Plain if `--plain` is specified at least once. + if self.matches.get_count("plain") > 0 { + return Some(StyleComponents(HashSet::from([StyleComponent::Plain]))); + } + + // Default behavior. + None + } + fn style_components(&self) -> Result { let matches = &self.matches; - let mut styled_components = StyleComponents( - if matches.get_one::("decorations").map(|s| s.as_str()) == Some("never") { - HashSet::new() - } else if matches.get_flag("number") { - [StyleComponent::LineNumbers].iter().cloned().collect() - } else if 0 < matches.get_count("plain") { - [StyleComponent::Plain].iter().cloned().collect() - } else { - matches - .get_one::("style") - .map(|styles| { - styles - .split(',') - .map(|style| style.parse::()) - .filter_map(|style| style.ok()) - .collect::>() - }) - .unwrap_or_else(|| vec![StyleComponent::Default]) + let mut styled_components = match self.forced_style_components() { + Some(forced_components) => forced_components, + + // Parse the `--style` arguments and merge them. + None if matches.contains_id("style") => { + let lists = matches + .get_many::("style") + .expect("styles present") + .map(|v| StyleComponentList::from_str(v)) + .collect::>>()?; + + StyleComponentList::to_components(lists, self.interactive_output, true) + } + + // Use the default. + None => StyleComponents(HashSet::from_iter( + StyleComponent::Default + .components(self.interactive_output) .into_iter() - .map(|style| style.components(self.interactive_output)) - .fold(HashSet::new(), |mut acc, components| { - acc.extend(components.iter().cloned()); - acc - }) - }, - ); + .cloned(), + )), + }; // If `grid` is set, remove `rule` as it is a subset of `grid`, and print a warning. if styled_components.grid() && styled_components.0.remove(&StyleComponent::Rule) { @@ -400,4 +437,25 @@ impl App { Ok(styled_components) } + + fn theme_options(&self) -> ThemeOptions { + let theme = self + .matches + .get_one::("theme") + .map(|t| ThemePreference::from_str(t).unwrap()) + .unwrap_or_default(); + let theme_dark = self + .matches + .get_one::("theme-dark") + .map(|t| ThemeName::from_str(t).unwrap()); + let theme_light = self + .matches + .get_one::("theme-light") + .map(|t| ThemeName::from_str(t).unwrap()); + ThemeOptions { + theme, + theme_dark, + theme_light, + } + } } diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index e70b1a5b..f5e3948e 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -1,9 +1,11 @@ +use bat::style::StyleComponentList; use clap::{ crate_name, crate_version, value_parser, Arg, ArgAction, ArgGroup, ColorChoice, Command, }; use once_cell::sync::Lazy; use std::env; use std::path::{Path, PathBuf}; +use std::str::FromStr; static VERSION: Lazy = Lazy::new(|| { #[cfg(feature = "bugreport")] @@ -75,11 +77,26 @@ pub fn build_app(interactive_output: bool) -> Command { * caret (^G, ^J, ^@, ..)", ), ) + .arg( + Arg::new("binary") + .long("binary") + .action(ArgAction::Set) + .default_value("no-printing") + .value_parser(["no-printing", "as-text"]) + .value_name("behavior") + .hide_default_value(true) + .help("How to treat binary content. (default: no-printing)") + .long_help( + "How to treat binary content. (default: no-printing)\n\n\ + Possible values:\n \ + * no-printing: do not print any binary content\n \ + * as-text: treat binary content as normal text", + ), + ) .arg( Arg::new("plain") .overrides_with("plain") .overrides_with("number") - .overrides_with("paging") .short('p') .long("plain") .action(ArgAction::Count) @@ -304,7 +321,6 @@ pub fn build_app(interactive_output: bool) -> Command { .long("paging") .overrides_with("paging") .overrides_with("no-paging") - .overrides_with("plain") .value_name("when") .value_parser(["auto", "never", "always"]) .default_value("auto") @@ -377,9 +393,40 @@ pub fn build_app(interactive_output: bool) -> Command { see all available themes. To set a default theme, add the \ '--theme=\"...\"' option to the configuration file or export the \ BAT_THEME environment variable (e.g.: export \ - BAT_THEME=\"...\").", + BAT_THEME=\"...\").\n\n\ + Special values:\n\n \ + * auto: Picks a dark or light theme depending on the terminal's colors (default).\n \ + Use '--theme-light' and '--theme-dark' to customize the selected theme.\n \ + * auto:always: Detect the terminal's colors even when the output is redirected.\n \ + * auto:system: Detect the color scheme from the system-wide preference (macOS only).\n \ + * dark: Use the dark theme specified by '--theme-dark'.\n \ + * light: Use the light theme specified by '--theme-light'.", ), ) + .arg( + Arg::new("theme-light") + .long("theme-light") + .overrides_with("theme-light") + .value_name("theme") + .help("Sets the color theme for syntax highlighting used for light backgrounds.") + .long_help( + "Sets the theme name for syntax highlighting used when the terminal uses a light background. \ + Use '--list-themes' to see all available themes. To set a default theme, add the \ + '--theme-light=\"...\" option to the configuration file or export the BAT_THEME_LIGHT \ + environment variable (e.g. export BAT_THEME_LIGHT=\"...\")."), + ) + .arg( + Arg::new("theme-dark") + .long("theme-dark") + .overrides_with("theme-dark") + .value_name("theme") + .help("Sets the color theme for syntax highlighting used for dark backgrounds.") + .long_help( + "Sets the theme name for syntax highlighting used when the terminal uses a dark background. \ + Use '--list-themes' to see all available themes. To set a default theme, add the \ + '--theme-dark=\"...\" option to the configuration file or export the BAT_THEME_DARK \ + environment variable (e.g. export BAT_THEME_DARK=\"...\")."), + ) .arg( Arg::new("list-themes") .long("list-themes") @@ -419,34 +466,13 @@ pub fn build_app(interactive_output: bool) -> Command { .arg( Arg::new("style") .long("style") + .action(ArgAction::Append) .value_name("components") - .overrides_with("style") - .overrides_with("plain") - .overrides_with("number") // Cannot use claps built in validation because we have to turn off clap's delimiters .value_parser(|val: &str| { - let mut invalid_vals = val.split(',').filter(|style| { - !&[ - "auto", - "full", - "default", - "plain", - "header", - "header-filename", - "header-filesize", - "grid", - "rule", - "numbers", - "snip", - #[cfg(feature = "git")] - "changes", - ].contains(style) - }); - - if let Some(invalid) = invalid_vals.next() { - Err(format!("Unknown style, '{invalid}'")) - } else { - Ok(val.to_owned()) + match StyleComponentList::from_str(val) { + Err(err) => Err(err), + Ok(_) => Ok(val.to_owned()), } }) .help( @@ -461,6 +487,12 @@ 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\ + When styles are specified in multiple places, the \"nearest\" set \ + of styles take precedence. The command-line arguments are the highest \ + priority, followed by the BAT_STYLE environment variable, and then \ + the configuration file. If any set of styles consists entirely of \ + components prefixed with \"+\" or \"-\", it will modify the \ + previous set of styles instead of replacing them.\n\n\ By default, the following components are enabled:\n \ changes, grid, header-filename, numbers, snip\n\n\ Possible values:\n\n \ diff --git a/src/bin/bat/config.rs b/src/bin/bat/config.rs index 9e38dfa4..a0ee7ba3 100644 --- a/src/bin/bat/config.rs +++ b/src/bin/bat/config.rs @@ -140,14 +140,19 @@ fn get_args_from_str(content: &str) -> Result, shell_words::ParseE pub fn get_args_from_env_vars() -> Vec { [ ("--tabs", "BAT_TABS"), - ("--theme", "BAT_THEME"), + ("--theme", bat::theme::env::BAT_THEME), + ("--theme-dark", bat::theme::env::BAT_THEME_DARK), + ("--theme-light", bat::theme::env::BAT_THEME_LIGHT), ("--pager", "BAT_PAGER"), ("--paging", "BAT_PAGING"), ("--style", "BAT_STYLE"), ] .iter() - .filter_map(|(flag, key)| env::var(key).ok().map(|var| [flag.to_string(), var])) - .flatten() + .filter_map(|(flag, key)| { + env::var(key) + .ok() + .map(|var| [flag.to_string(), var].join("=")) + }) .map(|a| a.into()) .collect() } diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 4528a60b..7b7bafe6 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -14,6 +14,7 @@ use std::io::{BufReader, Write}; use std::path::Path; use std::process; +use bat::theme::DetectColorScheme; use nu_ansi_term::Color::Green; use nu_ansi_term::Style; @@ -30,12 +31,12 @@ use directories::PROJECT_DIRS; use globset::GlobMatcher; use bat::{ - assets::HighlightingAssets, config::Config, controller::Controller, error::*, input::Input, style::{StyleComponent, StyleComponents}, + theme::{color_scheme, default_theme, ColorScheme}, MappingTarget, PagingMode, }; @@ -189,7 +190,12 @@ fn theme_preview_file<'a>() -> Input<'a> { Input::from_reader(Box::new(BufReader::new(THEME_PREVIEW_DATA))) } -pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result<()> { +pub fn list_themes( + cfg: &Config, + config_dir: &Path, + cache_dir: &Path, + detect_color_scheme: DetectColorScheme, +) -> Result<()> { let assets = assets_from_cache_or_binary(cfg.use_custom_assets, cache_dir)?; let mut config = cfg.clone(); let mut style = HashSet::new(); @@ -200,10 +206,14 @@ pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result< let stdout = io::stdout(); let mut stdout = stdout.lock(); - let default_theme = HighlightingAssets::default_theme(); + let default_theme_name = default_theme(color_scheme(detect_color_scheme).unwrap_or_default()); for theme in assets.themes() { - let default_theme_info = if default_theme == theme { + let default_theme_info = if !config.loop_through && default_theme_name == theme { " (default)" + } else if default_theme(ColorScheme::Dark) == theme { + " (default dark)" + } else if default_theme(ColorScheme::Light) == theme { + " (default light)" } else { "" }; @@ -371,7 +381,7 @@ fn run() -> Result { }; run_controller(inputs, &plain_config, cache_dir) } else if app.matches.get_flag("list-themes") { - list_themes(&config, config_dir, cache_dir)?; + list_themes(&config, config_dir, cache_dir, DetectColorScheme::default())?; Ok(true) } else if app.matches.get_flag("config-file") { println!("{}", config_file().to_string_lossy()); diff --git a/src/config.rs b/src/config.rs index eb7df8ee..eb44281c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,5 @@ use crate::line_range::{HighlightedLineRanges, LineRanges}; -use crate::nonprintable_notation::NonprintableNotation; +use crate::nonprintable_notation::{BinaryBehavior, NonprintableNotation}; #[cfg(feature = "paging")] use crate::paging::PagingMode; use crate::style::StyleComponents; @@ -44,6 +44,9 @@ pub struct Config<'a> { /// The configured notation for non-printable characters pub nonprintable_notation: NonprintableNotation, + /// How to treat binary content + pub binary: BinaryBehavior, + /// The character width of the terminal pub term_width: usize, diff --git a/src/lessopen.rs b/src/lessopen.rs index c8f5225d..4d7e0ece 100644 --- a/src/lessopen.rs +++ b/src/lessopen.rs @@ -112,7 +112,7 @@ impl LessOpenPreprocessor { } ( - RawOsString::from_string(lessopen_stdout), + RawOsString::new(lessopen_stdout), path_str.to_string(), OpenedInputKind::OrdinaryFile(path.to_path_buf()), ) diff --git a/src/lib.rs b/src/lib.rs index 23c4a800..502427a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,10 +49,11 @@ pub(crate) mod printer; pub mod style; pub(crate) mod syntax_mapping; mod terminal; +pub mod theme; mod vscreen; pub(crate) mod wrapping; -pub use nonprintable_notation::NonprintableNotation; +pub use nonprintable_notation::{BinaryBehavior, NonprintableNotation}; pub use preprocessor::StripAnsiMode; pub use pretty_printer::{Input, PrettyPrinter, Syntax}; pub use syntax_mapping::{MappingTarget, SyntaxMapping}; diff --git a/src/nonprintable_notation.rs b/src/nonprintable_notation.rs index ff09aca6..9f8d7cb8 100644 --- a/src/nonprintable_notation.rs +++ b/src/nonprintable_notation.rs @@ -10,3 +10,15 @@ pub enum NonprintableNotation { #[default] Unicode, } + +/// How to treat binary content +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum BinaryBehavior { + /// Do not print any binary content + #[default] + NoPrinting, + + /// Treat binary content as normal text + AsText, +} diff --git a/src/pretty_printer.rs b/src/pretty_printer.rs index eb123ea3..a70ac021 100644 --- a/src/pretty_printer.rs +++ b/src/pretty_printer.rs @@ -245,7 +245,9 @@ impl<'a> PrettyPrinter<'a> { self } - /// Specify the highlighting theme + /// Specify the highlighting theme. + /// You can use [`crate::theme::theme`] to pick a theme based on user preferences + /// and the terminal's background color. pub fn theme(&mut self, theme: impl AsRef) -> &mut Self { self.config.theme = theme.as_ref().to_owned(); self @@ -279,6 +281,11 @@ impl<'a> PrettyPrinter<'a> { /// If you want to call 'print' multiple times, you have to call the appropriate /// input_* methods again. pub fn print(&mut self) -> Result { + self.print_with_writer(None::<&mut dyn std::fmt::Write>) + } + + /// Pretty-print all specified inputs to a specified writer. + pub fn print_with_writer(&mut self, writer: Option) -> Result { let highlight_lines = std::mem::take(&mut self.highlighted_lines); self.config.highlighted_lines = HighlightedLineRanges(LineRanges::from(highlight_lines)); self.config.term_width = self @@ -315,7 +322,13 @@ impl<'a> PrettyPrinter<'a> { // Run the controller let controller = Controller::new(&self.config, &self.assets); - controller.run(inputs.into_iter().map(|i| i.into()).collect(), None) + + // If writer is provided, pass it to the controller, otherwise pass None + if let Some(mut w) = writer { + controller.run(inputs.into_iter().map(|i| i.into()).collect(), Some(&mut w)) + } else { + controller.run(inputs.into_iter().map(|i| i.into()).collect(), None) + } } } diff --git a/src/printer.rs b/src/printer.rs index e9bea3fd..95017188 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -35,6 +35,7 @@ use crate::style::StyleComponent; use crate::terminal::{as_terminal_escaped, to_ansi_color}; use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator}; use crate::wrapping::WrappingMode; +use crate::BinaryBehavior; use crate::StripAnsiMode; const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI { @@ -268,7 +269,8 @@ impl<'a> InteractivePrinter<'a> { .content_type .map_or(false, |c| c.is_binary() && !config.show_nonprintable); - let needs_to_match_syntax = !is_printing_binary + let needs_to_match_syntax = (!is_printing_binary + || matches!(config.binary, BinaryBehavior::AsText)) && (config.colored_output || config.strip_ansi == StripAnsiMode::Auto); let (is_plain_text, highlighter_from_set) = if needs_to_match_syntax { @@ -458,7 +460,10 @@ impl<'a> Printer for InteractivePrinter<'a> { } if !self.config.style_components.header() { - if Some(ContentType::BINARY) == self.content_type && !self.config.show_nonprintable { + if Some(ContentType::BINARY) == self.content_type + && !self.config.show_nonprintable + && !matches!(self.config.binary, BinaryBehavior::AsText) + { writeln!( handle, "{}: Binary content from {} will not be printed to the terminal \ @@ -539,7 +544,10 @@ impl<'a> Printer for InteractivePrinter<'a> { })?; if self.config.style_components.grid() { - if self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable { + if self.content_type.map_or(false, |c| c.is_text()) + || self.config.show_nonprintable + || matches!(self.config.binary, BinaryBehavior::AsText) + { self.print_horizontal_line(handle, '┼')?; } else { self.print_horizontal_line(handle, '┴')?; @@ -551,7 +559,9 @@ impl<'a> Printer for InteractivePrinter<'a> { fn print_footer(&mut self, handle: &mut OutputHandle, _input: &OpenedInput) -> Result<()> { if self.config.style_components.grid() - && (self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable) + && (self.content_type.map_or(false, |c| c.is_text()) + || self.config.show_nonprintable + || matches!(self.config.binary, BinaryBehavior::AsText)) { self.print_horizontal_line(handle, '┴') } else { @@ -599,7 +609,9 @@ impl<'a> Printer for InteractivePrinter<'a> { .into() } else { let mut line = match self.content_type { - Some(ContentType::BINARY) | None => { + Some(ContentType::BINARY) | None + if !matches!(self.config.binary, BinaryBehavior::AsText) => + { return Ok(()); } Some(ContentType::UTF_16LE) => UTF_16LE.decode_with_bom_removal(line_buffer).0, diff --git a/src/style.rs b/src/style.rs index 652b3743..b8d8b09f 100644 --- a/src/style.rs +++ b/src/style.rs @@ -138,3 +138,227 @@ impl StyleComponents { self.0.clear(); } } + +#[derive(Debug, PartialEq)] +enum ComponentAction { + Override, + Add, + Remove, +} + +impl ComponentAction { + fn extract_from_str(string: &str) -> (ComponentAction, &str) { + match string.chars().next() { + Some('-') => (ComponentAction::Remove, string.strip_prefix('-').unwrap()), + Some('+') => (ComponentAction::Add, string.strip_prefix('+').unwrap()), + _ => (ComponentAction::Override, string), + } + } +} + +/// A list of [StyleComponent] that can be parsed from a string. +pub struct StyleComponentList(Vec<(ComponentAction, StyleComponent)>); + +impl StyleComponentList { + fn expand_into(&self, components: &mut HashSet, interactive_terminal: bool) { + for (action, component) in self.0.iter() { + let subcomponents = component.components(interactive_terminal); + + use ComponentAction::*; + match action { + Override | Add => components.extend(subcomponents), + Remove => components.retain(|c| !subcomponents.contains(c)), + } + } + } + + /// Returns `true` if any component in the list was not prefixed with `+` or `-`. + fn contains_override(&self) -> bool { + self.0.iter().any(|(a, _)| *a == ComponentAction::Override) + } + + /// Combines multiple [StyleComponentList]s into a single [StyleComponents] set. + /// + /// ## Precedence + /// The most recent list will take precedence and override all previous lists + /// unless it only contains components prefixed with `-` or `+`. When this + /// happens, the list's components will be merged into the previous list. + /// + /// ## Example + /// ```text + /// [numbers,grid] + [header,changes] -> [header,changes] + /// [numbers,grid] + [+header,-grid] -> [numbers,header] + /// ``` + /// + /// ## Parameters + /// - `with_default`: If true, the styles lists will build upon the StyleComponent::Auto style. + pub fn to_components( + lists: impl IntoIterator, + interactive_terminal: bool, + with_default: bool, + ) -> StyleComponents { + let mut components: HashSet = HashSet::new(); + if with_default { + components.extend(StyleComponent::Auto.components(interactive_terminal)) + } + + StyleComponents(lists.into_iter().fold(components, |mut components, list| { + if list.contains_override() { + components.clear(); + } + + list.expand_into(&mut components, interactive_terminal); + components + })) + } +} + +impl Default for StyleComponentList { + fn default() -> Self { + StyleComponentList(vec![(ComponentAction::Override, StyleComponent::Default)]) + } +} + +impl FromStr for StyleComponentList { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(StyleComponentList( + s.split(",") + .map(|s| ComponentAction::extract_from_str(s)) // If the component starts with "-", it's meant to be removed + .map(|(a, s)| Ok((a, StyleComponent::from_str(s)?))) + .collect::>>()?, + )) + } +} + +#[cfg(test)] +mod test { + use std::collections::HashSet; + use std::str::FromStr; + + use super::ComponentAction::*; + use super::StyleComponent; + use super::StyleComponent::*; + use super::StyleComponentList; + + #[test] + pub fn style_component_list_parse() { + assert_eq!( + StyleComponentList::from_str("grid,+numbers,snip,-snip,header") + .expect("no error") + .0, + vec![ + (Override, Grid), + (Add, LineNumbers), + (Override, Snip), + (Remove, Snip), + (Override, Header), + ] + ); + + assert!(StyleComponentList::from_str("not-a-component").is_err()); + assert!(StyleComponentList::from_str("grid,not-a-component").is_err()); + assert!(StyleComponentList::from_str("numbers,-not-a-component").is_err()); + } + + #[test] + pub fn style_component_list_to_components() { + assert_eq!( + StyleComponentList::to_components( + vec![StyleComponentList::from_str("grid,numbers").expect("no error")], + false, + false + ) + .0, + HashSet::from([Grid, LineNumbers]) + ); + } + + #[test] + pub fn style_component_list_to_components_removes_negated() { + assert_eq!( + StyleComponentList::to_components( + vec![StyleComponentList::from_str("grid,numbers,-grid").expect("no error")], + false, + false + ) + .0, + HashSet::from([LineNumbers]) + ); + } + + #[test] + pub fn style_component_list_to_components_expands_subcomponents() { + assert_eq!( + StyleComponentList::to_components( + vec![StyleComponentList::from_str("full").expect("no error")], + false, + false + ) + .0, + HashSet::from_iter(Full.components(true).to_owned()) + ); + } + + #[test] + pub fn style_component_list_expand_negates_subcomponents() { + assert!(!StyleComponentList::to_components( + vec![StyleComponentList::from_str("full,-numbers").expect("no error")], + true, + false + ) + .numbers()); + } + + #[test] + pub fn style_component_list_to_components_precedence_overrides_previous_lists() { + assert_eq!( + StyleComponentList::to_components( + vec![ + StyleComponentList::from_str("grid").expect("no error"), + StyleComponentList::from_str("numbers").expect("no error"), + ], + false, + false + ) + .0, + HashSet::from([LineNumbers]) + ); + } + + #[test] + pub fn style_component_list_to_components_precedence_merges_previous_lists() { + assert_eq!( + StyleComponentList::to_components( + vec![ + StyleComponentList::from_str("grid,header").expect("no error"), + StyleComponentList::from_str("-grid").expect("no error"), + StyleComponentList::from_str("+numbers").expect("no error"), + ], + false, + false + ) + .0, + HashSet::from([HeaderFilename, LineNumbers]) + ); + } + + #[test] + pub fn style_component_list_default_builds_on_auto() { + assert_eq!( + StyleComponentList::to_components( + vec![StyleComponentList::from_str("-numbers").expect("no error"),], + true, + true + ) + .0, + { + let mut expected: HashSet = HashSet::new(); + expected.extend(Auto.components(true)); + expected.remove(&LineNumbers); + expected + } + ); + } +} diff --git a/src/syntax_mapping/builtins/common/50-citation.toml b/src/syntax_mapping/builtins/common/50-citation.toml new file mode 100644 index 00000000..aa06b5b9 --- /dev/null +++ b/src/syntax_mapping/builtins/common/50-citation.toml @@ -0,0 +1,2 @@ +[mappings] +"YAML" = ["CITATION.cff"] diff --git a/src/syntax_mapping/builtins/common/50-diff.toml b/src/syntax_mapping/builtins/common/50-diff.toml new file mode 100644 index 00000000..2998d9c5 --- /dev/null +++ b/src/syntax_mapping/builtins/common/50-diff.toml @@ -0,0 +1,3 @@ +# .debdiff is the extension used for diffs in Debian packaging +[mappings] +"Diff" = ["*.debdiff"] diff --git a/src/syntax_mapping/builtins/common/50-json.toml b/src/syntax_mapping/builtins/common/50-json.toml index e604868a..3198c4f3 100644 --- a/src/syntax_mapping/builtins/common/50-json.toml +++ b/src/syntax_mapping/builtins/common/50-json.toml @@ -1,3 +1,3 @@ # JSON Lines is a simple variation of JSON #2535 [mappings] -"JSON" = ["*.jsonl", "*.jsonc"] +"JSON" = ["*.jsonl", "*.jsonc", "*.jsonld", "*.geojson"] diff --git a/src/syntax_mapping/builtins/common/50-markdown.toml b/src/syntax_mapping/builtins/common/50-markdown.toml new file mode 100644 index 00000000..aa0531d2 --- /dev/null +++ b/src/syntax_mapping/builtins/common/50-markdown.toml @@ -0,0 +1,2 @@ +[mappings] +"Markdown" = ["*.mkd"] diff --git a/src/syntax_mapping/builtins/linux/50-kubernetes.toml b/src/syntax_mapping/builtins/linux/50-kubernetes.toml new file mode 100644 index 00000000..6a81a35a --- /dev/null +++ b/src/syntax_mapping/builtins/linux/50-kubernetes.toml @@ -0,0 +1,2 @@ +[mappings] +"YAML" = ["/etc/kubernetes/*.conf"] diff --git a/src/syntax_mapping/builtins/linux/50-pacman.toml b/src/syntax_mapping/builtins/linux/50-pacman.toml index 655118c5..2f4ee71f 100644 --- a/src/syntax_mapping/builtins/linux/50-pacman.toml +++ b/src/syntax_mapping/builtins/linux/50-pacman.toml @@ -1,3 +1,8 @@ [mappings] -# pacman hooks -"INI" = ["/usr/share/libalpm/hooks/*.hook", "/etc/pacman.d/hooks/*.hook"] +"INI" = [ + # config + "/etc/pacman.conf", + # hooks + "/usr/share/libalpm/hooks/*.hook", + "/etc/pacman.d/hooks/*.hook", +] diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 00000000..5863bf85 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,571 @@ +//! Utilities for choosing an appropriate theme for syntax highlighting. + +use std::convert::Infallible; +use std::fmt; +use std::io::IsTerminal as _; +use std::str::FromStr; + +/// Environment variable names. +pub mod env { + /// See [`crate::theme::ThemeOptions::theme`]. + pub const BAT_THEME: &str = "BAT_THEME"; + /// See [`crate::theme::ThemeOptions::theme_dark`]. + pub const BAT_THEME_DARK: &str = "BAT_THEME"; + /// See [`crate::theme::ThemeOptions::theme_light`]. + pub const BAT_THEME_LIGHT: &str = "BAT_THEME"; +} + +/// Chooses an appropriate theme or falls back to a default theme +/// based on the user-provided options and the color scheme of the terminal. +/// +/// Intentionally returns a [`ThemeResult`] instead of a simple string so +/// that downstream consumers such as `delta` can easily apply their own +/// default theme and can use the detected color scheme elsewhere. +pub fn theme(options: ThemeOptions) -> ThemeResult { + theme_impl(options, &TerminalColorSchemeDetector) +} + +/// The default theme, suitable for the given color scheme. +/// Use [`theme`] if you want to automatically detect the color scheme from the terminal. +pub const fn default_theme(color_scheme: ColorScheme) -> &'static str { + match color_scheme { + ColorScheme::Dark => "Monokai Extended", + ColorScheme::Light => "Monokai Extended Light", + } +} + +/// Detects the color scheme from the terminal. +pub fn color_scheme(when: DetectColorScheme) -> Option { + color_scheme_impl(when, &TerminalColorSchemeDetector) +} + +/// Options for configuring the theme used for syntax highlighting. +/// Used together with [`theme`]. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ThemeOptions { + /// Configures how the theme is chosen. If set to a [`ThemePreference::Fixed`] value, + /// then the given theme is used regardless of the terminal's background color. + /// This corresponds with the `BAT_THEME` environment variable and the `--theme` option. + pub theme: ThemePreference, + /// The theme to use in case the terminal uses a dark background with light text. + /// This corresponds with the `BAT_THEME_DARK` environment variable and the `--theme-dark` option. + pub theme_dark: Option, + /// The theme to use in case the terminal uses a light background with dark text. + /// This corresponds with the `BAT_THEME_LIGHT` environment variable and the `--theme-light` option. + pub theme_light: Option, +} + +/// What theme should `bat` use? +/// +/// The easiest way to construct this is from a string: +/// ``` +/// # use bat::theme::{ThemePreference, DetectColorScheme}; +/// let preference = ThemePreference::new("auto:system"); +/// assert_eq!(ThemePreference::Auto(DetectColorScheme::System), preference); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ThemePreference { + /// Choose between [`ThemeOptions::theme_dark`] and [`ThemeOptions::theme_light`] + /// based on the terminal's color scheme. + Auto(DetectColorScheme), + /// Always use the same theme regardless of the terminal's color scheme. + Fixed(ThemeName), + /// Use a dark theme. + Dark, + /// Use a light theme. + Light, +} + +impl Default for ThemePreference { + fn default() -> Self { + ThemePreference::Auto(Default::default()) + } +} + +impl ThemePreference { + /// Creates a theme preference from a string. + pub fn new(s: impl Into) -> Self { + use ThemePreference::*; + let s = s.into(); + match s.as_str() { + "auto" => Auto(Default::default()), + "auto:always" => Auto(DetectColorScheme::Always), + "auto:system" => Auto(DetectColorScheme::System), + "dark" => Dark, + "light" => Light, + _ => Fixed(ThemeName::new(s)), + } + } +} + +impl FromStr for ThemePreference { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(ThemePreference::new(s)) + } +} + +impl fmt::Display for ThemePreference { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use ThemePreference::*; + match self { + Auto(DetectColorScheme::Auto) => f.write_str("auto"), + Auto(DetectColorScheme::Always) => f.write_str("auto:always"), + Auto(DetectColorScheme::System) => f.write_str("auto:system"), + Fixed(theme) => theme.fmt(f), + Dark => f.write_str("dark"), + Light => f.write_str("light"), + } + } +} + +/// The name of a theme or the default theme. +/// +/// ``` +/// # use bat::theme::ThemeName; +/// assert_eq!(ThemeName::Default, ThemeName::new("default")); +/// assert_eq!(ThemeName::Named("example".to_string()), ThemeName::new("example")); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ThemeName { + Named(String), + Default, +} + +impl ThemeName { + /// Creates a theme name from a string. + pub fn new(s: impl Into) -> Self { + let s = s.into(); + if s == "default" { + ThemeName::Default + } else { + ThemeName::Named(s) + } + } +} + +impl FromStr for ThemeName { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(ThemeName::new(s)) + } +} + +impl fmt::Display for ThemeName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ThemeName::Named(t) => f.write_str(t), + ThemeName::Default => f.write_str("default"), + } + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DetectColorScheme { + /// Only query the terminal for its colors when appropriate (i.e. when the output is not redirected). + #[default] + Auto, + /// Always query the terminal for its colors. + Always, + /// Detect the system-wide dark/light preference (macOS only). + System, +} + +/// The color scheme used to pick a fitting theme. Defaults to [`ColorScheme::Dark`]. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ColorScheme { + #[default] + Dark, + Light, +} + +/// The resolved theme and the color scheme as determined from +/// the terminal, OS or fallback. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ThemeResult { + /// The theme selected according to the [`ThemeOptions`]. + pub theme: ThemeName, + /// Either the user's chosen color scheme, the terminal's color scheme, the OS's + /// color scheme or `None` if the color scheme was not detected because the user chose a fixed theme. + pub color_scheme: Option, +} + +impl fmt::Display for ThemeResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.theme { + ThemeName::Named(name) => f.write_str(name), + ThemeName::Default => f.write_str(default_theme(self.color_scheme.unwrap_or_default())), + } + } +} + +fn theme_impl(options: ThemeOptions, detector: &dyn ColorSchemeDetector) -> ThemeResult { + // Implementation note: This function is mostly pure (i.e. it has no side effects) for the sake of testing. + // All the side effects (e.g. querying the terminal for its colors) are performed in the detector. + match options.theme { + ThemePreference::Fixed(theme) => ThemeResult { + theme, + color_scheme: None, + }, + ThemePreference::Dark => choose_theme_opt(Some(ColorScheme::Dark), options), + ThemePreference::Light => choose_theme_opt(Some(ColorScheme::Light), options), + ThemePreference::Auto(when) => choose_theme_opt(color_scheme_impl(when, detector), options), + } +} + +fn choose_theme_opt(color_scheme: Option, options: ThemeOptions) -> ThemeResult { + ThemeResult { + color_scheme, + theme: color_scheme + .and_then(|c| choose_theme(options, c)) + .unwrap_or(ThemeName::Default), + } +} + +fn choose_theme(options: ThemeOptions, color_scheme: ColorScheme) -> Option { + match color_scheme { + ColorScheme::Dark => options.theme_dark, + ColorScheme::Light => options.theme_light, + } +} + +fn color_scheme_impl( + when: DetectColorScheme, + detector: &dyn ColorSchemeDetector, +) -> Option { + let should_detect = match when { + DetectColorScheme::Auto => detector.should_detect(), + DetectColorScheme::Always => true, + DetectColorScheme::System => return color_scheme_from_system(), + }; + should_detect.then(|| detector.detect()).flatten() +} + +trait ColorSchemeDetector { + fn should_detect(&self) -> bool; + + fn detect(&self) -> Option; +} + +struct TerminalColorSchemeDetector; + +impl ColorSchemeDetector for TerminalColorSchemeDetector { + fn should_detect(&self) -> bool { + // Querying the terminal for its colors via OSC 10 / OSC 11 requires "exclusive" access + // since we read/write from the terminal and enable/disable raw mode. + // This causes race conditions with pagers such as less when they are attached to the + // same terminal as us. + // + // This is usually only an issue when the output is manually piped to a pager. + // For example: `bat Cargo.toml | less`. + // Otherwise, if we start the pager ourselves, then there's no race condition + // since the pager is started *after* the color is detected. + std::io::stdout().is_terminal() + } + + fn detect(&self) -> Option { + use terminal_colorsaurus::{color_scheme, ColorScheme as ColorsaurusScheme, QueryOptions}; + match color_scheme(QueryOptions::default()).ok()? { + ColorsaurusScheme::Dark => Some(ColorScheme::Dark), + ColorsaurusScheme::Light => Some(ColorScheme::Light), + } + } +} + +#[cfg(not(target_os = "macos"))] +fn color_scheme_from_system() -> Option { + crate::bat_warning!( + "Theme 'auto:system' is only supported on macOS, \ + using default." + ); + None +} + +#[cfg(target_os = "macos")] +fn color_scheme_from_system() -> Option { + const PREFERENCES_FILE: &str = "Library/Preferences/.GlobalPreferences.plist"; + const STYLE_KEY: &str = "AppleInterfaceStyle"; + + let preferences_file = home::home_dir() + .map(|home| home.join(PREFERENCES_FILE)) + .expect("Could not get home directory"); + + match plist::Value::from_file(preferences_file).map(|file| file.into_dictionary()) { + Ok(Some(preferences)) => match preferences.get(STYLE_KEY).and_then(|val| val.as_string()) { + Some("Dark") => Some(ColorScheme::Dark), + // If the key does not exist, then light theme is currently in use. + Some(_) | None => Some(ColorScheme::Light), + }, + // Unreachable, in theory. All macOS users have a home directory and preferences file setup. + Ok(None) | Err(_) => None, + } +} + +#[cfg(test)] +impl ColorSchemeDetector for Option { + fn should_detect(&self) -> bool { + true + } + + fn detect(&self) -> Option { + *self + } +} + +#[cfg(test)] +mod tests { + use super::ColorScheme::*; + use super::*; + use std::cell::Cell; + use std::iter; + + mod color_scheme_detection { + use super::*; + + #[test] + fn not_called_for_dark_or_light() { + for theme in [ThemePreference::Dark, ThemePreference::Light] { + let detector = DetectorStub::should_detect(Some(Dark)); + let options = ThemeOptions { + theme, + ..Default::default() + }; + _ = theme_impl(options, &detector); + assert!(!detector.was_called.get()); + } + } + + #[test] + fn called_for_always() { + let detectors = [ + DetectorStub::should_detect(Some(Dark)), + DetectorStub::should_not_detect(), + ]; + for detector in detectors { + let options = ThemeOptions { + theme: ThemePreference::Auto(DetectColorScheme::Always), + ..Default::default() + }; + _ = theme_impl(options, &detector); + assert!(detector.was_called.get()); + } + } + + #[test] + fn called_for_auto_if_should_detect() { + let detector = DetectorStub::should_detect(Some(Dark)); + _ = theme_impl(ThemeOptions::default(), &detector); + assert!(detector.was_called.get()); + } + + #[test] + fn not_called_for_auto_if_not_should_detect() { + let detector = DetectorStub::should_not_detect(); + _ = theme_impl(ThemeOptions::default(), &detector); + assert!(!detector.was_called.get()); + } + } + + mod precedence { + use super::*; + + #[test] + fn theme_is_preferred_over_light_or_dark_themes() { + for color_scheme in optional(color_schemes()) { + for options in [ + ThemeOptions { + theme: ThemePreference::Fixed(ThemeName::Named("Theme".to_string())), + ..Default::default() + }, + ThemeOptions { + theme: ThemePreference::Fixed(ThemeName::Named("Theme".to_string())), + theme_dark: Some(ThemeName::Named("Dark Theme".to_string())), + theme_light: Some(ThemeName::Named("Light Theme".to_string())), + ..Default::default() + }, + ] { + let detector = ConstantDetector(color_scheme); + assert_eq!("Theme", theme_impl(options, &detector).to_string()); + } + } + } + + #[test] + fn detector_is_not_called_if_theme_is_present() { + let options = ThemeOptions { + theme: ThemePreference::Fixed(ThemeName::Named("Theme".to_string())), + ..Default::default() + }; + let detector = DetectorStub::should_detect(Some(Dark)); + _ = theme_impl(options, &detector); + assert!(!detector.was_called.get()); + } + } + + mod default_theme { + use super::*; + + #[test] + fn default_dark_if_unable_to_detect_color_scheme() { + let detector = ConstantDetector(None); + assert_eq!( + default_theme(ColorScheme::Dark), + theme_impl(ThemeOptions::default(), &detector).to_string() + ); + } + + // For backwards compatibility, if the default theme is requested + // explicitly through BAT_THEME, we always pick the default dark theme. + #[test] + fn default_dark_if_requested_explicitly_through_theme() { + for color_scheme in optional(color_schemes()) { + let options = ThemeOptions { + theme: ThemePreference::Fixed(ThemeName::Default), + ..Default::default() + }; + let detector = ConstantDetector(color_scheme); + assert_eq!( + default_theme(ColorScheme::Dark), + theme_impl(options, &detector).to_string() + ); + } + } + + #[test] + fn varies_depending_on_color_scheme() { + for color_scheme in color_schemes() { + for options in [ + ThemeOptions::default(), + ThemeOptions { + theme_dark: Some(ThemeName::Default), + theme_light: Some(ThemeName::Default), + ..Default::default() + }, + ] { + let detector = ConstantDetector(Some(color_scheme)); + assert_eq!( + default_theme(color_scheme), + theme_impl(options, &detector).to_string() + ); + } + } + } + } + + mod choosing { + use super::*; + + #[test] + fn chooses_default_theme_if_unknown() { + let options = ThemeOptions { + theme_dark: Some(ThemeName::Named("Dark".to_string())), + theme_light: Some(ThemeName::Named("Light".to_string())), + ..Default::default() + }; + let detector = ConstantDetector(None); + assert_eq!( + default_theme(ColorScheme::default()), + theme_impl(options, &detector).to_string() + ); + } + + #[test] + fn chooses_dark_theme_if_dark_or_unknown() { + let options = ThemeOptions { + theme_dark: Some(ThemeName::Named("Dark".to_string())), + theme_light: Some(ThemeName::Named("Light".to_string())), + ..Default::default() + }; + let detector = ConstantDetector(Some(ColorScheme::Dark)); + assert_eq!("Dark", theme_impl(options, &detector).to_string()); + } + + #[test] + fn chooses_light_theme_if_light() { + let options = ThemeOptions { + theme_dark: Some(ThemeName::Named("Dark".to_string())), + theme_light: Some(ThemeName::Named("Light".to_string())), + ..Default::default() + }; + let detector = ConstantDetector(Some(ColorScheme::Light)); + assert_eq!("Light", theme_impl(options, &detector).to_string()); + } + } + + mod theme_preference { + use super::*; + + #[test] + fn values_roundtrip_via_display() { + let prefs = [ + ThemePreference::Auto(DetectColorScheme::Auto), + ThemePreference::Auto(DetectColorScheme::Always), + ThemePreference::Auto(DetectColorScheme::System), + ThemePreference::Fixed(ThemeName::Default), + ThemePreference::Fixed(ThemeName::new("foo")), + ThemePreference::Dark, + ThemePreference::Light, + ]; + for pref in prefs { + assert_eq!(pref, ThemePreference::new(&pref.to_string())); + } + } + } + + struct DetectorStub { + should_detect: bool, + color_scheme: Option, + was_called: Cell, + } + + impl DetectorStub { + fn should_detect(color_scheme: Option) -> Self { + DetectorStub { + should_detect: true, + color_scheme, + was_called: Cell::default(), + } + } + + fn should_not_detect() -> Self { + DetectorStub { + should_detect: false, + color_scheme: None, + was_called: Cell::default(), + } + } + } + + impl ColorSchemeDetector for DetectorStub { + fn should_detect(&self) -> bool { + self.should_detect + } + + fn detect(&self) -> Option { + self.was_called.set(true); + self.color_scheme + } + } + + struct ConstantDetector(Option); + + impl ColorSchemeDetector for ConstantDetector { + fn should_detect(&self) -> bool { + true + } + + fn detect(&self) -> Option { + self.0 + } + } + + fn optional(value: impl Iterator) -> impl Iterator> { + value.map(Some).chain(iter::once(None)) + } + + fn color_schemes() -> impl Iterator { + [Dark, Light].into_iter() + } +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index d6009361..cd5c0846 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -9,7 +9,6 @@ use tempfile::tempdir; mod unix { pub use std::fs::File; pub use std::io::{self, Write}; - pub use std::os::unix::io::FromRawFd; pub use std::path::PathBuf; pub use std::process::Stdio; pub use std::thread; @@ -274,11 +273,8 @@ fn squeeze_limit_line_numbers() { #[test] fn list_themes_with_colors() { - #[cfg(target_os = "macos")] - let default_theme_chunk = "Monokai Extended Light\x1B[0m (default)"; - - #[cfg(not(target_os = "macos"))] let default_theme_chunk = "Monokai Extended\x1B[0m (default)"; + let default_light_theme_chunk = "Monokai Extended Light\x1B[0m (default light)"; bat() .arg("--color=always") @@ -287,24 +283,33 @@ fn list_themes_with_colors() { .success() .stdout(predicate::str::contains("DarkNeon").normalize()) .stdout(predicate::str::contains(default_theme_chunk).normalize()) + .stdout(predicate::str::contains(default_light_theme_chunk).normalize()) .stdout(predicate::str::contains("Output the square of a number.").normalize()); } #[test] fn list_themes_without_colors() { - #[cfg(target_os = "macos")] - let default_theme_chunk = "Monokai Extended Light (default)"; - - #[cfg(not(target_os = "macos"))] let default_theme_chunk = "Monokai Extended (default)"; + let default_light_theme_chunk = "Monokai Extended Light (default light)"; bat() .arg("--color=never") + .arg("--decorations=always") // trick bat into setting `Config::loop_through` to false .arg("--list-themes") .assert() .success() .stdout(predicate::str::contains("DarkNeon").normalize()) - .stdout(predicate::str::contains(default_theme_chunk).normalize()); + .stdout(predicate::str::contains(default_theme_chunk).normalize()) + .stdout(predicate::str::contains(default_light_theme_chunk).normalize()); +} + +#[test] +fn list_themes_to_piped_output() { + bat() + .arg("--list-themes") + .assert() + .success() + .stdout(predicate::str::contains("(default)").not()); } #[test] @@ -405,9 +410,10 @@ fn no_args_doesnt_break() { // as the slave end of a pseudo terminal. Although both point to the same "file", bat should // not exit, because in this case it is safe to read and write to the same fd, which is why // this test exists. + let OpenptyResult { master, slave } = openpty(None, None).expect("Couldn't open pty."); - let mut master = unsafe { File::from_raw_fd(master) }; - let stdin_file = unsafe { File::from_raw_fd(slave) }; + let mut master = File::from(master); + let stdin_file = File::from(slave); let stdout_file = stdin_file.try_clone().unwrap(); let stdin = Stdio::from(stdin_file); let stdout = Stdio::from(stdout_file); @@ -415,6 +421,7 @@ fn no_args_doesnt_break() { let mut child = bat_raw_command() .stdin(stdin) .stdout(stdout) + .env("TERM", "dumb") // Suppresses color detection .spawn() .expect("Failed to start."); @@ -1010,6 +1017,31 @@ fn enable_pager_if_pp_flag_comes_before_paging() { .stdout(predicate::eq("pager-output\n").normalize()); } +#[test] +fn paging_does_not_override_simple_plain() { + bat() + .env("PAGER", "echo pager-output") + .arg("--decorations=always") + .arg("--plain") + .arg("--paging=never") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::eq("hello world\n")); +} + +#[test] +fn simple_plain_does_not_override_paging() { + bat() + .env("PAGER", "echo pager-output") + .arg("--paging=always") + .arg("--plain") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::eq("pager-output\n")); +} + #[test] fn pager_failed_to_parse() { bat() @@ -1929,6 +1961,16 @@ fn show_all_with_unicode() { .stderr(""); } +#[test] +fn binary_as_text() { + bat() + .arg("--binary=as-text") + .arg("control_characters.txt") + .assert() + .stdout("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7F") + .stderr(""); +} + #[test] fn no_paging_arg() { bat() @@ -2810,3 +2852,71 @@ fn strip_ansi_auto_does_not_strip_ansi_when_plain_text_by_option() { assert!(output.contains("\x1B[33mYellow")) } + +// Tests that style components can be removed with `-component`. +#[test] +fn style_components_can_be_removed() { + bat() + .arg({ + #[cfg(not(feature = "git"))] + { + "--style=full,-grid" + } + #[cfg(feature = "git")] + { + "--style=full,-grid,-changes" + } + }) + .arg("--decorations=always") + .arg("--color=never") + .write_stdin("test") + .assert() + .success() + .stdout(" STDIN\n Size: -\n 1 test\n") + .stderr(""); +} + +// Tests that style components are chosen based on the rightmost `--style` argument. +#[test] +fn style_components_can_be_overidden() { + bat() + .arg("--style=full") + .arg("--style=header,numbers") + .arg("--decorations=always") + .arg("--color=never") + .write_stdin("test") + .assert() + .success() + .stdout(" STDIN\n 1 test\n") + .stderr(""); +} + +// Tests that style components can be merged across multiple `--style` arguments. +#[test] +fn style_components_will_merge() { + bat() + .arg("--style=header,grid") + .arg("--style=-grid,+numbers") + .arg("--decorations=always") + .arg("--color=never") + .write_stdin("test") + .assert() + .success() + .stdout(" STDIN\n 1 test\n") + .stderr(""); +} + +// Tests that style components can be merged with the `BAT_STYLE` environment variable. +#[test] +fn style_components_will_merge_with_env_var() { + bat() + .env("BAT_STYLE", "header,grid") + .arg("--style=-grid,+numbers") + .arg("--decorations=always") + .arg("--color=never") + .write_stdin("test") + .assert() + .success() + .stdout(" STDIN\n 1 test\n") + .stderr(""); +} diff --git a/tests/syntax-tests/highlighted/CFML/test.cfml b/tests/syntax-tests/highlighted/CFML/test.cfml new file mode 100644 index 00000000..4aa8ecdc --- /dev/null +++ b/tests/syntax-tests/highlighted/CFML/test.cfml @@ -0,0 +1,54 @@ +<head>  +<title>Add New Employees  +  +<body>  +<h1>Add New Employees  +  +  +<cfparam name="Form.firstname" default="">  +<cfparam name="Form.lastname" default="">  +<cfparam name="Form.email" default="">  +<cfparam name="Form.phone" default="">  +<cfparam name="Form.department" default="">  +  +<cfif #Form.firstname# eq "">  +<p>Please fill out the form.  +<cfelse>  +<cfoutput>  +<cfscript>  +employee=StructNew();  +employee.firstname = Form.firstname;  +employee.lastname = Form.lastname;  +employee.email = Form.email;  +employee.phone = Form.phone;  +employee.department = Form.department;  +  +  +First name is #StructFind(employee, "firstname")#<br>  +Last name is #StructFind(employee, "lastname")#<br>  +EMail is #StructFind(employee, "email")#<br>  +Phone is #StructFind(employee, "phone")#<br>  +Department is #StructFind(employee, "department")#<br>  +  +  +<cf_addemployee empinfo="#employee#">  +  +  +<hr>  +<form action="newemployee.cfm" method="Post">  +First Name:&nbsp;  +<input name="firstname" type="text" hspace="30" maxlength="30"><br>  +Last Name:&nbsp;  +<input name="lastname" type="text" hspace="30" maxlength="30"><br>  +EMail:&nbsp;  +<input name="email" type="text" hspace="30" maxlength="30"><br>  +Phone:&nbsp;  +<input name="phone" type="text" hspace="20" maxlength="20"><br>  +Department:&nbsp;  +<input name="department" type="text" hspace="30" maxlength="30"><br>  +<input type="Submit" value="OK">  +  +<br>  +  + diff --git a/tests/syntax-tests/source/CFML/test.cfml b/tests/syntax-tests/source/CFML/test.cfml new file mode 100644 index 00000000..f119af76 --- /dev/null +++ b/tests/syntax-tests/source/CFML/test.cfml @@ -0,0 +1,54 @@ + +Add New Employees + + +

Add New Employees

+ + + + + + + + + +

Please fill out the form.

+ + + +employee=StructNew(); +employee.firstname = Form.firstname; +employee.lastname = Form.lastname; +employee.email = Form.email; +employee.phone = Form.phone; +employee.department = Form.department; + + +First name is #StructFind(employee, "firstname")#
+Last name is #StructFind(employee, "lastname")#
+EMail is #StructFind(employee, "email")#
+Phone is #StructFind(employee, "phone")#
+Department is #StructFind(employee, "department")#
+
+ + +
+ +
+
+First Name:  +
+Last Name:  +
+EMail:  +
+Phone:  +
+Department:  +
+ +
+
+ +