1
0
mirror of https://github.com/sharkdp/bat synced 2026-08-03 18:51:44 +00:00

Compare commits

...

383 Commits

Author SHA1 Message Date
dependabot[bot] be1bd8b41b build(deps): bump assets/syntaxes/02_Extra/NSIS
Bumps [assets/syntaxes/02_Extra/NSIS](https://github.com/SublimeText/NSIS) from `619a65a` to `fe5aec8`.
- [Release notes](https://github.com/SublimeText/NSIS/releases)
- [Commits](https://github.com/SublimeText/NSIS/compare/619a65a04efbf343bdfcde875700675b9b273368...fe5aec8de320fd6c32c67cafd9911a3b521e7026)

---
updated-dependencies:
- dependency-name: assets/syntaxes/02_Extra/NSIS
  dependency-version: fe5aec8de320fd6c32c67cafd9911a3b521e7026
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 02:04:20 +00:00
Keith Hall 73dc3258be Merge pull request #3862 from lenamonj/fix/sanitize-bidi-control-set
Fix --sanitize passing through the bidi control characters U+200E, U+200F and U+061C
2026-07-30 00:27:20 +03:00
lenamonj df38225653 Add CHANGELOG entry for #3862 2026-07-29 17:02:33 -04:00
lenamonj c2cc79daa7 Fix --sanitize passing through the bidi control characters U+200E, U+200F and U+061C 2026-07-29 17:02:11 -04:00
Keith Hall 0230cba42d Merge pull request #3828 from cyphercodes/fix-list-languages-paging-never
Respect paging mode for --list-languages
2026-07-29 23:49:08 +03:00
Keith Hall 227e0fa00f Merge branch 'master' into fix-list-languages-paging-never 2026-07-29 23:33:51 +03:00
auto-merge-dependabot-prs[bot] 384f2f5d13 Merge pull request #3821 from sharkdp/dependabot/cargo/regex-1.12.4
build(deps): bump regex from 1.12.3 to 1.13.1
2026-07-29 20:23:23 +00:00
dependabot[bot] 060ccb1385 build(deps): bump regex from 1.12.3 to 1.13.1
Bumps [regex](https://github.com/rust-lang/regex) from 1.12.3 to 1.13.1.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.12.3...1.13.1)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 1.12.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 20:09:58 +00:00
auto-merge-dependabot-prs[bot] feabb25209 Merge pull request #3823 from sharkdp/dependabot/cargo/serde_with-3.21.0
build(deps): bump serde_with from 3.19.0 to 3.21.0
2026-07-29 20:06:40 +00:00
dependabot[bot] 839f2300b7 build(deps): bump serde_with from 3.19.0 to 3.21.0
Bumps [serde_with](https://github.com/jonasbb/serde_with) from 3.19.0 to 3.21.0.
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.19.0...v3.21.0)

---
updated-dependencies:
- dependency-name: serde_with
  dependency-version: 3.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 19:46:45 +00:00
auto-merge-dependabot-prs[bot] ea0742babc Merge pull request #3820 from sharkdp/dependabot/cargo/quote-1.0.46
build(deps): bump quote from 1.0.45 to 1.0.47
2026-07-29 19:40:36 +00:00
dependabot[bot] bfa6e59978 build(deps): bump quote from 1.0.45 to 1.0.47
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.45 to 1.0.47.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.45...1.0.47)

---
updated-dependencies:
- dependency-name: quote
  dependency-version: 1.0.46
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 19:17:50 +00:00
auto-merge-dependabot-prs[bot] 9710a231d8 Merge pull request #3819 from sharkdp/dependabot/cargo/bytesize-2.4.0
build(deps): bump bytesize from 2.3.1 to 2.4.2
2026-07-29 19:14:35 +00:00
dependabot[bot] a9b67b2d99 build(deps): bump bytesize from 2.3.1 to 2.4.2
Bumps [bytesize](https://github.com/bytesize-rs/bytesize) from 2.3.1 to 2.4.2.
- [Release notes](https://github.com/bytesize-rs/bytesize/releases)
- [Changelog](https://github.com/bytesize-rs/bytesize/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bytesize-rs/bytesize/compare/bytesize-v2.3.1...bytesize-v2.4.2)

---
updated-dependencies:
- dependency-name: bytesize
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 18:54:55 +00:00
auto-merge-dependabot-prs[bot] b22be63fec Merge pull request #3822 from sharkdp/dependabot/cargo/unicode-segmentation-1.13.3
build(deps): bump unicode-segmentation from 1.13.2 to 1.13.3
2026-07-29 18:50:14 +00:00
Keith Hall 3a7a750969 Merge branch 'master' into fix-list-languages-paging-never 2026-07-29 21:25:10 +03:00
dependabot[bot] be5343c742 build(deps): bump unicode-segmentation from 1.13.2 to 1.13.3
Bumps [unicode-segmentation](https://github.com/unicode-rs/unicode-segmentation) from 1.13.2 to 1.13.3.
- [Commits](https://github.com/unicode-rs/unicode-segmentation/commits)

---
updated-dependencies:
- dependency-name: unicode-segmentation
  dependency-version: 1.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 18:21:51 +00:00
Keith Hall c371044276 Merge pull request #3861 from TyceHerrman/fix/cargo-audit-2026-07
chore(deps): resolve current cargo audit failures
2026-07-29 21:18:39 +03:00
Tyce Herrman d449ce5b7e chore(deps): resolve current cargo audit failures 2026-07-29 12:10:05 -04:00
cyphercodes 218afc30ac Respect paging mode for list languages 2026-07-04 06:31:26 +03:00
Keith Hall 78951393e2 Merge pull request #3729 from curious-rabbit/sanitize
add --sanitize option
2026-07-01 23:15:58 +03:00
curious-rabbit 0a52e4321f improve patch 2026-07-01 07:54:51 +02:00
curious-rabbit d72163160e add sanitize option 2026-07-01 07:09:49 +02:00
auto-merge-dependabot-prs[bot] 971f9679da Merge pull request #3817 from sharkdp/dependabot/submodules/assets/syntaxes/02_Extra/LESS-d076a4d
build(deps): bump assets/syntaxes/02_Extra/LESS from `836b47e` to `d076a4d`
2026-07-01 02:16:46 +00:00
dependabot[bot] 57d436861a build(deps): bump assets/syntaxes/02_Extra/LESS
Bumps [assets/syntaxes/02_Extra/LESS](https://github.com/danro/LESS-sublime) from `836b47e` to `d076a4d`.
- [Release notes](https://github.com/danro/LESS-sublime/releases)
- [Commits](https://github.com/danro/LESS-sublime/compare/836b47ec61a9c6a6445b4007e8353337fe63e2c9...d076a4dd416bcdb155bacf90cda72f7bf1c8abbb)

---
updated-dependencies:
- dependency-name: assets/syntaxes/02_Extra/LESS
  dependency-version: d076a4dd416bcdb155bacf90cda72f7bf1c8abbb
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-01 02:03:33 +00:00
Keith Hall bb4f48a124 Merge pull request #3816 from adnrivera/fix/ignored-suffix-firstline
Fix --ignored-suffix to fall back to first-line detection
2026-07-01 00:40:18 +03:00
Adrian Rivera c75cc01eef Fix --ignored-suffix to fall back to first-line detection
When an ignored suffix is also a registered extension (e.g. `.txt` maps to
"Plain Text"), `get_syntax_for_file_extension` matched the raw extension
before attempting the suffix strip. As a result `--ignored-suffix .txt` had
no effect on such files and first-line/shebang detection was never reached,
so a shell script saved as `*.txt` stayed unhighlighted.

Strip ignored suffixes first and detect on the remainder; only fall back to
the raw extension when no ignored suffix applies. When a suffix is stripped
but the remainder has no syntax, return `None` so the caller can use
first-line detection. Default behavior (no `--ignored-suffix`) is unchanged.

See #2745.
2026-06-30 12:20:14 -07:00
Keith Hall 835bd2a756 Merge pull request #3793 from Matei02355/fix-sml-definition-link
docs: fix SML syntax source link
2026-06-28 22:25:36 +03:00
Keith Hall 3409674d9c Merge branch 'master' into fix-sml-definition-link 2026-06-28 22:15:42 +03:00
Keith Hall 7de0c4ba47 Merge pull request #3809 from dhruvkb/patch-1
Include `.code-workspace` as a JSON extension
2026-06-27 22:21:18 +03:00
Keith Hall 20a818fe14 Merge branch 'master' into patch-1 2026-06-27 21:58:52 +03:00
Keith Hall 30b50a9eed Merge pull request #3814 from injust/dnf-repo
Add syntax mapping for DNF config
2026-06-27 21:57:37 +03:00
injust 088f1e4253 Add changelog entry 2026-06-27 12:50:29 -04:00
injust 53769d03b0 Add syntax mapping for DNF config 2026-06-27 12:50:28 -04:00
Keith Hall b7ae4585ad Merge pull request #3812 from greymoth-jp/fix/list-languages-narrow-width-underflow
fix: avoid usize underflow in --list-languages at narrow terminal width
2026-06-27 06:12:21 +03:00
Keith Hall a4609d1778 Merge branch 'master' into fix/list-languages-narrow-width-underflow 2026-06-27 06:03:16 +03:00
Keith Hall 8f70e0cf00 Merge pull request #3703 from blinxen/master
Replace libgit2 with gitoxide (#2)
2026-06-27 05:53:22 +03:00
blinxen e34595b53e Update gix to 0.85 2026-06-26 20:28:29 +02:00
blinxen 6ba745b74b cargo fmt 2026-06-26 20:27:49 +02:00
blinxen 98f09fd6f3 Add test with fauly git repo version 2026-06-26 20:27:49 +02:00
blinxen 69f40d9d8a Regenerate Cargo.lock after rebase 2026-06-26 20:27:49 +02:00
blinxen 6e6137ce10 Update to gix 0.83 2026-06-26 20:27:49 +02:00
blinxen df164cc2e3 fix immediate dereference of reference 2026-06-26 20:27:49 +02:00
blinxen f9eed8d619 cargo fmt 2026-06-26 20:27:49 +02:00
blinxen d695ad8f88 Add more tests for git diff 2026-06-26 20:27:49 +02:00
blinxen e60d184f97 Use histogram instead of myers for diffing 2026-06-26 20:27:49 +02:00
blinxen e2568c52cd Use correct PR number 2026-06-26 20:27:49 +02:00
blinxen c5e6f6aae3 Replace libgit2 with gitoxide 2026-06-26 20:27:49 +02:00
greymoth 8f93137246 doc(changelog): add #3812 bugfix entry 2026-06-26 22:13:51 +09:00
greymoth-jp fa00fb8ce8 fix(list-languages): clamp desired_width to avoid usize underflow at tiny --terminal-width
`get_languages` computed `desired_width = term_width - longest - separator.len()`.
When `--terminal-width` is smaller than the longest language name (e.g. 1), this
underflows `usize`: in overflow-checked builds it panics ("attempt to subtract with
overflow"), and in release it wraps to ~usize::MAX, silently disabling the extension
line-wrapping so `--terminal-width` is ignored for `--list-languages`.

Use `saturating_sub`, mirroring the fix applied to `print_snip` in #3804 (the snip
separator had the same `term_width - ...` underflow pattern). Output at normal widths
is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:24:01 +09:00
Dhruv Bhanushali d117bf2320 Summarise the change in the CHANGELOG.md file 2026-06-23 08:08:14 +04:00
Dhruv Bhanushali f4d836ee87 Include .code-workspace as a JSON extension 2026-06-23 08:01:50 +04:00
auto-merge-dependabot-prs[bot] 04576fd5a5 Merge pull request #3783 from sharkdp/dependabot/submodules/assets/syntaxes/02_Extra/vscode-wgsl-a285c38
build(deps): bump assets/syntaxes/02_Extra/vscode-wgsl from `acf2671` to `a285c38`
2026-06-17 17:51:31 +00:00
dependabot[bot] e7025d9bdb build(deps): bump assets/syntaxes/02_Extra/vscode-wgsl
Bumps [assets/syntaxes/02_Extra/vscode-wgsl](https://github.com/PolyMeilex/vscode-wgsl) from `acf2671` to `a285c38`.
- [Release notes](https://github.com/PolyMeilex/vscode-wgsl/releases)
- [Commits](https://github.com/PolyMeilex/vscode-wgsl/compare/acf26718d7a327377641e31d8f9a9dab376efa84...a285c38f74eba2eb5c5a06be8d95b9f581338509)

---
updated-dependencies:
- dependency-name: assets/syntaxes/02_Extra/vscode-wgsl
  dependency-version: a285c38f74eba2eb5c5a06be8d95b9f581338509
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-17 17:38:53 +00:00
Keith Hall 6d4a3cef53 Merge pull request #3789 from CosmicHorrorDev/caddy
feat: add syntax highlighting for `Caddyfile`
2026-06-17 20:35:17 +03:00
Cosmic Horror 6b8887133d feat: add syntax highlighting for Caddyfile 2026-06-17 04:44:53 -06:00
Keith Hall 5bb35b4bed Merge pull request #3804 from leeewee/fix-snip-terminal-width-1-panic
Fix capacity-overflow panic in snip separator at --terminal-width 1
2026-06-16 20:19:08 +03:00
weili d28aa4a8b4 Fix capacity-overflow panic in print_snip at --terminal-width 1
`InteractivePrinter::print_snip` computes the snip separator width as
`term_width - panel_count - snip_left_count - title_count`. At `--terminal-width 1`
the panel is disabled and `title_count == 2`, so the subtraction underflows
`usize` (release has no overflow-checks, so it wraps to ~usize::MAX); `str::repeat`
then aborts with "capacity overflow" whenever a snip separator is emitted (two or
more disjoint line ranges, or a diff gap) — which the default style does.

Use `saturating_sub` for the repeat counts so they clamp to 0 instead of
underflowing. Added an integration test.
2026-06-16 07:06:29 +00:00
Matei6942 e9b552d5d0 docs: fix SML syntax source link 2026-06-05 17:40:39 +03:00
auto-merge-dependabot-prs[bot] af1f53d9a9 Merge pull request #3786 from sharkdp/dependabot/cargo/nix-0.31.3
build(deps): bump nix from 0.31.2 to 0.31.3
2026-06-01 05:15:08 +00:00
dependabot[bot] 8033b1e7be build(deps): bump nix from 0.31.2 to 0.31.3
Bumps [nix](https://github.com/nix-rust/nix) from 0.31.2 to 0.31.3.
- [Changelog](https://github.com/nix-rust/nix/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nix-rust/nix/compare/v0.31.2...v0.31.3)

---
updated-dependencies:
- dependency-name: nix
  dependency-version: 0.31.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 05:03:33 +00:00
auto-merge-dependabot-prs[bot] 01408ce094 Merge pull request #3785 from sharkdp/dependabot/cargo/predicates-3.1.4
build(deps): bump predicates from 3.1.3 to 3.1.4
2026-06-01 05:00:15 +00:00
dependabot[bot] bcd1d3f45b build(deps): bump predicates from 3.1.3 to 3.1.4
Bumps [predicates](https://github.com/assert-rs/predicates-rs) from 3.1.3 to 3.1.4.
- [Changelog](https://github.com/assert-rs/predicates-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/assert-rs/predicates-rs/compare/v3.1.3...v3.1.4)

---
updated-dependencies:
- dependency-name: predicates
  dependency-version: 3.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 04:48:18 +00:00
auto-merge-dependabot-prs[bot] 4cd5db2a78 Merge pull request #3787 from sharkdp/dependabot/cargo/indexmap-2.14.0
build(deps): bump indexmap from 2.13.0 to 2.14.0
2026-06-01 04:44:39 +00:00
dependabot[bot] 67aa491313 build(deps): bump indexmap from 2.13.0 to 2.14.0
Bumps [indexmap](https://github.com/indexmap-rs/indexmap) from 2.13.0 to 2.14.0.
- [Changelog](https://github.com/indexmap-rs/indexmap/blob/main/RELEASES.md)
- [Commits](https://github.com/indexmap-rs/indexmap/compare/2.13.0...2.14.0)

---
updated-dependencies:
- dependency-name: indexmap
  dependency-version: 2.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 04:28:21 +00:00
auto-merge-dependabot-prs[bot] e490c0f322 Merge pull request #3782 from sharkdp/dependabot/cargo/minus-5.7.1
build(deps): bump minus from 5.6.1 to 5.7.1
2026-06-01 04:24:38 +00:00
dependabot[bot] 2d29ecb939 build(deps): bump minus from 5.6.1 to 5.7.1
Bumps [minus](https://github.com/AMythicDev/minus) from 5.6.1 to 5.7.1.
- [Changelog](https://github.com/AMythicDev/minus/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AMythicDev/minus/compare/v5.6.1...v5.7.1)

---
updated-dependencies:
- dependency-name: minus
  dependency-version: 5.7.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 03:59:42 +00:00
auto-merge-dependabot-prs[bot] 707b5949fd Merge pull request #3781 from sharkdp/dependabot/cargo/plist-1.9.0
build(deps): bump plist from 1.7.0 to 1.9.0
2026-06-01 03:50:11 +00:00
dependabot[bot] fd53693e1f build(deps): bump plist from 1.7.0 to 1.9.0
Bumps [plist](https://github.com/ebarnard/rust-plist) from 1.7.0 to 1.9.0.
- [Release notes](https://github.com/ebarnard/rust-plist/releases)
- [Changelog](https://github.com/ebarnard/rust-plist/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ebarnard/rust-plist/compare/v1.7.0...v1.9.0)

---
updated-dependencies:
- dependency-name: plist
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 03:30:20 +00:00
Keith Hall f3d0773468 Merge pull request #3761 from mvanhorn/fix/3760-completion-skip-pager
fix(completions): force --no-paging on bat invocations in completion scripts (#3760)
2026-05-20 07:00:37 +03:00
Matt Van Horn 66e336bcfb fix(completions): force --no-paging on bat invocations in completion scripts (#3760)
Every shell completion script (bash, zsh, fish, PowerShell) shells out
to bat to enumerate languages/themes for tab completion candidates. If
the user has wired bat into LESSOPEN (e.g. LESSOPEN='|-bat -f -pp %s'),
bat's normal pager auto-detection can engage when stdout looks like a
terminal at completion time and reflect ANSI escape sequences back into
the candidate list. The result is the issue's reproducer: tab completion
expands 'Per' to '\033[38;2;248;248;242mPerl' instead of 'Perl'.

The list-languages/list-themes calls are always meant to be machine-
readable, so they should never page. Pass --no-paging explicitly to
every bat invocation inside the four completion files. The flag is the
public alias for --paging=never (already documented in bat --help) and
is the same form completion scripts elsewhere in the codebase use.

Touches the four completion files only; no production code changes.
2026-05-19 04:19:31 -07:00
Keith Hall ce7ad9adca Merge pull request #3758 from injust/patch-1
Include `.ssh/` subdirectories in SSH Config syntax mapping
2026-05-18 20:26:18 +03:00
Keith Hall f9fa590c3d Merge branch 'master' into patch-1 2026-05-18 20:11:48 +03:00
Keith Hall 1e1c3773fb Merge pull request #3759 from injust/patch-2
Add Ghostty syntax mapping
2026-05-18 20:10:26 +03:00
Justin Su 0ef0342ff2 Add changelog entry 2026-05-18 03:33:50 -04:00
Justin Su de396794cf Add Ghostty syntax mapping 2026-05-18 03:29:12 -04:00
Justin Su 43c4957e18 Add changelog entry 2026-05-18 03:08:39 -04:00
Justin Su c9fa10e76b Include .ssh/ subdirectories in SSH Config syntax mapping 2026-05-18 02:45:50 -04:00
Keith Hall 2cec8cdf25 Merge pull request #3737 from truffle-dev/fix/zsh-completion-language-only
fix: only offer language names in zsh tab completion for `-l`
2026-05-11 20:32:12 +03:00
truffle 138d70fd4c fix(zsh): drop redundant awk pipeline in language completion
`bat --list-languages` already emits each entry in `name:matchers`
form, which is the format `_describe` consumes directly. The previous
awk script split each line on `:` and re-emitted `$1:$2`, which is
byte-identical to the input.

Verified with `diff <(bat --list-languages) <(bat --list-languages |
awk -F: '{ printf("%s:%s\\n", $1, $2) }')` against the current
syntax set.
2026-05-10 14:10:54 +00:00
truffle 98df25434e fix: reference PR number in changelog entry 2026-05-10 09:20:40 +00:00
truffle 5f952066fb fix: only offer language names in zsh tab completion for -l
The previous awk script in `bat.zsh.in` split each line of
`bat --list-languages` on `:` or `,` and emitted every field as a
completion candidate, including the second column. That column lists
file matchers, which can be plain extensions (`rs`), globs (`*.rs`),
absolute paths (`/etc/profile`), or filenames (`Containerfile`). None
of those parse as `-l` arguments, so completing them produces
`unknown syntax` errors.

Switch to splitting on `:` only and emit the language name as the
completion value with the file-matcher list as its description, which
matches the bash completion's behavior.

Closes #3735.
2026-05-10 09:19:54 +00:00
Keith Hall f74082cee8 Merge pull request #3734 from kimjune01/fix/zsh-completion-force-plain
fix: pass --color=never --decorations=never in zsh completions
2026-05-09 13:52:47 +03:00
June Kim a0c95618c4 fix: pass --color=never --decorations=never in zsh completions
When BAT_OPTS contains --color=always or --decorations=always, the zsh
completion script's calls to --list-languages and --list-themes produce
ANSI escape codes that corrupt tab completion results.

Pass --color=never --decorations=never explicitly so completion output
is always plain text regardless of user config.

Fixes #3733
2026-05-09 03:35:54 -07:00
Keith Hall f57c1b6b1d Merge pull request #3726 from curious-rabbit/less
fix command injection in LESS template
2026-05-07 05:34:13 +03:00
Keith Hall 517de0463b Merge branch 'master' into less 2026-05-07 05:23:26 +03:00
curious-rabbit 3be4e30e00 improve patch 2026-05-06 10:58:02 +02:00
auto-merge-dependabot-prs[bot] 15fdc7637a Merge pull request #3717 from sharkdp/dependabot/cargo/terminal-colorsaurus-1.0.3
build(deps): bump terminal-colorsaurus from 1.0.1 to 1.0.3
2026-05-06 03:58:05 +00:00
dependabot[bot] 72fc697372 build(deps): bump terminal-colorsaurus from 1.0.1 to 1.0.3
Bumps [terminal-colorsaurus](https://github.com/tautropfli/terminal-colorsaurus) from 1.0.1 to 1.0.3.
- [Release notes](https://github.com/tautropfli/terminal-colorsaurus/releases)
- [Changelog](https://github.com/tautropfli/terminal-colorsaurus/blob/main/changelog.md)
- [Commits](https://github.com/tautropfli/terminal-colorsaurus/compare/1.0.1...1.0.3)

---
updated-dependencies:
- dependency-name: terminal-colorsaurus
  dependency-version: 1.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-06 03:45:27 +00:00
auto-merge-dependabot-prs[bot] b8d2781784 Merge pull request #3715 from sharkdp/dependabot/cargo/flate2-1.1.9
build(deps): bump flate2 from 1.1.2 to 1.1.9
2026-05-06 03:41:52 +00:00
dependabot[bot] 787a36acb6 build(deps): bump flate2 from 1.1.2 to 1.1.9
Bumps [flate2](https://github.com/rust-lang/flate2-rs) from 1.1.2 to 1.1.9.
- [Release notes](https://github.com/rust-lang/flate2-rs/releases)
- [Commits](https://github.com/rust-lang/flate2-rs/compare/1.1.2...1.1.9)

---
updated-dependencies:
- dependency-name: flate2
  dependency-version: 1.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-06 03:29:10 +00:00
auto-merge-dependabot-prs[bot] a79701a43e Merge pull request #3712 from sharkdp/dependabot/cargo/regex-1.12.3
build(deps): bump regex from 1.12.2 to 1.12.3
2026-05-06 03:26:32 +00:00
dependabot[bot] 3bcbc45ab0 build(deps): bump regex from 1.12.2 to 1.12.3
Bumps [regex](https://github.com/rust-lang/regex) from 1.12.2 to 1.12.3.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.12.2...1.12.3)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 1.12.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-06 03:09:47 +00:00
auto-merge-dependabot-prs[bot] 7953df44ea Merge pull request #3711 from sharkdp/dependabot/cargo/clap-4.6.1
build(deps): bump clap from 4.5.60 to 4.6.1
2026-05-06 03:06:46 +00:00
dependabot[bot] 443bcb9287 build(deps): bump clap from 4.5.60 to 4.6.1
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.60 to 4.6.1.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.60...clap_complete-v4.6.1)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-06 02:44:23 +00:00
auto-merge-dependabot-prs[bot] 0523b70473 Merge pull request #3714 from sharkdp/dependabot/cargo/bugreport-0.6.0
build(deps): bump bugreport from 0.5.1 to 0.6.0
2026-05-06 02:41:47 +00:00
dependabot[bot] f776d1ad3f build(deps): bump bugreport from 0.5.1 to 0.6.0
Bumps [bugreport](https://github.com/sharkdp/bugreport) from 0.5.1 to 0.6.0.
- [Release notes](https://github.com/sharkdp/bugreport/releases)
- [Commits](https://github.com/sharkdp/bugreport/compare/v0.5.1...v0.6.0)

---
updated-dependencies:
- dependency-name: bugreport
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-06 02:33:22 +00:00
Keith Hall cfc6d9c415 Merge pull request #3728 from Daeraxa/fix-fedora-section
Fix README Fedora section links
2026-05-06 04:59:37 +03:00
Daeraxa 89d7c86b19 update fedora package link 2026-05-05 23:53:13 +01:00
curious-rabbit c437ad4d27 fix command injection in LESS template 2026-05-06 00:46:35 +02:00
Daeraxa 1c6e763d75 Update readme to fix Fedora section 2026-05-05 23:42:30 +01:00
Keith Hall baebdd1789 Merge pull request #3623 from zachvalenta/makefile-syntax-for-justfiles
Makefile syntax for justfiles
2026-05-02 14:04:55 +03:00
Keith Hall f7b84a3b41 Merge branch 'master' into makefile-syntax-for-justfiles 2026-05-02 13:56:45 +03:00
auto-merge-dependabot-prs[bot] b483f90612 Merge pull request #3716 from sharkdp/dependabot/submodules/assets/syntaxes/02_Extra/typst-syntax-highlight-5f71d12
build(deps): bump assets/syntaxes/02_Extra/typst-syntax-highlight from `363f0e7` to `5f71d12`
2026-05-01 03:03:58 +00:00
dependabot[bot] 6edad56b00 build(deps): bump assets/syntaxes/02_Extra/typst-syntax-highlight
Bumps [assets/syntaxes/02_Extra/typst-syntax-highlight](https://github.com/hyrious/typst-syntax-highlight) from `363f0e7` to `5f71d12`.
- [Release notes](https://github.com/hyrious/typst-syntax-highlight/releases)
- [Commits](https://github.com/hyrious/typst-syntax-highlight/compare/363f0e767c938c615a14912c302db7936f025fc2...5f71d12fa129165bbe51aa867292555cdff6eb75)

---
updated-dependencies:
- dependency-name: assets/syntaxes/02_Extra/typst-syntax-highlight
  dependency-version: 5f71d12fa129165bbe51aa867292555cdff6eb75
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-01 02:43:12 +00:00
auto-merge-dependabot-prs[bot] e2174379ed Merge pull request #3713 from sharkdp/dependabot/submodules/assets/syntaxes/02_Extra/PureScript-1773f4f
build(deps): bump assets/syntaxes/02_Extra/PureScript from `5acebc1` to `1773f4f`
2026-05-01 02:38:41 +00:00
dependabot[bot] 816aea0625 build(deps): bump assets/syntaxes/02_Extra/PureScript
Bumps [assets/syntaxes/02_Extra/PureScript](https://github.com/tellnobody1/sublime-purescript-syntax) from `5acebc1` to `1773f4f`.
- [Release notes](https://github.com/tellnobody1/sublime-purescript-syntax/releases)
- [Commits](https://github.com/tellnobody1/sublime-purescript-syntax/compare/5acebc18503697be09df047591964e68e80fcf8e...1773f4fddb08560d6bcb354901088e61e9ea0908)

---
updated-dependencies:
- dependency-name: assets/syntaxes/02_Extra/PureScript
  dependency-version: 1773f4fddb08560d6bcb354901088e61e9ea0908
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-01 02:11:34 +00:00
Keith Hall 7fcab0616a Merge pull request #3700 from optimistiCli/list-themes-term-probe-fix
Fixed hardcoded terminal probing when `--list-themes` is called
2026-04-30 18:09:32 +03:00
Keith Hall e157ac911b Merge branch 'master' into list-themes-term-probe-fix 2026-04-30 17:58:24 +03:00
Keith Hall b29e1d55c4 Merge pull request #3709 from mvanhorn/osc/3228-readme-style-list
docs(README): list available --style components in Output style section
2026-04-30 14:15:25 +03:00
Keith Hall 12ee94f530 Merge branch 'master' into list-themes-term-probe-fix 2026-04-30 14:13:47 +03:00
Matt Van Horn cbe7919451 docs(README): list available --style components in Output style section
The Output style section explained how to combine values into --style
without listing what those values are, so the recommended path was
either to read the man page or grep the source. Adds two tables: the
four pre-defined styles (default, full, auto, plain) and the eight
individual components (changes, header, header-filename, header-filesize,
grid, rule, numbers, snip), copying the descriptions from `bat --help`
verbatim. Also calls out the default-enabled set explicitly so the
"by default..." sentence in the existing tip block has a referent.

Closes #3228
2026-04-30 01:54:42 -07:00
David Peter 10b4f07a87 Remove sponsors 2026-04-30 10:18:58 +02:00
David Peter ebf469b99a Update README 2026-04-30 10:16:33 +02:00
Keith Hall 8fbc96a5f2 Merge pull request #3654 from cuiweixie/fix/lessclose-nonzero-warning
fix: correct $LESSCLOSE exit-status warning
2026-04-30 08:19:42 +03:00
Keith Hall 54bfc33248 Merge branch 'master' into fix/lessclose-nonzero-warning 2026-04-30 08:10:40 +03:00
Keith Hall 91aac95db2 Merge pull request #3691 from curious-rabbit/master
sanitize filenames
2026-04-28 23:24:00 +03:00
zach valenta 5bf932d511 Merge branch 'master' into makefile-syntax-for-justfiles 2026-04-28 14:41:15 -04:00
Keith Hall 5977245a23 Merge branch 'master' into fix/lessclose-nonzero-warning 2026-04-28 19:24:00 +03:00
Keith Hall 3f98e6f10f Merge branch 'master' into master 2026-04-28 19:22:51 +03:00
auto-merge-dependabot-prs[bot] 78963986e0 Merge pull request #3666 from sharkdp/dependabot/submodules/assets/syntaxes/02_Extra/Idris2-4d8eb35
build(deps): bump assets/syntaxes/02_Extra/Idris2 from `bbfe50e` to `4d8eb35`
2026-04-27 19:41:31 +00:00
dependabot[bot] de64d3a0eb build(deps): bump assets/syntaxes/02_Extra/Idris2
Bumps [assets/syntaxes/02_Extra/Idris2](https://github.com/buzden/sublime-syntax-idris2) from `bbfe50e` to `4d8eb35`.
- [Commits](https://github.com/buzden/sublime-syntax-idris2/compare/bbfe50e023e0edc74f5e0c003eb946528d49279f...4d8eb35a38254d422030e77b4933530008dd3c6e)

---
updated-dependencies:
- dependency-name: assets/syntaxes/02_Extra/Idris2
  dependency-version: 4d8eb35a38254d422030e77b4933530008dd3c6e
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 19:28:44 +00:00
Keith Hall 78cb14af05 Merge pull request #3699 from guille/master
Improve Kotlin syntax
2026-04-27 22:26:10 +03:00
Keith Hall 8c8ed90d6d Merge branch 'master' into master 2026-04-27 22:20:33 +03:00
guille 492c387ce7 CHANGELOG: reference PR not issue 2026-04-27 22:16:09 +03:00
guille bceb260e91 Improve Kotlin syntax 2026-04-27 22:16:09 +03:00
Keith Hall 8cadefb5cb Merge pull request #3706 from lawrence3699/fix/initial-read-errors
Propagate initial input read errors
2026-04-27 21:46:10 +03:00
lawrence3699 64567c4819 Propagate initial input read errors 2026-04-28 01:11:48 +10:00
Keith Hall de2f21562a Merge pull request #3704 from sharkdp/fix/zsh-completion-language-word-split
Fix/zsh completion language word split
2026-04-27 06:07:37 +03:00
Yoshi Tacke 5722311b2e docs(changelog): add bugfix entry for zsh completion fix 2026-04-27 05:55:48 +03:00
YoshKoz f39d63b85d fix(zsh): use newline-splitting for language completions to fix word-splitting on names with spaces
Language names like "HTML (Jinja2)" and "Apache Conf" contain spaces, which
caused $() command substitution to word-split them into garbage tokens. Use
${(f)"$(...)"}  (split on newlines only) to match the pattern already used
for theme completions on line 100.

Fixes #2897
2026-04-27 05:55:48 +03:00
Ish West 4144059e7a Fixed CHANGELOG.md entry format 2026-04-25 21:30:03 +02:00
Ish West 044d445adc Updated CHANGELOG.md 2026-04-25 21:17:02 +02:00
Ish West c3df0e6a88 Fixed a bug with hardcoded terminal probing when --list-themes is called 2026-04-25 17:37:36 +02:00
curious-rabbit 71c894e843 santize filenames 2026-04-20 14:25:49 +02:00
Keith Hall 93c52a4f88 Merge pull request #3692 from barry3406/fix/remove-crt-windows
windows: statically link the CRT to remove vcruntime dependency
2026-04-20 05:09:54 +03:00
Barry ea7fafca1e windows: statically link the CRT to remove vcruntime dependency
Add a .cargo/config.toml that sets target-feature=+crt-static for the
i686, x86_64, and aarch64 MSVC targets so bat.exe no longer depends on
the vcruntime DLL. Mirrors the fix applied to fd in sharkdp/fd#1891.

Closes #3634
2026-04-19 14:19:00 -07:00
Keith Hall e782ec125c Merge pull request #3647 from mvanhorn/osc/3513-tcl-shebang
feat: add tclsh, wish, and expect shebang detection for Tcl syntax
2026-04-19 05:52:52 +03:00
Matt Van Horn 0ecdeb28d4 test(Tcl): regenerate shebang golden files to match patched syntax
The Tcl.sublime-syntax.patch adds first_line_match so bat recognizes
tclsh/wish/expect shebang files as Tcl. With the patch applied, the
leading '#!/...' and '# comment' lines get tokenized as Tcl comments
and rendered in Monokai-Extended's comment color instead of default
foreground.

The goldens were generated against unpatched bat in the original
commit, so CI's 'Run tests with updated syntaxes and themes' job (which
runs assets/create.sh before the regression test) disagreed. Regenerate
against the patched build so the regression test passes.
2026-04-18 09:42:26 -07:00
Matt Van Horn 77ea750e66 test(Tcl): add shebang source files for regression test
CI reported FileNotFoundError for Tcl/{tclsh,expect,wish}_shebang in
tests/syntax-tests/source/Tcl/. The highlighted files existed but the
source files had been omitted, so create_highlighted_versions.py had
nothing to read from.

Source files match the shebang regression test inputs at
tests/examples/regression_tests/issue_3647_*.
2026-04-18 06:58:45 -07:00
Matt Van Horn cafad6b036 test: add highlighted outputs for Tcl shebang regression tests
Generate highlighted test outputs for tclsh, wish, and expect
shebang detection files to prevent regressions.
2026-04-18 06:39:51 -07:00
Matt Van Horn e070d105b5 test: add shebang regression tests and move changelog to Syntaxes section
Add extensionless regression test files for tclsh, wish, and expect
shebangs so syntect bumps don't silently break first-line detection.
Move changelog entry from Features to Syntaxes per reviewer suggestion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 06:38:44 -07:00
Matt Van Horn 1f89178fce fix: reference PR number in changelog entry 2026-04-18 06:38:27 -07:00
Matt Van Horn 52763e0205 feat: add tclsh, wish, and expect shebang detection for Tcl syntax
Add first_line_match to the Tcl syntax definition via a patch file,
enabling automatic Tcl highlighting for scripts with tclsh, wish,
or expect shebangs.

Fixes #3513
2026-04-18 06:38:18 -07:00
Keith Hall f822bd05ce Merge pull request #3689 from janlarres/python-uv
[Python] Support uv as script runner in shebang
2026-04-13 20:28:02 +03:00
Jan Larres e89c515e9a [Python] Support uv as script runner in shebang 2026-04-13 17:56:05 +12:00
Keith Hall a36b02d90f Merge pull request #3688 from orbisai0security/fix-fix-shell-injection-subprocess-call
fix: sanitize subprocess call in generate_snapshots.py
2026-04-12 16:04:47 +03:00
Kira Security Bot 6876a782da Apply code changes: @orbisai0security can you address code review comm... 2026-04-12 12:37:43 +00:00
orbisai0security db647c9813 fix: V-001 security vulnerability
Automated security fix generated by Orbis Security AI
2026-04-12 11:43:12 +00:00
Keith Hall 33045c3558 Merge pull request #3687 from officialasishkumar/support-bat-width-env
Support BAT_WIDTH for terminal width
2026-04-12 04:59:03 +03:00
Asish Kumar 01174b31f5 Update changelog entry for #3687
Signed-off-by: Asish Kumar <officialasishkumar@gmail.com>
2026-04-12 01:13:08 +05:30
Asish Kumar a9137fab05 Support BAT_WIDTH as an alias for --terminal-width
Signed-off-by: Asish Kumar <officialasishkumar@gmail.com>
2026-04-12 01:12:55 +05:30
Keith Hall a995764d23 Merge pull request #3686 from officialasishkumar/fix-zip-binary-detection
Detect ZIP archives as binary content
2026-04-10 20:30:27 +03:00
Asish Kumar b3aec318cf Update changelog entry for #3686 2026-04-10 14:37:00 +00:00
Asish Kumar 2459aa9404 Detect ZIP archives as binary content 2026-04-10 14:35:33 +00:00
Keith Hall 111aa2e10e Merge pull request #3682 from ltrzesniewski/slnx
Add .NET `slnx` extension
2026-04-08 05:20:55 +03:00
Lucas Trzesniewski c64b6761fe Add .NET slnx extension
This is the new XML-based format for solution files.
2026-04-07 23:25:40 +02:00
Keith Hall 11efacbe64 Merge pull request #3661 from sharkdp/diagnostic_themes
Update --diagnostic output with BAT_THEME_LIGHT/DARK env vars and detected theme type
2026-04-02 21:44:28 +03:00
Keith Hall 204ca18d71 Merge branch 'master' into diagnostic_themes 2026-04-02 21:36:24 +03:00
auto-merge-dependabot-prs[bot] a158f12106 Merge pull request #3669 from sharkdp/dependabot/cargo/quote-1.0.45
build(deps): bump quote from 1.0.40 to 1.0.45
2026-04-01 04:07:40 +00:00
dependabot[bot] 956f0aeb08 build(deps): bump quote from 1.0.40 to 1.0.45
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.40 to 1.0.45.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.40...1.0.45)

---
updated-dependencies:
- dependency-name: quote
  dependency-version: 1.0.45
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 03:54:15 +00:00
auto-merge-dependabot-prs[bot] 1a6073d2e8 Merge pull request #3668 from sharkdp/dependabot/cargo/tempfile-3.27.0
build(deps): bump tempfile from 3.23.0 to 3.27.0
2026-04-01 03:51:21 +00:00
dependabot[bot] 57e99e9bcd build(deps): bump tempfile from 3.23.0 to 3.27.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.23.0 to 3.27.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.23.0...v3.27.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-version: 3.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 03:38:08 +00:00
auto-merge-dependabot-prs[bot] 68eaa96e06 Merge pull request #3664 from sharkdp/dependabot/cargo/toml-1.1.1spec-1.1.0
build(deps): bump toml from 0.9.8 to 1.1.1+spec-1.1.0
2026-04-01 03:34:35 +00:00
dependabot[bot] de9ca4ef7b build(deps): bump toml from 0.9.8 to 1.1.1+spec-1.1.0
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.8 to 1.1.1+spec-1.1.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.8...toml-v1.1.1)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 1.1.1+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 03:13:41 +00:00
auto-merge-dependabot-prs[bot] 7bb905436b Merge pull request #3667 from sharkdp/dependabot/cargo/unicode-width-0.2.2
build(deps): bump unicode-width from 0.2.1 to 0.2.2
2026-04-01 03:11:34 +00:00
dependabot[bot] f820ad1050 build(deps): bump unicode-width from 0.2.1 to 0.2.2
Bumps [unicode-width](https://github.com/unicode-rs/unicode-width) from 0.2.1 to 0.2.2.
- [Commits](https://github.com/unicode-rs/unicode-width/compare/v0.2.1...v0.2.2)

---
updated-dependencies:
- dependency-name: unicode-width
  dependency-version: 0.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 02:41:02 +00:00
auto-merge-dependabot-prs[bot] 259e1266a8 Merge pull request #3665 from sharkdp/dependabot/cargo/unicode-segmentation-1.13.2
build(deps): bump unicode-segmentation from 1.12.0 to 1.13.2
2026-04-01 02:37:51 +00:00
dependabot[bot] 31b0a35719 build(deps): bump unicode-segmentation from 1.12.0 to 1.13.2
Bumps [unicode-segmentation](https://github.com/unicode-rs/unicode-segmentation) from 1.12.0 to 1.13.2.
- [Commits](https://github.com/unicode-rs/unicode-segmentation/compare/v1.12.0...v1.13.2)

---
updated-dependencies:
- dependency-name: unicode-segmentation
  dependency-version: 1.13.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 02:03:59 +00:00
Keith Hall 3e4169b8c3 cargo fmt 2026-03-31 22:35:21 +03:00
Keith Hall ba223289ce Update --diagnostic output with detected terminal color scheme type 2026-03-31 22:23:10 +03:00
Keith Hall abab6533e4 Update --diagnostic output with BAT_THEME_LIGHT/DARK env vars 2026-03-31 22:22:31 +03:00
Keith Hall 7d9884d6d2 Merge pull request #3655 from claw-explorer/docs/fish-abbr-removal
docs: add instructions for removing fish help abbreviations
2026-03-28 22:44:07 +02:00
Claw Explorer 652489251b docs: move removal instructions into a TIP callout
Address review feedback from @keith-hall: wrap the abbreviation
removal instructions in a GitHub callout (> [!TIP]) to visually
separate them from the main setup flow. Also reorder so the
'This way, you can keep on using...' sentence follows directly
after the abbreviation creation, improving readability.
2026-03-28 16:34:28 -04:00
Claw Explorer 89f3d2d31a docs: add instructions for removing fish help abbreviations
Add documentation showing how to erase the fish abbreviations for
--help and -h, since the dash-prefixed names make removal non-obvious.
The key is using -- before the abbreviation name to prevent fish from
interpreting it as a flag.

Closes #3536
2026-03-26 12:21:56 -04:00
Weixie Cui 5c3b8040db fix: add changelog entry for LESSCLOSE warning fix 2026-03-26 11:11:19 +08:00
Weixie Cui 7ff1fb3278 fix: warn when $LESSCLOSE fails, not when it succeeds
The Drop cleanup for Preprocessed inverted the exit-status check, so bat
emitted a warning when LESSCLOSE exited successfully and stayed silent on
failure. Align the condition with the intended behavior.
2026-03-26 11:10:58 +08:00
Keith Hall 3ee9ea9a8d Merge pull request #3652 from cyqsimon/syntax-mapping-glob-parse-cleanup
Builtin syntax mapping: cleanup matcher glob parsing logic
2026-03-25 22:01:52 +02:00
cyqsimon b511b928f4 Write changelog 2026-03-25 15:58:58 +08:00
cyqsimon 3e789f5241 Imprv: cleanup matcher glob parsing logic
- Data flow is now strictly linear from `RawMatcher` to `Matcher`
  - I've also hoisted `RawMatcher` in front of `Matcher` to signal this
- Re-type `RawMatcher.case_sensitive` from `bool` to `Option<bool>`
  - This moves all parser logic away from `RawMatcher`, making it a more faithful representation of the data
- Favour default consts in `Matcher::try_from<RawMatcher>` to `Default` impl on `Case`
  - Because the default choice of casing is a design decision of the logic, not an intrinsic property of the type
2026-03-25 15:58:45 +08:00
Keith Hall a500fb236a Merge pull request #3650 from Sim-hu/fix/i686-deb-architecture
fix: use correct Debian architecture name for i686 .deb
2026-03-24 22:37:48 +02:00
Keith Hall 515b9d62db Merge branch 'master' into fix/i686-deb-architecture 2026-03-24 22:23:29 +02:00
Keith Hall 62907b1da5 Merge pull request #3651 from sharkdp/minus_home_end_keys
Add Home/End key bindings to builtin minus pager
2026-03-24 22:22:53 +02:00
Keith Hall eef7107407 update changelog 2026-03-24 21:38:07 +02:00
copilot-swe-agent[bot] 87e043b91c Add Home/End key bindings to builtin minus pager
Co-authored-by: keith-hall <11882719+keith-hall@users.noreply.github.com>
2026-03-24 21:33:43 +02:00
Sim-hu b8d462ba87 fix: add PR number and author to changelog entry 2026-03-24 19:24:36 +09:00
Sim-hu ca3ef28d56 fix: use correct Debian architecture name for i686 .deb package
The i686 .deb package declared its architecture as "i686", which
Debian does not recognize. Debian uses "i386" for 32-bit x86,
matching the convention already used by the other targets (arm64,
armhf, amd64).

Closes #3611
2026-03-24 19:20:18 +09:00
Sungjoon Moon 0b4c886efc ci: Use git version of cross for better target support 2026-03-24 06:09:32 +01:00
Keith Hall 780fdeb888 Merge pull request #3576 from vorburger/patch-2
feat: Map BUILD to Python (Starlark) for Bazel (fixes #3575)
2026-03-22 20:14:05 +02:00
Keith Hall 7a9c6e0de5 Merge branch 'master' into patch-2 2026-03-22 17:47:24 +02:00
Keith Hall a1e7c0ab4a Merge pull request #3643 from mvanhorn/feat/diff-plain-compatibility
feat: preserve change markers when combining --diff with --plain
2026-03-21 16:30:43 +02:00
Keith Hall 1e36873704 Merge branch 'master' into feat/diff-plain-compatibility 2026-03-21 16:21:56 +02:00
Matt Van Horn e86797fbf4 style: auto-format integration tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 15:46:54 -07:00
Matt Van Horn e60875ac12 fix: gate Changes component on git feature flag
When bat is built without the `git` feature, `StyleComponent::Changes`
is not available. Add `#[cfg(feature = "git")]` guard so `--diff --plain`
works in both configurations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 15:27:10 -07:00
Keith Hall 66eece4e2c Merge branch 'master' into patch-2 2026-03-20 23:57:19 +02:00
Keith Hall a19593b383 improvements from PR review
1. Added `Deserialize` derive and `#[serde(try_from = "RawMatcher")]` to the `Matcher` struct. With `try_from`, serde generates a `Deserialize` impl that first deserializes into `RawMatcher`, then calls `TryFrom<RawMatcher> for Matcher`. It never attempts to deserialize `Matcher`'s fields directly, so `Case` and `MatcherSegment` don't need `Deserialize` impls.
2. Replaced the manual `impl<'de> serde::Deserialize<'de> for Matcher` with a standard `impl TryFrom<RawMatcher> for Matcher`. The logic is identical - the conversion is fallible because `Matcher::from_str` returns `Result<_, anyhow::Error>`, so `try_from` (not `from`) is the correct choice, avoiding any panics.

The net effect: same behavior, same error handling, but using the idiomatic serde pattern instead of a manual deserializer impl. The `RawMatcher` intermediate type and its `#[serde(untagged)]` derive remain unchanged.
2026-03-20 23:53:30 +02:00
Keith Hall 7a6f442c86 improvements from PR review
- prefer to use `Default::default` because it's semantically clearer why we've chosen to use a particular value
2026-03-20 23:35:27 +02:00
Keith Hall 3767f15c2a improvements from PR review
fix documentation about syntax mappings
2026-03-20 23:34:38 +02:00
Keith Hall 1932d1da39 Merge pull request #3642 from mvanhorn/fix/deb-musl-package-names
fix: consistent .deb MUSL package names
2026-03-20 22:57:41 +02:00
Keith Hall b0e5590864 Merge branch 'master' into fix/deb-musl-package-names 2026-03-20 22:42:24 +02:00
Keith Hall f2daa1eb6e Merge pull request #3640 from eyupcanakman/fix/binary-as-text-width-3631
Fix line wrapping for files with control characters
2026-03-20 22:40:28 +02:00
Eyüp Can Akman 1f540752ef fix: reference PR number in CHANGELOG entry 2026-03-20 22:28:39 +02:00
Eyüp Can Akman fc94a0ec49 fix: account for caret notation width in text wrapping
Control characters displayed in caret notation (e.g. ^@ for NUL)
occupy 2 terminal columns, but the width calculation treated them
as 0-width. Add a char_width() helper that returns 2 for control
characters, fixing incorrect line wrapping with --binary=as-text.

Fixes #3631
2026-03-20 22:28:39 +02:00
Matt Van Horn 169dc7c45b test: add integration tests for --diff combined with --plain 2026-03-20 10:33:33 -07:00
Matt Van Horn 5e140558b1 fix: update changelog entry with PR number for CI check 2026-03-19 21:38:03 -07:00
Matt Van Horn 99c8e15c27 fix: update changelog entry with PR number for CI check 2026-03-19 21:37:45 -07:00
Matt Van Horn 2a3ed948ec feat: preserve change markers when combining --diff with --plain
When --plain is set (via CLI or config file), --diff output loses all
visual markers, making it indistinguishable from plain text. The diff
line filtering still works, but without Changes markers or Snip
separators the output is not useful.

Automatically include Changes and Snip style components when --diff is
active alongside --plain. This preserves the --plain intent (no grid,
no header, no line numbers) while keeping diff output readable.

Closes #3630
2026-03-19 21:35:48 -07:00
Matt Van Horn 618d7340bb fix: consistent .deb MUSL package names
Fix two inconsistencies in MUSL .deb package naming:

1. Change dpkg_arch for aarch64-unknown-linux-musl from `arm64` to
   `musl-linux-arm64` to match the convention used by other MUSL targets.

2. Change the DPKG_BASENAME case pattern from `*-musl)` to `*-musl*)`
   so it also matches `musleabihf`, giving the arm target the `bat-musl`
   prefix like all other MUSL packages.

After this fix, all MUSL .deb packages follow the consistent pattern:
`bat-musl_VERSION_musl-linux-ARCH.deb`

Closes #3482
2026-03-19 21:34:12 -07:00
Keith Hall 4a38eab3ea Merge pull request #3628 from XploitMonk0x01/codex/clarify-bat-vs-batcat
docs: clarify Ubuntu and Debian executable name
2026-03-19 23:00:59 +02:00
XploitMonk0x01 9fccdbc484 docs: clarify Ubuntu and Debian executable name 2026-03-19 22:50:23 +02:00
Keith Hall c6e661d80b cargo fmt 2026-03-18 22:25:07 +02:00
Keith Hall 031ba0953a Merge branch 'master' into patch-2 2026-03-18 22:17:18 +02:00
copilot-swe-agent[bot] 2a29802dd5 Refactor: string-or-struct matcher syntax, Case enum, remove case_sensitive_mappings table
Co-authored-by: keith-hall <11882719+keith-hall@users.noreply.github.com>
2026-03-18 22:16:50 +02:00
Keith Hall 31e30a2e97 Merge pull request #3636 from victor-gp/add-mapping-for-ignore-files
Add mapping for ignore files
2026-03-18 22:08:02 +02:00
Víctor González Prieto ffed52f6eb Document how to contribute syntax mappings 2026-03-18 21:58:23 +02:00
Víctor González Prieto a4e853c4fa Map ignore files to Git Ignore syntax
This would catch any dotfiles that end in "ignore", but not `.ignore`

Set as a lower priority rule (90 vs the usual 50) because it's a blanket
assignation. If there are conflict with other, more specific rules,
those will take precedence.
2026-03-18 21:58:23 +02:00
Keith Hall f11374535d Merge pull request #3629 from Rohan5commit/docs/fix-readme-wording
docs: fix README sidebar wording
2026-03-18 20:37:04 +02:00
rohan436 de209f8a23 docs: fix README sidebar wording 2026-03-18 20:15:53 +02:00
Keith Hall 22c38b6fcb Merge remote-tracking branch 'origin/master' into patch-2 2026-03-18 05:19:21 +02:00
Keith Hall 63d5796f8f Merge pull request #3635 from victor-gp/add-mappings-for-gcloud-cli
Add syntax mappings for GCloud CLI config files
2026-03-17 22:45:12 +02:00
Víctor González Prieto a1917275cd Add syntax mappings for GCloud CLI config files
> .boto => INI

I was uninstalling gcloud-cli and the docs said to review the contents
of ~/.boto [^1].

`bat` doesn't recognize it but it's just an INI-like file to configure
gsutil [^2].

I took the chance to add a few more mappings for other GCloud config
files without extension...

> .gcloudignore => Git Ignore

This one's self-explanatory, but it's documented in [^3].

Makes me wonder if we could just do a blanket `Git Ignore = ".?*ignore"`
for every kind of ignore file. For instance, `bat` doesn't catch
.dockerignore either.

> **/gcloud/configurations/config_* => INI

These are configuration files for each different profile you have
configured for the gcloud-cli [^4].

They're named like "config_default" or "config_<name>", without a
file extension, but they are also INI syntax [^5][^6].

[^1]: https://docs.cloud.google.com/sdk/docs/uninstall-cloud-sdk
[^2]: https://docs.cloud.google.com/storage/docs/boto-gsutil
[^3]: https://docs.cloud.google.com/sdk/gcloud/reference/topic/gcloudignore
[^4]: https://docs.cloud.google.com/sdk/docs/configurations
[^5]: https://stackoverflow.com/questions/74290882/return-active-gcp-project-name-programatically-for-both-local-dev-and-deployed-c/7^4292151#74292151
[^6]: https://stackoverflow.com/questions/49212350/where-does-gcloud-store-its-defaults/49256835#49256835
2026-03-17 20:51:23 +01:00
Keith Hall 884765bdeb Merge remote-tracking branch 'origin/master' into patch-2 2026-03-14 09:21:19 +02:00
Keith Hall 56fe0fa226 Add case-sensitive glob support to syntax mapping
to allow us to map `BUILD` case sensitively to Python for Skylark
2026-03-14 09:18:56 +02:00
Keith Hall d9adfe90fc Merge pull request #3627 from XploitMonk0x01/codex/clarify-theme-format
docs: clarify supported custom theme format
2026-03-14 09:08:30 +02:00
XploitMonk0x01 b1f04995a6 docs: clarify supported custom theme format 2026-03-14 11:44:38 +05:30
auto-merge-dependabot-prs[bot] ea6d2ac911 Merge pull request #3605 from sharkdp/dependabot/submodules/assets/syntaxes/02_Extra/cmd-help-db0a616
build(deps): bump assets/syntaxes/02_Extra/cmd-help from `273cb98` to `db0a616`
2026-03-11 17:06:16 +00:00
dependabot[bot] ac97356930 build(deps): bump assets/syntaxes/02_Extra/cmd-help
Bumps [assets/syntaxes/02_Extra/cmd-help](https://github.com/victor-gp/cmd-help-sublime-syntax) from `273cb98` to `db0a616`.
- [Commits](https://github.com/victor-gp/cmd-help-sublime-syntax/compare/273cb988177e96f4187e06008b13fa72ad22ae4d...db0a616cfa367a4f52fd81d1a46c8ebea11e867d)

---
updated-dependencies:
- dependency-name: assets/syntaxes/02_Extra/cmd-help
  dependency-version: db0a616cfa367a4f52fd81d1a46c8ebea11e867d
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-11 16:54:46 +00:00
Keith Hall 23abbdcea9 Merge pull request #3584 from adukhan99/add-cobol-syntax
Add COBOL syntax highlighting
2026-03-11 18:51:14 +02:00
Alex Dukhan 927873392b Drop .ss extension from COBOL 2026-03-11 15:06:47 +00:00
Alex Dukhan 91965d28a6 Add COBOL sans CPY ext 2026-03-11 14:36:20 +00:00
Alex Dukhan 88ff40f93e Merge branch 'master' into add-cobol-syntax 2026-03-11 11:26:01 +00:00
zachvalenta 652d50e92e Makefile syntax for justfiles 2026-03-08 21:06:19 -04:00
Keith Hall 0dcdcb678a Merge pull request #3621 from Xavrir/fix-symlink-syntax-detection
Fix syntax highlighting for symlinked files by resolving target path
2026-03-08 20:01:25 +02:00
Rizky Mirzaviandy Priambodo f49641a6c3 Fix syntax highlighting for symlinked files by resolving target path
When a symlink name has no recognizable extension (e.g. Aliases/0install),
syntax detection fails because the symlink path doesn't match any syntax.
The target file may have a recognizable extension (e.g. Formula/zero-install.rb)
but was never consulted.

When path-based syntax detection fails with UndetectedSyntax, canonicalize
the path to resolve symlinks and retry detection with the target path.
This preserves existing behavior where the symlink path itself matches
(e.g. .ssh/config), since the original path is tried first.

Closes #1001
2026-03-09 00:01:09 +07:00
Keith Hall fc4f12b390 Merge pull request #3620 from Xavrir/fix-bat-config-dir-duplicate
Fix BAT_CONFIG_DIR pointing at system config directory causing duplicate flag errors
2026-03-08 08:23:14 +02:00
Rizky Mirzaviandy Priambodo 9cce9e04d2 Fix BAT_CONFIG_DIR pointing at system config dir causing duplicate flag errors
When BAT_CONFIG_DIR is set to the system config directory (e.g. /etc/bat),
both system_config_file() and config_file() resolve to /etc/bat/config.
The config file was read twice, causing clap errors for non-repeatable flags
like --italic-text.

Skip the user config read when it resolves to the same file as the system
config, using canonicalization to also handle symlinks.

Closes #3589
2026-03-08 12:20:27 +07:00
Rizky Mirzaviandy Priambodo 844bfded50 Add --fallback-syntax for undetected files (#3617)
* feat(cli): add fallback syntax option

Expose a new fallback syntax CLI option so users can opt into syntax highlighting only when auto-detection fails.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(syntax): apply fallback only after detection fails

Use the fallback syntax only when path and first-line detection fail, preserving existing behavior for detected files and explicit language selection.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* test(cli): cover fallback syntax behavior

Add integration coverage for fallback syntax usage, precedence with --language, and no-op behavior when syntax is already detected; update help snapshots for the new option.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* docs(changelog): document fallback syntax option

Record the new fallback syntax feature in the unreleased changelog section.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-08 05:18:29 +02:00
Matei6942 ab80bd9717 feat(syntax): add support for hidden_file_extensions (#3613)
* feat(syntax): add support for hidden_file_extensions
2026-03-06 20:14:20 +02:00
foxfromworld a1a10c777f Docs: clarify PATH requirement and explain highlighted outputs (Fixes… (#3610)
* Docs: clarify PATH requirement and explain highlighted outputs (Fixes #2376)

* Update CHANGELOG for PR #3610

* Apply suggestion from @keith-hall

Applied reviewer’s suggestion to improve PATH export: safer with quotes and more convenient with $(pwd).

Co-authored-by: Keith Hall <keith-hall@users.noreply.github.com>

* Docs: clarify PATH setup in CONTRIBUTING.md, remove that section from README.md

* Docs: add cross reference to syntax tests step 5 in doc/assets.md

---------

Co-authored-by: Keith Hall <keith-hall@users.noreply.github.com>
2026-03-05 05:55:05 +00:00
Varun Chawla cc5f782d28 Add word wrapping mode (#3597)
* feat: add word wrapping mode for --wrap flag

* Run `cargo fmt` and add CHANGELOG entry

* Add word wrap tests, update manpage and shell completions

- Add integration tests for word wrapping: basic word boundary breaking,
  fallback to character wrapping for long words, line numbers, and
  short lines that fit without wrapping
- Update manpage to document the new 'word' wrapping mode
- Update bash, fish, zsh, and PowerShell completions with 'word' option
- Avoid unnecessary clone of `line_buf` when word wrap is disabled

* make clippy and cargo fmt happy

---------

Co-authored-by: Keith Hall <keith-hall@users.noreply.github.com>
2026-03-03 05:18:49 +02:00
Keith Hall 1424f9d6bf Merge pull request #3609 from Nicolas0315/fix/builtin-pager-drop-panic
Fix panic in BuiltinPager Drop when pager thread fails (#3449)
2026-03-02 04:57:37 +02:00
Nicolas fd67095cff Fix panic in BuiltinPager drop when pager thread panics
The Drop impl for OutputType::BuiltinPager calls
handle.take().unwrap().join().unwrap() which panics if the
pager thread itself panicked. In Drop, this causes a double
panic (abort).

Replace with if-let and discard the join result, matching the
pattern used for OutputType::Pager which also uses let _ =.

Reported in #3449 where BAT_PAGER=builtin with --help
causes the pager thread to fail.

Fixes #3449
2026-03-02 04:12:50 +09:00
auto-merge-dependabot-prs[bot] 908b1f22d1 Merge pull request #3608 from sharkdp/dependabot/cargo/bytesize-2.3.1
build(deps): bump bytesize from 1.3.0 to 2.3.1
2026-03-01 04:47:12 +00:00
dependabot[bot] 17a70d9b8c build(deps): bump bytesize from 1.3.0 to 2.3.1
Bumps [bytesize](https://github.com/bytesize-rs/bytesize) from 1.3.0 to 2.3.1.
- [Release notes](https://github.com/bytesize-rs/bytesize/releases)
- [Changelog](https://github.com/bytesize-rs/bytesize/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bytesize-rs/bytesize/compare/v1.3.0...bytesize-v2.3.1)

---
updated-dependencies:
- dependency-name: bytesize
  dependency-version: 2.3.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-01 04:36:13 +00:00
auto-merge-dependabot-prs[bot] 49d77a469a Merge pull request #3607 from sharkdp/dependabot/cargo/clap-4.5.60
build(deps): bump clap from 4.5.56 to 4.5.60
2026-03-01 04:33:43 +00:00
dependabot[bot] fa35495888 build(deps): bump clap from 4.5.56 to 4.5.60
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.56 to 4.5.60.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.56...clap_complete-v4.5.60)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.60
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-01 04:18:28 +00:00
auto-merge-dependabot-prs[bot] 8a858b19dc Merge pull request #3606 from sharkdp/dependabot/cargo/git2-0.20.4
build(deps): bump git2 from 0.20.3 to 0.20.4
2026-03-01 04:16:23 +00:00
dependabot[bot] 397252d2cc build(deps): bump git2 from 0.20.3 to 0.20.4
Bumps [git2](https://github.com/rust-lang/git2-rs) from 0.20.3 to 0.20.4.
- [Changelog](https://github.com/rust-lang/git2-rs/blob/git2-0.20.4/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/git2-rs/compare/git2-0.20.3...git2-0.20.4)

---
updated-dependencies:
- dependency-name: git2
  dependency-version: 0.20.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-01 04:00:55 +00:00
auto-merge-dependabot-prs[bot] 5e59957349 Merge pull request #3603 from sharkdp/dependabot/cargo/nix-0.31.2
build(deps): bump nix from 0.30.1 to 0.31.2
2026-03-01 03:58:09 +00:00
dependabot[bot] 90f8e00e0c build(deps): bump nix from 0.30.1 to 0.31.2
Bumps [nix](https://github.com/nix-rust/nix) from 0.30.1 to 0.31.2.
- [Changelog](https://github.com/nix-rust/nix/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nix-rust/nix/compare/v0.30.1...v0.31.2)

---
updated-dependencies:
- dependency-name: nix
  dependency-version: 0.31.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-01 03:35:51 +00:00
auto-merge-dependabot-prs[bot] 673929c189 Merge pull request #3604 from sharkdp/dependabot/cargo/serde_with-3.17.0
build(deps): bump serde_with from 3.16.1 to 3.17.0
2026-03-01 03:32:28 +00:00
dependabot[bot] e36bb8cc66 build(deps): bump serde_with from 3.16.1 to 3.17.0
Bumps [serde_with](https://github.com/jonasbb/serde_with) from 3.16.1 to 3.17.0.
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.16.1...v3.17.0)

---
updated-dependencies:
- dependency-name: serde_with
  dependency-version: 3.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-01 03:04:00 +00:00
Keith Hall 7d7e202bd5 Merge pull request #3588 from IMaloney/fix/warn-missing-pager
warn when pager is missing instead of silently falling back
2026-02-20 20:46:38 +02:00
Keith Hall 74de075748 Merge branch 'master' into fix/warn-missing-pager 2026-02-20 20:37:07 +02:00
Keith Hall 594069581d Merge pull request #3592 from IMaloney/fix/wrap-never-clean
respect --wrap=never flag when piping to pager
2026-02-20 20:26:23 +02:00
Ian Maloney 62b0388236 remove code comments 2026-02-20 06:12:14 +00:00
Ian Maloney 16d041492c fix pr number in changelog 2026-02-20 06:07:23 +00:00
Ian Maloney 22ee03ff00 add changelog entry for wrap-never bug fix 2026-02-20 06:04:29 +00:00
Ian Maloney 790bed3a2d fix: respect --wrap=never flag when paging is enabled
When output is piped to a pager, the wrapping_mode logic was not checking the explicit --wrap=never flag and would always default to NoWrapping(false). This meant the -S flag was not passed to less, causing lines to wrap despite the user's explicit request.

The fix prioritizes explicit CLI flags (--wrap=never, --chop-long-lines) over the interactive_output-based logic, ensuring they are always respected.

Fixes #3587
2026-02-20 06:04:14 +00:00
Ian Maloney bc84854d4b use cat as test pager instead of builtin (builtin is interactive, doesnt output to stdout) 2026-02-20 00:19:47 +00:00
Ian Maloney 319811df01 fix test: use builtin pager instead of less, revert to warn for all missing pagers 2026-02-20 00:09:02 +00:00
Ian Maloney 2c1a8caadd simplify: just never warn for less (universal default) 2026-02-19 22:49:02 +00:00
Ian Maloney a240aa4afd never warn for missing 'less' pager (common default) 2026-02-19 22:21:17 +00:00
Ian Maloney 00e38cd056 only warn for explicitly configured pagers, not defaults 2026-02-19 22:01:58 +00:00
Ian Maloney 335eff51f3 fix type error, pager.bin is already a string 2026-02-19 21:44:47 +00:00
Ian Maloney d04b960c05 warn when pager is missing instead of silently falling back
when a configured pager (via BAT_PAGER, PAGER, or --pager) is not found,
bat now shows a warning message before falling back to stdout. this helps
users understand why their pager isn't running and makes it obvious when
there's a typo or PATH issue.

fixes issue #2904
2026-02-19 21:29:59 +00:00
Keith Hall fbc05da785 Merge pull request #3586 from BlueElectivire/master
Fixed manpage syntax to remove ANSI artifacts
2026-02-19 18:56:14 +02:00
Guy Barzilai a71d16fa1b Fixed manpage syntax-test format 2026-02-19 18:11:36 +02:00
Guy Barzilai 5bd08845a3 Fixed manpage regex regression and add syntax test 2026-02-18 23:59:38 +02:00
Guy Barzilai 3ebfbb7ac5 Fixed manpage syntax to remove ANSI artifacts 2026-02-17 21:54:58 +02:00
Alex Dukhan 2b47f3c5eb Updated CHANGELOG.md 2026-02-12 22:10:08 +00:00
Alex Dukhan 9f8ac934ab Updated CHANGELOG.md 2026-02-12 21:43:34 +00:00
Alex Dukhan ab393a0281 Add COBOL syntax highlighting 2026-02-12 21:06:03 +00:00
Keith Hall 990b2fc06f Merge pull request #3583 from mainnebula/steward/sharkdp-bat-3555-2de8ac3c
feat: implement --unbuffered mode for streaming input
2026-02-12 08:40:44 +02:00
mainnebula 0c883d6f28 docs: add changelog entry for --unbuffered mode (#3583) 2026-02-11 23:45:20 -05:00
mainnebula 167dda63a8 feat: implement --unbuffered mode for streaming input (#3555)
Repurpose the existing --unbuffered/-u flag (previously a POSIX no-op)
to enable unbuffered input reading using fill_buf()/consume() instead
of read_until(b'\n'). This allows partial lines to display immediately
when piping streaming input like `tail -f` into bat.

- Add unbuffered field to Config and InputReader
- Add read_line_unbuffered() using BufRead::fill_buf()/consume()
- Add flush() to OutputHandle, called after each line in unbuffered mode
- Auto-disable line numbers in unbuffered mode to avoid partial line confusion
- Update help text, man page, and shell completions
- Add unit tests and integration tests
2026-02-11 23:27:08 -05:00
dddffgg cb8b637574 fix(cache): allow --help flag for cache subcommand (#3580)
* fix(cache): allow --help flag for cache subcommand

* docs: add changelog entry for cache --help fix
2026-02-09 05:06:40 +02:00
Keith Hall 391e7ff072 Merge pull request #3581 from NORMAL-EX/chore/update-time-crate
chore(deps): update time crate to 0.3.47 (RUSTSEC-2026-0009)
2026-02-07 13:37:17 +02:00
dddffgg 2b517199a2 docs: add changelog entry for MSRV bump and time crate update 2026-02-07 19:23:57 +08:00
dddffgg a0f326aa22 chore: bump MSRV to 1.88 2026-02-07 19:23:47 +08:00
dddffgg a4a7692134 chore(deps): update time crate to 0.3.47 (RUSTSEC-2026-0009) 2026-02-06 21:32:02 +08:00
Keith Hall 3df9d581b7 Merge pull request #3527 from Anchal-T/master
Fix bat crash with BusyBox less on Windows
2026-02-03 22:40:04 +02:00
Keith Hall eb724f2dc4 Merge branch 'master' into master 2026-02-03 22:27:30 +02:00
Keith Hall c491488140 Merge pull request #3578 from vorburger/nix
Add initial flake.nix; just for develop, for now (fixes#3577)
2026-02-03 22:25:49 +02:00
Michael Vorburger 3f12b1c1ab docs: Use PR not Issue ID in CHANGELOG.md 2026-02-01 14:20:40 +01:00
Michael Vorburger 2c682ff6a6 docs: Document flake.nix in CHANGELOG.md (see #3577) 2026-02-01 14:13:03 +01:00
Michael Vorburger 36b37e1aa5 Add x86_64-darwin (Intel macOS) to flake.nix 2026-02-01 14:11:05 +01:00
Michael Vorburger d19c80a7c1 Remove allowUnfree = true from flake.nix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-01 14:09:18 +01:00
Michael Vorburger eff57943c9 Add initial flake.nix; just for develop, for now (fixes#3577) 2026-02-01 14:06:05 +01:00
Michael Vorburger 5a4a7de933 docs: Document mapping BUILD to Python (Starlark) for Bazel in CHANGELOG.md (see #3576) 2026-02-01 13:40:51 +01:00
Michael Vorburger ba97230b98 feat: Map BUILD to Python (Starlark) for Bazel (fixes #3575)
See #3575.
2026-02-01 13:34:33 +01:00
Keith Hall 8664cc9df9 Merge pull request #3563 from NORMAL-EX/feat/quiet-empty
feat: add --quiet-empty flag to suppress output on empty input
2026-02-01 11:15:45 +02:00
dddffgg 3b34b47d55 docs: add --quiet-empty to PowerShell completion 2026-02-01 16:21:38 +08:00
dddffgg 9a5519057c docs: add --quiet-empty to fish completion 2026-02-01 16:21:22 +08:00
dddffgg 93411a96e4 docs: add --quiet-empty to zsh completion 2026-02-01 16:21:05 +08:00
dddffgg bd461afd75 docs: add --quiet-empty to bash completion 2026-02-01 16:20:49 +08:00
dddffgg 55306c15a6 docs: add --quiet-empty to man page 2026-02-01 16:20:27 +08:00
dddffgg afd804ebea Merge branch 'master' into feat/quiet-empty 2026-02-01 14:50:07 +08:00
auto-merge-dependabot-prs[bot] 3d0a7ed0a0 Merge pull request #3568 from sharkdp/dependabot/cargo/indexmap-2.13.0
build(deps): bump indexmap from 2.12.1 to 2.13.0
2026-02-01 05:30:27 +00:00
dependabot[bot] 8f6f5dfbd2 build(deps): bump indexmap from 2.12.1 to 2.13.0
Bumps [indexmap](https://github.com/indexmap-rs/indexmap) from 2.12.1 to 2.13.0.
- [Changelog](https://github.com/indexmap-rs/indexmap/blob/main/RELEASES.md)
- [Commits](https://github.com/indexmap-rs/indexmap/compare/2.12.1...2.13.0)

---
updated-dependencies:
- dependency-name: indexmap
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 05:19:49 +00:00
auto-merge-dependabot-prs[bot] 43ab174d42 Merge pull request #3567 from sharkdp/dependabot/cargo/proc-macro2-1.0.106
build(deps): bump proc-macro2 from 1.0.103 to 1.0.106
2026-02-01 05:16:54 +00:00
dependabot[bot] b396ab865f build(deps): bump proc-macro2 from 1.0.103 to 1.0.106
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.103 to 1.0.106.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.103...1.0.106)

---
updated-dependencies:
- dependency-name: proc-macro2
  dependency-version: 1.0.106
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 04:54:36 +00:00
auto-merge-dependabot-prs[bot] 8a82847121 Merge pull request #3571 from sharkdp/dependabot/cargo/serde_with-3.16.1
build(deps): bump serde_with from 3.15.1 to 3.16.1
2026-02-01 04:48:09 +00:00
dependabot[bot] c3c19c8018 build(deps): bump serde_with from 3.15.1 to 3.16.1
Bumps [serde_with](https://github.com/jonasbb/serde_with) from 3.15.1 to 3.16.1.
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.15.1...v3.16.1)

---
updated-dependencies:
- dependency-name: serde_with
  dependency-version: 3.16.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 04:19:31 +00:00
auto-merge-dependabot-prs[bot] 0d2078b32b Merge pull request #3569 from sharkdp/dependabot/cargo/clap-4.5.56
build(deps): bump clap from 4.5.46 to 4.5.56
2026-02-01 04:15:25 +00:00
dependabot[bot] cddfad83e4 build(deps): bump clap from 4.5.46 to 4.5.56
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.46 to 4.5.56.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.46...clap_complete-v4.5.56)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.56
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 03:49:22 +00:00
dddffgg 2e355ffa4e Merge branch 'master' into feat/quiet-empty 2026-02-01 11:48:43 +08:00
dddffgg fa66d8e3ef fix: correct position of --quiet-empty in long-help.txt 2026-02-01 11:48:10 +08:00
dddffgg cb83b8fb9a fix: correct position of --quiet-empty in short-help.txt 2026-02-01 11:47:24 +08:00
auto-merge-dependabot-prs[bot] adfdcf4c6d Merge pull request #3564 from sharkdp/dependabot/cargo/git2-0.20.3
build(deps): bump git2 from 0.20.2 to 0.20.3
2026-02-01 03:39:56 +00:00
dependabot[bot] 593cf76811 build(deps): bump git2 from 0.20.2 to 0.20.3
Bumps [git2](https://github.com/rust-lang/git2-rs) from 0.20.2 to 0.20.3.
- [Changelog](https://github.com/rust-lang/git2-rs/blob/git2-0.20.3/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/git2-rs/compare/git2-0.20.2...git2-0.20.3)

---
updated-dependencies:
- dependency-name: git2
  dependency-version: 0.20.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 03:03:36 +00:00
dddffgg 3aaec8cb3b docs: update long-help.txt with --quiet-empty flag 2026-01-31 17:02:24 +08:00
dddffgg 2030ffa664 docs: update short-help.txt with --quiet-empty flag 2026-01-31 17:02:12 +08:00
dddffgg edb8342eab docs: fix changelog entry to reference PR number 2026-01-31 16:48:03 +08:00
dddffgg 6a7936a26f test: add integration tests for --quiet-empty flag 2026-01-31 16:45:46 +08:00
dddffgg 26118afde0 docs: add changelog entry for --quiet-empty flag 2026-01-31 16:44:39 +08:00
dddffgg 783acbc83d feat: implement quiet_empty behavior in printer 2026-01-31 16:42:28 +08:00
dddffgg 6f0a61cef9 feat: wire up quiet_empty config in app 2026-01-31 16:41:46 +08:00
dddffgg 53a10e0b0a feat: add --quiet-empty CLI flag 2026-01-31 16:41:13 +08:00
dddffgg 626154317f feat: add quiet_empty config option 2026-01-31 16:40:59 +08:00
Keith Hall 885ae1dc81 Merge pull request #3561 from cerdelen/small_typo_fix
Small typo fixes
2026-01-28 22:03:09 +02:00
Cedric Erdelen f6f1f18e9c remove trailing whitespace 2026-01-28 19:59:10 +01:00
cerdelen 69a2ffbdac Update src/config.rs
Co-authored-by: Keith Hall <keith-hall@users.noreply.github.com>
2026-01-28 19:48:02 +01:00
Cedric Erdelen 6b8decf2a4 Small typo fixes 2026-01-28 14:12:32 +01:00
Keith Hall cd06fe46e0 Merge pull request #3552 from CosmicHorrorDev/remove-vs-dark+
Remove Visual Studio Dark+ theme
2026-01-15 22:13:41 +02:00
Cosmic Horror 099d716455 Remove Visual Studio Dark+ theme 2026-01-14 21:13:44 -07:00
auto-merge-dependabot-prs[bot] 298e1f5321 Merge pull request #3506 from sharkdp/dependabot/github_actions/actions/checkout-6
build(deps): bump actions/checkout from 5 to 6
2026-01-12 05:14:43 +00:00
dependabot[bot] 3cb55e85d0 build(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-12 06:04:14 +01:00
Keith Hall 981f25989a Merge pull request #3550 from nmacl/master
Use cargo_bin! macro to fix tests for future Cargo versions
2026-01-10 13:53:09 +02:00
Your Name 57bf8ff7da Update changelog with PR number 2026-01-08 09:45:41 -05:00
Your Name 31f982e736 Fixed formatting 2026-01-08 09:42:42 -05:00
Your Name 0a97a1edd7 Add changelog entry 2026-01-08 09:35:01 -05:00
Your Name 6c75acfffc Replace deprecated cargo_bin with cargo_bin! macro
The old Command::cargo_bin() is deprecated and will break when Cargo
changes build directory layout. Updated to use cargo_bin!() macro instead.

Bumped assert_cmd to 2.0.16 to get the new macro.

Fixes #3528
2026-01-08 09:28:11 -05:00
auto-merge-dependabot-prs[bot] 4e84e838a3 Merge pull request #3544 from sharkdp/dependabot/cargo/shell-words-1.1.1
build(deps): bump shell-words from 1.1.0 to 1.1.1
2026-01-01 04:33:25 +00:00
dependabot[bot] d48bf240a7 build(deps): bump shell-words from 1.1.0 to 1.1.1
Bumps [shell-words](https://github.com/tmiasko/shell-words) from 1.1.0 to 1.1.1.
- [Commits](https://github.com/tmiasko/shell-words/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: shell-words
  dependency-version: 1.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 04:20:47 +00:00
auto-merge-dependabot-prs[bot] 7dc180eefb Merge pull request #3542 from sharkdp/dependabot/cargo/console-0.16.2
build(deps): bump console from 0.16.1 to 0.16.2
2026-01-01 04:18:01 +00:00
dependabot[bot] 3276e9469a build(deps): bump console from 0.16.1 to 0.16.2
Bumps [console](https://github.com/console-rs/console) from 0.16.1 to 0.16.2.
- [Release notes](https://github.com/console-rs/console/releases)
- [Changelog](https://github.com/console-rs/console/blob/main/CHANGELOG.md)
- [Commits](https://github.com/console-rs/console/compare/0.16.1...0.16.2)

---
updated-dependencies:
- dependency-name: console
  dependency-version: 0.16.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 04:03:19 +00:00
auto-merge-dependabot-prs[bot] 60902c796f Merge pull request #3541 from sharkdp/dependabot/cargo/nu-ansi-term-0.50.3
build(deps): bump nu-ansi-term from 0.50.1 to 0.50.3
2026-01-01 04:00:33 +00:00
dependabot[bot] c12cdcace3 build(deps): bump nu-ansi-term from 0.50.1 to 0.50.3
Bumps [nu-ansi-term](https://github.com/nushell/nu-ansi-term) from 0.50.1 to 0.50.3.
- [Release notes](https://github.com/nushell/nu-ansi-term/releases)
- [Changelog](https://github.com/nushell/nu-ansi-term/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nushell/nu-ansi-term/compare/v0.50.1...v0.50.3)

---
updated-dependencies:
- dependency-name: nu-ansi-term
  dependency-version: 0.50.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 03:19:08 +00:00
auto-merge-dependabot-prs[bot] 823fd9f5c6 Merge pull request #3539 from sharkdp/dependabot/cargo/tempfile-3.23.0
build(deps): bump tempfile from 3.16.0 to 3.23.0
2026-01-01 03:15:20 +00:00
dependabot[bot] 88d6fd0248 build(deps): bump tempfile from 3.16.0 to 3.23.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.16.0 to 3.23.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.16.0...v3.23.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-version: 3.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 03:02:10 +00:00
Keith Hall 32047346e3 Merge pull request #3534 from ltrzesniewski/windows-clone
Make the repository clone correctly under Windows
2025-12-28 08:02:56 +02:00
Lucas Trzesniewski 56678dc771 Change the Jinja2 fork URL 2025-12-28 07:53:27 +02:00
Lucas Trzesniewski b34a1e0cc2 Make the repository clone correctly under Windows 2025-12-28 07:53:27 +02:00
Keith Hall 41f55bf902 Merge pull request #3535 from ltrzesniewski/windows-build
Fix integration tests on Windows
2025-12-28 07:46:13 +02:00
Lucas Trzesniewski c8514543af Fix integration tests on Windows 2025-12-27 21:17:40 +01:00
Hakil ad608014d9 fix issue #3526 (#3529) 2025-12-23 19:08:38 +02:00
Anchal 782ce71247 Update issue references in CHANGELOG.md 2025-12-16 22:40:35 +05:30
Anchal 8c7fccc8a5 Update changelog for feature #3527 2025-12-16 22:33:44 +05:30
ANCHAL fca5502f21 Fix bat crash with BusyBox less on Windows
- Retrieve less version earlier in src/output.rs.
- Skip -K argument if less is detected as BusyBox version.
- Reuses the version check for the existing --no-init logic.
- Fixes #3518.
2025-12-16 22:25:27 +05:30
Keith Hall 9bf344f760 Merge pull request #3517 from akirk/strip-overstriking
Improve native man pages and command help syntax highlighting by stripping overstriking
2025-12-11 08:36:18 +02:00
Alex Kirk 4db185834b Update changelog 2025-12-11 06:12:44 +01:00
Alex Kirk 4549ab3f22 Move the strip_overstrike check 2025-12-11 06:06:07 +01:00
Alex Kirk 629a8968fc Remove unnecessary code change 2025-12-11 05:54:30 +01:00
Alex Kirk de414ed631 Limit overstrike stripping to man pages and help 2025-12-11 05:49:46 +01:00
Alex Kirk 59c5896902 Simplify MANPAGER guide 2025-12-11 05:49:46 +01:00
Alex Kirk b22fc5db6b Add show-all integration test 2025-12-11 05:49:46 +01:00
Alex Kirk f97f9ebf03 Remove double backspace scan 2025-12-11 05:49:46 +01:00
Alex Kirk 64a4b204a2 Only strip overstrike when a syntax highlighting theme is used 2025-12-11 05:49:46 +01:00
Alex Kirk 5fb8a25c30 Performance optimization 2025-12-11 05:49:46 +01:00
Alex Kirk 80ef6832d3 Add a changelog entry 2025-12-11 05:49:46 +01:00
Alex Kirk 51bdaa5f88 Strip overstriking to better support man pages 2025-12-11 05:49:46 +01:00
Keith Hall eb2a8e29c7 Merge pull request #3524 from sharkdp/help_custom_assets
Support custom assets (themes) when displaying help output
2025-12-10 22:39:51 +02:00
Keith Hall b7dd88eafd Support custom assets (themes) when displaying help output 2025-12-10 22:30:01 +02:00
Keith Hall 79fe5fea63 Merge pull request #3521 from sharkdp/rainbow_csv_strings
[CSV] don't apply string highlighting by default
2025-12-08 20:08:04 +02:00
Keith Hall d15e399fce [CSV] don't apply string highlighting by default 2025-12-07 21:55:05 +02:00
Keith Hall 4d188e45fe Merge pull request #3519 from sorairolake/change-zig-url
chore: Change URL of Zig submodule from GitHub to Codeberg
2025-12-06 13:18:58 +02:00
Shun Sakai abce6ac0d1 chore: Change URL of Zig submodule 2025-12-06 12:00:48 +09:00
Keith Hall 566595f0eb Merge pull request #3516 from sharkdp/fix_help_builtin_pager
Fix --help --pager=builtin
2025-12-05 22:22:22 +02:00
Keith Hall 1796324531 Fix --help --pager=builtin
Previously it still used less
2025-12-04 21:53:45 +02:00
David Peter 2371077352 Update sponsors section (#3514)
Removed Graphite sponsorship information from README.
2025-12-04 09:29:18 +01:00
Keith Hall 7e7ab8a860 Merge pull request #3511 from sharkdp/prepare_changelog
add empty changelog sections ready for future changes
2025-12-03 22:48:16 +02:00
Keith Hall c054541776 add empty changelog sections ready for future changes 2025-12-03 22:37:26 +02:00
auto-merge-dependabot-prs[bot] d6ffe3a9be Merge pull request #3503 from sharkdp/dependabot/cargo/execute-0.2.15
build(deps): bump execute from 0.2.13 to 0.2.15
2025-12-02 22:34:27 +00:00
dependabot[bot] 4a1ceb3ce0 build(deps): bump execute from 0.2.13 to 0.2.15
Bumps [execute](https://github.com/magiclen/execute) from 0.2.13 to 0.2.15.
- [Commits](https://github.com/magiclen/execute/compare/v0.2.13...v0.2.15)

---
updated-dependencies:
- dependency-name: execute
  dependency-version: 0.2.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 22:24:11 +00:00
auto-merge-dependabot-prs[bot] 45a6ee95d5 Merge pull request #3505 from sharkdp/dependabot/cargo/toml-0.9.8
build(deps): bump toml from 0.9.1 to 0.9.8
2025-12-02 22:21:11 +00:00
dependabot[bot] 3eeaf29675 build(deps): bump toml from 0.9.1 to 0.9.8
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.1 to 0.9.8.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.1...toml-v0.9.8)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 22:06:24 +00:00
auto-merge-dependabot-prs[bot] 58d1fa83e9 Merge pull request #3504 from sharkdp/dependabot/cargo/proc-macro2-1.0.103
build(deps): bump proc-macro2 from 1.0.95 to 1.0.103
2025-12-02 22:04:00 +00:00
dependabot[bot] 9d4ca3bc22 build(deps): bump proc-macro2 from 1.0.95 to 1.0.103
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.95 to 1.0.103.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.95...1.0.103)

---
updated-dependencies:
- dependency-name: proc-macro2
  dependency-version: 1.0.103
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 21:48:06 +00:00
auto-merge-dependabot-prs[bot] 9b23b05841 Merge pull request #3502 from sharkdp/dependabot/cargo/console-0.16.1
build(deps): bump console from 0.16.0 to 0.16.1
2025-12-02 21:45:41 +00:00
dependabot[bot] f3e8ffc276 build(deps): bump console from 0.16.0 to 0.16.1
Bumps [console](https://github.com/console-rs/console) from 0.16.0 to 0.16.1.
- [Release notes](https://github.com/console-rs/console/releases)
- [Changelog](https://github.com/console-rs/console/blob/main/CHANGELOG.md)
- [Commits](https://github.com/console-rs/console/compare/0.16.0...0.16.1)

---
updated-dependencies:
- dependency-name: console
  dependency-version: 0.16.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 21:21:35 +00:00
auto-merge-dependabot-prs[bot] 9b73911f2c Merge pull request #3501 from sharkdp/dependabot/cargo/grep-cli-0.1.12
build(deps): bump grep-cli from 0.1.11 to 0.1.12
2025-12-02 21:19:00 +00:00
dependabot[bot] 25bfa9fa81 build(deps): bump grep-cli from 0.1.11 to 0.1.12
Bumps [grep-cli](https://github.com/BurntSushi/ripgrep) from 0.1.11 to 0.1.12.
- [Release notes](https://github.com/BurntSushi/ripgrep/releases)
- [Changelog](https://github.com/BurntSushi/ripgrep/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BurntSushi/ripgrep/compare/grep-cli-0.1.11...grep-cli-0.1.12)

---
updated-dependencies:
- dependency-name: grep-cli
  dependency-version: 0.1.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 20:47:04 +00:00
104 changed files with 6097 additions and 1734 deletions
+11
View File
@@ -0,0 +1,11 @@
# On Windows MSVC, statically link the C runtime so that the resulting EXE does
# not depend on the vcruntime DLL.
#
# See: https://github.com/sharkdp/bat/issues/3634
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
[target.i686-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
[target.aarch64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
+1
View File
@@ -0,0 +1 @@
use flake
+13 -15
View File
@@ -35,7 +35,7 @@ jobs:
name: Extract crate metadata
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Extract crate information
id: crate_metadata
run: |
@@ -58,7 +58,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt,clippy
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- run: cargo fmt -- --check
- run: cargo clippy --locked --all-targets --all-features -- -D warnings
@@ -68,7 +68,7 @@ jobs:
needs: crate_metadata
steps:
- name: Checkout source code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install rust toolchain (v${{ needs.crate_metadata.outputs.msrv }})
uses: dtolnay/rust-toolchain@master
with:
@@ -80,7 +80,7 @@ jobs:
name: License checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: true # we especially want to perform license checks on submodules
- run: tests/scripts/license-checks.sh
@@ -90,7 +90,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Git checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: true # we need all syntax and theme submodules
- name: Install Rust toolchain
@@ -119,7 +119,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Git checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Prepare environment variables
run: |
echo "BAT_SYSTEM_CONFIG_PREFIX=$GITHUB_WORKSPACE/tests/examples/system_config" >> $GITHUB_ENV
@@ -135,7 +135,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Git checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Check documentation
@@ -150,7 +150,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- run: cargo install cargo-audit --locked
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- run: cargo audit
build:
@@ -161,12 +161,12 @@ jobs:
fail-fast: false
matrix:
job:
- { target: aarch64-unknown-linux-musl , os: ubuntu-latest , dpkg_arch: arm64, use-cross: true }
- { target: aarch64-unknown-linux-musl , os: ubuntu-latest , dpkg_arch: musl-linux-arm64, use-cross: true }
- { target: aarch64-unknown-linux-gnu , os: ubuntu-latest , dpkg_arch: arm64, use-cross: true }
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-latest , dpkg_arch: armhf, use-cross: true }
- { target: arm-unknown-linux-musleabihf, os: ubuntu-latest , dpkg_arch: musl-linux-armhf, use-cross: true }
- { target: i686-pc-windows-msvc , os: windows-2025 , }
- { target: i686-unknown-linux-gnu , os: ubuntu-latest , dpkg_arch: i686, use-cross: true }
- { target: i686-unknown-linux-gnu , os: ubuntu-latest , dpkg_arch: i386, use-cross: true }
- { target: i686-unknown-linux-musl , os: ubuntu-latest , dpkg_arch: musl-linux-i686, use-cross: true }
- { target: x86_64-apple-darwin , os: macos-15-intel, }
- { target: aarch64-apple-darwin , os: macos-latest , }
@@ -178,7 +178,7 @@ jobs:
BUILD_CMD: cargo
steps:
- name: Checkout source code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install prerequisites
shell: bash
@@ -195,9 +195,7 @@ jobs:
- name: Install cross
if: matrix.job.use-cross
uses: taiki-e/install-action@v2
with:
tool: cross
run: cargo install cross --git https://github.com/cross-rs/cross --rev 588b3c99db52b5a9c5906fab96cfadcf1bde7863
- name: Overwrite build command env variable
if: matrix.job.use-cross
@@ -335,7 +333,7 @@ jobs:
DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}
DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }}-musl
case ${{ matrix.job.target }} in *-musl) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac;
case ${{ matrix.job.target }} in *-musl*) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac;
DPKG_VERSION=${{ needs.crate_metadata.outputs.version }}
DPKG_ARCH="${{ matrix.job.dpkg_arch }}"
DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb"
@@ -13,7 +13,7 @@ jobs:
PR_NUMBER: ${{ github.event.number }}
PR_BASE: ${{ github.base_ref }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Fetch PR base
run: git fetch --no-tags --prune --depth=1 origin
+7
View File
@@ -1,6 +1,11 @@
.direnv/
/target/
**/*.rs.bk
# Editors
.idea/
.vscode/
# Generated files
/assets/completions/_bat.ps1
/assets/completions/bat.bash
@@ -8,3 +13,5 @@
/assets/completions/bat.zsh
/assets/manual/bat.1
/assets/metadata.yaml
+11 -8
View File
@@ -49,9 +49,6 @@
[submodule "assets/themes/zenburn"]
path = assets/themes/zenburn
url = https://github.com/colinta/zenburn.git
[submodule "assets/syntaxes/Kotlin"]
path = assets/syntaxes/02_Extra/Kotlin
url = https://github.com/vkostyukov/kotlin-sublime-package
[submodule "assets/syntaxes/Elm"]
path = assets/syntaxes/02_Extra/Elm
url = https://github.com/elm-community/SublimeElmLanguageSupport
@@ -130,7 +127,7 @@
url = https://github.com/djuretic/SublimeStrace
[submodule "assets/syntaxes/Jinja2"]
path = assets/syntaxes/02_Extra/Jinja2
url = https://github.com/Martin819/sublime-jinja2
url = https://github.com/ltrzesniewski/sublime-jinja2.git
[submodule "assets/syntaxes/SLS"]
path = assets/syntaxes/02_Extra/SLS
url = https://github.com/saltstack/sublime-text
@@ -196,16 +193,13 @@
url = https://github.com/leanprover/vscode-lean4.git
[submodule "assets/syntaxes/02_Extra/Zig"]
path = assets/syntaxes/02_Extra/Zig
url = https://github.com/ziglang/sublime-zig-language.git
url = https://codeberg.org/ziglang/sublime-zig-language.git
[submodule "assets/syntaxes/02_Extra/gnuplot"]
path = assets/syntaxes/02_Extra/gnuplot
url = https://github.com/hesstobi/sublime_gnuplot
[submodule "assets/syntaxes/02_Extra/SystemVerilog"]
path = assets/syntaxes/02_Extra/SystemVerilog
url = https://github.com/TheClams/SystemVerilog.git
[submodule "assets/themes/visual-studio-dark-plus"]
path = assets/themes/visual-studio-dark-plus
url = https://github.com/vidann1/visual-studio-dark-plus.git
[submodule "assets/syntaxes/02_Extra/SublimeEthereum"]
path = assets/syntaxes/02_Extra/SublimeEthereum
url = https://github.com/davidhq/SublimeEthereum.git
@@ -281,3 +275,12 @@
[submodule "assets/syntaxes/02_Extra/Gomod"]
path = assets/syntaxes/02_Extra/Gomod
url = https://github.com/mitranim/sublime-gomod
[submodule "assets/syntaxes/02_Extra/COBOL"]
path = assets/syntaxes/02_Extra/COBOL
url = https://github.com/adukhan99/sublime_cobol.git
[submodule "assets/syntaxes/02_Extra/Kotlin"]
path = assets/syntaxes/02_Extra/Kotlin
url = https://github.com/guille/sublime-kotlin
[submodule "assets/syntaxes/02_Extra/Caddy"]
path = assets/syntaxes/02_Extra/Caddy
url = https://github.com/caddyserver/sublimetext.git
+86
View File
@@ -1,3 +1,89 @@
# unreleased
## Other
- Update Cargo dependencies to resolve current RustSec advisories, see #3861 (@TyceHerrman)
- Add instructions for removing fish help abbreviations to README, see #3655 (@claw-explorer). Closes #3536
- Add .NET slnx extension, see #3682 (@ltrzesniewski)
## Features
- Add a `--sanitize=<auto|always|never>` flag for safe display of untrusted input. It implies `--strip-ansi` at the same value and additionally substitutes terminal-active control bytes (cursor moves, charset switches, beep, etc.) and Unicode bidi / zero-width formatting characters with the Unicode replacement character (U+FFFD). Mitigates Trojan-Source-style spoofing (CVE-2021-42574). See #3729 (@curious-rabbit)
- Map justfile, Justfile, .justfile, and *.justfile to Makefile syntax highlighting, see #3623 (@zachvalenta)
- Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn)
- Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355)
- Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey)
- Support configuring `--terminal-width` via `BAT_WIDTH`, see #3679 (@officialasishkumar)
- Implement `--unbuffered` mode for streaming input, allowing partial lines to display immediately (e.g. `tail -f | bat -u`). Closes #3555, see #3583 (@mainnebula)
- Added an initial `flake.nix` for a ready made development environment; see #3578 (@vorburger)
- Add `--quiet-empty` (`-E`) flag to suppress output when input is empty. Closes #1936, see #3563 (@NORMAL-EX)
- Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk)
- Add `--fallback-syntax`/`--fallback-language` to apply syntax highlighting only when auto-detection fails, see #1341 (@Xavrir)
- Map `BUILD` case sensitively to Python (Starlark) for Bazel, see #3576 (@vorburger)
- Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres)
## Bugfixes
- Fix `--list-languages` respecting `--paging=never`, see #3828 (@cyphercodes)
- Fix `--sanitize` passing through the bidi control characters U+200E, U+200F and U+061C, see #3862 (@lenamonj)
- `--strip-ansi`: also strip 8-bit C1 introducers (U+0090, U+0098, U+009B, U+009D, U+009E, U+009F) and DCS/SOS/PM/APC sequence bodies, which previously passed through. See #3729 (@curious-rabbit)
- Fix `--ignored-suffix` not falling back to first-line/shebang detection when the ignored suffix is also a registered extension (e.g. `--ignored-suffix .txt` on a shebang script), see #2745 and #3816 (@adnrivera)
- Fix `capacity overflow` panic when printing a snip separator at `--terminal-width=1` with multiple line ranges. Closes #3803, see #3804 (@leeewee)
- Pass `--no-paging` to `bat` invocations inside the bash / zsh / fish / PowerShell shell completion scripts so that shell-level pager wiring (e.g. `LESSOPEN='|-bat -f -pp %s'`) cannot inject ANSI escape sequences into the completion candidates. Closes #3760 (@mvanhorn)
- Quote filenames before substituting them into `$LESSOPEN` / `$LESSCLOSE` templates, preventing shell injection when a filename contains shell metacharacters, see #3726 (@curious-rabbit)
- Fix `--list-themes` unconditionally probing the terminal via OSC 10/11 even when `--theme` was set to an explicit value, see #3700 (regression introduced in bc42149a). (@optimistiCli)
- Fix inverted `$LESSCLOSE` warning so bat warns on nonzero exit, not on success. See #3654 (@cuiweixie)
- Sanitize control characters in filenames before displaying them in the file header, error messages, and the terminal title, preventing ANSI escape injection via crafted filenames. Closes #3054, see #3691 (@curious-rabbit)
- Report initial input read errors instead of treating them as empty input. Closes #3002, see #3706 (@lawrence3699)
- Treat ZIP archives as binary content based on their magic header, see #3686 (@officialasishkumar)
- Fix i686 `.deb` package using incorrect architecture name (`i686` instead of `i386`), preventing installation on Debian. Closes #3611, see #3650 (@Sim-hu)
- Fix inconsistent `.deb` MUSL package names (aarch64-musl used `arm64` instead of `musl-linux-arm64`, and `musleabihf` target missed `bat-musl` prefix). Closes #3482, see #3642 (@mvanhorn)
- Fix incorrect text width computation when using `--binary=as-text` with non-printable characters in caret notation, see #3640 and #3631 (@eyupcanakman)
- Fix `BAT_CONFIG_DIR` pointing at system config directory causing duplicate flag errors. Closes #3589, see #3620 (@Xavrir)
- Fix syntax highlighting for symlinked files when the symlink name has no extension but the target does. Closes #1001, see #3621 (@Xavrir)
- Report error when pager is missing instead of silently falling back, see #3588 (@IMaloney)
- Fix `--wrap=never` and `-S` flags being ignored when piping to pager, see #3592 (@IMaloney)
- Fix crash with BusyBox `less` on Windows, see #3527 (@Anchal-T)
- Fix `bat cache --help` failing with 'unexpected argument' error, see #3580 and #3560 (@NORMAL-EX)
- `--help` now correctly honors `--pager=builtin`. See #3516 (@keith-hall)
- `--help` now correctly honors custom themes. See #3524 (@keith-hall)
- Fixed test compatibility with future Cargo build directory changes, see #3550 (@nmacl)
- Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l)
- Fixed syntax tests path, see #3610 (@foxfromworld)
- Fix zsh tab completion word-splitting language names containing spaces (e.g. `HTML (Jinja2)`, `Apache Conf`), see #3693 (@YoshKoz)
- Fix zsh tab completion offering invalid `-l` arguments (file globs, paths, hidden filenames) sourced from the second column of `--list-languages`. Closes #3735, see #3737 (@truffle-dev)
- Fix `usize` underflow in `--list-languages` when `--terminal-width` is smaller than the longest language name, see #3812 (@greymoth-jp)
## Other
- Use git version of cross. See #3533 (@OctopusET)
- Bump MSRV to 1.88, update `time` crate to 0.3.47 to fix RUSTSEC-2026-0009, see #3581 (@NORMAL-EX)
- Allow home and end keys to be used with builtin pager, see #3651 (@keith-hall)
- Builtin syntax mapping: cleanup matcher glob parsing logic #3652 (@cyqsimon)
- Statically link the CRT for MSVC builds via Cargo config to avoid runtime DLL dependencies. Closes #3634, see #3692 (@barry3406)
- Replace `libgit2` with a pure Rust implementation of git called `gitoxide`, see PR #3703 (@blinxen)
## Syntaxes
- Add shebang-based detection for Tcl (`tclsh`, `wish`) and Expect (`expect`) scripts, see #3647 (@mvanhorn)
- Change the URL of Zig submodule from GitHub to Codeberg, see #3519 (@sorairolake)
- Don't color strings inside CSV files, to make it easier to tell which column they belong to, see #3521 (@keith-hall)
- Add syntax highlighting support for COBOL, see #3584 (@adukhan99)
- Fixed manpage syntax so that ANSI escape codes don't get incorrectly highlighted and thus broken, see #3586 (@BlueElectivire)
- Map several Google Cloud CLI config files to their appropriate syntax #3635 (@victor-gp)
- Map all ignore dotfiles to Git Ignore syntax #3636 (@victor-gp)
- Improved Kotlin syntax, see #3699 (@guille)
- Include subdirectories in SSH Config syntax mapping, see #3758 (@injust)
- Add Ghostty syntax mapping, see #3759 (@injust)
- Add syntax highlighting for `Caddyfile` #3789 (@CosmicHorrorDev)
- Include `.code-workspace` as a JSON extension #3809 (@dhruvkb)
- Add syntax mapping for DNF repo configuration files, see #3814 (@injust)
## Themes
- Remove the Visual Studio Dark+ theme, see #3552 (@CosmicHorrorDev)
## `bat` as a library
# v0.26.1
## Features
+28
View File
@@ -68,6 +68,8 @@ users, please read the corresponding
[documentation](https://github.com/sharkdp/bat/blob/master/doc/assets.md)
first.
To map a file name pattern to an existing syntax, read [the documentation here](https://github.com/sharkdp/bat/blob/master/src/syntax_mapping/builtins/README.md).
Note: We are currently not accepting new default themes.
@@ -91,3 +93,29 @@ To learn how to write regression tests for theme and syntax changes, read the
[Syntax
tests](https://github.com/sharkdp/bat/blob/master/doc/assets.md#syntax-tests)
section in `assets.md`.
### Ensuring bat is available for Syntax tests
The syntax test script (`tests/syntax-tests/update.sh`) calls `bat` from your PATH and regenerates the highlighted output files under
`tests/syntax-tests/highlighted/`. These files are used to verify that syntax highlighting works as expected.
- If you only built the binaries with:
```bash
cargo build --bins
```
you need to add the debug build to your PATH from the bat project root before running the tests.
See also step 5 in [Syntax
tests](https://github.com/sharkdp/bat/blob/master/doc/assets.md#syntax-tests) for related instructions.
```bash
export PATH="$PATH:$(pwd)/target/debug"
```
Otherwise, you will see:
```bash
Error: Could not execute 'bat'. Please make sure that the executable is available on the PATH.
```
- If you installed bat with:
```bash
cargo install --path . --locked
```
then bat will be available in ~/.cargo/bin (usually already in PATH), and the tests will run without issues.
Generated
+1613 -769
View File
File diff suppressed because it is too large Load Diff
+29 -29
View File
@@ -11,7 +11,7 @@ exclude = ["assets/syntaxes/*", "assets/themes/*"]
build = "build/main.rs"
edition = '2021'
# You are free to bump MSRV as soon as a reason for bumping emerges.
rust-version = "1.87"
rust-version = "1.88"
[features]
default = ["application", "git"]
@@ -31,8 +31,7 @@ minimal-application = [
"regex-onig",
"wild",
]
git = ["git2"] # Support indicating git modifications
vendored-libgit2 = ["git2/vendored-libgit2"]
git = ["gix"] # Support indicating git modifications
paging = [ "shell-words", "grep-cli", "minus"] # Support applying a pager on the output
lessopen = ["execute"] # Support $LESSOPEN preprocessor
build-assets = ["syntect/yaml-load", "syntect/plist-load", "regex", "walkdir"]
@@ -42,21 +41,21 @@ regex-onig = ["syntect/regex-onig"] # Use the "oniguruma" regex engine
regex-fancy = ["syntect/regex-fancy"] # Use the rust-only "fancy-regex" engine
[dependencies]
nu-ansi-term = "0.50.0"
nu-ansi-term = "0.50.3"
ansi_colours = "^1.2"
bincode = "1.0"
console = "0.16.0"
console = "0.16.2"
flate2 = "1.1"
once_cell = "1.20"
thiserror = "2.0"
wild = { version = "2.2", optional = true }
content_inspector = "0.2.4"
shell-words = { version = "1.1.0", optional = true }
minus = { version = "5.6", optional = true, features = [
shell-words = { version = "1.1.1", optional = true }
minus = { version = "5.7", optional = true, features = [
"dynamic_output",
"search",
] }
unicode-width = "0.2.1"
unicode-width = "0.2.2"
globset = "0.4"
serde = "1.0"
serde_derive = "1.0"
@@ -64,22 +63,23 @@ serde_yaml = "0.9.28"
semver = "1.0"
path_abs = { version = "0.5", default-features = false }
clircle = { version = "0.6.1", default-features = false }
bugreport = { version = "0.5.0", optional = true }
bugreport = { version = "0.6.0", optional = true }
etcetera = { version = "0.11.0", optional = true }
grep-cli = { version = "0.1.11", optional = true }
regex = { version = "1.12.2", optional = true }
grep-cli = { version = "0.1.12", optional = true }
regex = { version = "1.12.3", optional = true }
walkdir = { version = "2.5", optional = true }
bytesize = { version = "1.3.0" }
bytesize = { version = "2.3.1" }
encoding_rs = "0.8.35"
execute = { version = "0.2.13", optional = true }
execute = { version = "0.2.15", optional = true }
terminal-colorsaurus = "1.0"
unicode-segmentation = "1.12.0"
unicode-segmentation = "1.13.2"
itertools = "0.14.0"
[dependencies.git2]
version = "0.20"
[dependencies.gix]
version = "0.85"
optional = true
default-features = false
features = ["sha1", "blob-diff"]
[dependencies.syntect]
version = "5.3.0"
@@ -87,43 +87,43 @@ default-features = false
features = ["parsing"]
[dependencies.clap]
version = "4.5.46"
version = "4.6.1"
optional = true
features = ["wrap_help", "cargo"]
[target.'cfg(target_os = "macos")'.dependencies]
plist = "1.7.0"
plist = "1.9.0"
[dev-dependencies]
assert_cmd = "2.0.12"
assert_cmd = "2.0.16"
expect-test = "1.5.0"
serial_test = { version = "2.0.0", default-features = false }
predicates = "3.1.3"
predicates = "3.1.4"
wait-timeout = "0.2.1"
tempfile = "3.16.0"
tempfile = "3.27.0"
serde = { version = "1.0", features = ["derive"] }
[target.'cfg(unix)'.dev-dependencies]
nix = { version = "0.30", default-features = false, features = ["term"] }
nix = { version = "0.31", default-features = false, features = ["term"] }
[build-dependencies]
anyhow = "1.0.97"
indexmap = { version = "2.8.0", features = ["serde"] }
indexmap = { version = "2.14.0", features = ["serde"] }
itertools = "0.14.0"
once_cell = "1.20"
prettyplease = "0.2.37"
proc-macro2 = "1.0.95"
quote = "1.0.40"
regex = "1.12.2"
proc-macro2 = "1.0.106"
quote = "1.0.45"
regex = "1.12.3"
serde = "1.0"
serde_derive = "1.0"
serde_with = { version = "3.15.1", default-features = false, features = ["macros"] }
serde_with = { version = "3.17.0", default-features = false, features = ["macros"] }
syn = { version = "2.0.104", features = ["full"] }
toml = { version = "0.9.1", features = ["preserve_order"] }
toml = { version = "1.1.1", features = ["preserve_order"] }
walkdir = "2.5"
[build-dependencies.clap]
version = "4.5.46"
version = "4.6.1"
optional = true
features = ["wrap_help", "cargo"]
+44 -38
View File
@@ -19,26 +19,6 @@
[<a href="doc/README-ru.md">Русский</a>]
</p>
### Sponsors
A special *thank you* goes to our biggest <a href="doc/sponsors.md">sponsors</a>:<br>
<p>
<a href="https://www.warp.dev/bat">
<img src="doc/sponsors/warp-logo.png" width="200" alt="Warp">
<br>
<strong>Warp, the intelligent terminal</strong>
<br>
<sub>Available on MacOS, Linux, Windows</sub>
</a>
</p><p>
<a href="https://graphite.dev/?utm_source=github&utm_medium=repo&utm_campaign=bat">
<img src="doc/sponsors/graphite-logo.jpeg" width="200" alt="Graphite">
<br>
<strong>Graphite is the AI developer productivity platform helping<br>teams on GitHub ship higher quality software, faster</strong>
</a>
</p>
### Syntax highlighting
`bat` supports syntax highlighting for a large number of programming and markup
@@ -49,7 +29,7 @@ languages:
### Git integration
`bat` communicates with `git` to show modifications with respect to the index
(see left side bar):
(see left sidebar):
![Git integration example](https://i.imgur.com/2lSW4RE.png)
@@ -201,30 +181,19 @@ bat main.cpp | xclip
`MANPAGER` environment variable:
```bash
export MANPAGER="sh -c 'awk '\''{ gsub(/\x1B\[[0-9;]*m/, \"\", \$0); gsub(/.\x08/, \"\", \$0); print }'\'' | bat -p -lman'"
export MANPAGER="bat -plman"
man 2 select
```
(replace `bat` with `batcat` if you are on Debian or Ubuntu)
(on some older Debian or Ubuntu releases, the executable is named `batcat` instead of `bat`)
If you prefer to have this bundled in a new command, you can also use [`batman`](https://github.com/eth-p/bat-extras/blob/master/doc/batman.md).
> [!WARNING]
> This will [not work](https://github.com/sharkdp/bat/issues/1145) out of the box with Mandoc's `man` implementation.
>
> Please either use `batman`, or convert the shell script to a [shebang executable](https://en.wikipedia.org/wiki/Shebang_(Unix)) and point `MANPAGER` to that.
Note that the [Manpage syntax](assets/syntaxes/02_Extra/Manpage.sublime-syntax) is developed in this repository and still needs some work.
#### `prettier` / `shfmt` / `rustfmt`
The [`prettybat`](https://github.com/eth-p/bat-extras/blob/master/doc/prettybat.md) script is a wrapper that will format code and print it with `bat`.
#### `Warp`
<a href="https://app.warp.dev/drive/folder/-Bat-Warp-Pack-lxhe7HrEwgwpG17mvrFSz1">
<img src="doc/sponsors/warp-pack-header.png" alt="Warp">
</a>
#### Highlighting `--help` messages
You can use `bat` to colorize help text: `$ cp --help | bat -plhelp`
@@ -257,6 +226,14 @@ abbr -a --position anywhere -- -h '-h | bat -plhelp'
This way, you can keep on using `cp --help`, but get colorized help pages.
> [!TIP]
> To remove these abbreviations later, run:
> ```fish
> abbr -e -- --help
> abbr -e -- -h
> ```
> The `--` before the abbreviation name is required because `--help` and `-h` start with dashes, which would otherwise be interpreted as flags to `abbr` itself.
Be aware that in some cases, `-h` may not be a shorthand of `--help` (for example with `ls`). In cases where you need to use `-h`
as a command argument you can prepend `\` to the argument (eg. `ls \-h`) to escape the aliasing defined above.
@@ -287,8 +264,8 @@ If your Ubuntu/Debian installation is new enough you can simply run:
sudo apt install bat
```
**Important**: If you install `bat` this way, please note that the executable may be installed as `batcat` instead of `bat` (due to [a name
clash with another package](https://github.com/sharkdp/bat/issues/982)). You can set up a `bat -> batcat` symlink or alias to prevent any issues that may come up because of this and to be consistent with other distributions:
**Important**: On some older Ubuntu/Debian releases, the executable is installed as `batcat` instead of `bat` (due to [a name
clash with another package](https://github.com/sharkdp/bat/issues/982)). On newer releases, the executable is available as `bat`. If `bat --version` does not work after installation, try `batcat --version` instead. You can set up a `bat -> batcat` symlink or alias to prevent any issues that may come up because of this and to be consistent with other distributions:
``` bash
mkdir -p ~/.local/bin
ln -s /usr/bin/batcat ~/.local/bin/bat
@@ -330,7 +307,7 @@ pacman -S bat
### On Fedora
You can install [the `bat` package](https://koji.fedoraproject.org/koji/packageinfo?packageID=27506) from the official [Fedora Modular](https://docs.fedoraproject.org/en-US/modularity/using-modules/) repository.
You can install [the `bat` package](https://packages.fedoraproject.org/pkgs/rust-bat/bat/) from the official sources:
```bash
dnf install bat
@@ -530,6 +507,30 @@ 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](#configuration-file).
By default, `bat` enables `changes`, `grid`, `header-filename`, `numbers`, and `snip`.
The available pre-defined styles are:
| Style | Description |
|-------|-------------|
| `default` | Enables the recommended style components listed above. |
| `full` | Enables all available components. |
| `auto` | Same as `default`, unless the output is piped. |
| `plain` | Disables all available components. |
The available individual components are:
| Component | Description |
|-----------|-------------|
| `changes` | Show Git modification markers. |
| `header` | Alias for `header-filename`. |
| `header-filename` | Show filenames before the content. |
| `header-filesize` | Show file sizes before the content. |
| `grid` | Vertical/horizontal lines to separate the side bar and header from the content. |
| `rule` | Horizontal lines to delimit files. |
| `numbers` | Show line numbers in the side bar. |
| `snip` | Draw separation lines between distinct line ranges. |
>[!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.
@@ -592,7 +593,8 @@ syntax:
This works very similar to how we add new syntax definitions.
> [!NOTE]
> Themes are stored in [`.tmTheme` files](https://www.sublimetext.com/docs/color_schemes_tmtheme.html).
> Custom themes must be stored in [`.tmTheme` files](https://www.sublimetext.com/docs/color_schemes_tmtheme.html).
> Newer `.sublime-color-scheme` files are currently not supported.
First, create a folder with the new syntax highlighting themes:
```bash
@@ -841,6 +843,10 @@ If your version of `bat` supports the `--strip-ansi=auto` option, it can be used
before syntax highlighting. Alternatively, you may disable both syntax highlighting and wrapping by
passing the `--color=never --wrap=never` options to `bat`.
For untrusted input, the `--sanitize=auto|always|never` option additionally replaces terminal-active
control bytes and Unicode bidi / zero-width formatting characters with the Unicode replacement
character. It implies `--strip-ansi` at the same value.
> [!NOTE]
> The `auto` option of `--strip-ansi` avoids removing escape sequences when the syntax is plain text.
+12 -5
View File
@@ -9,17 +9,17 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
$ArrayCompletion = @('bash', 'fish', 'zsh', 'ps1')
$ArrayWhen = @('auto', 'never', 'always')
$ArrayYesNo = @('never', 'always')
$ArrayWrap = @('always', 'never', 'character')
$ArrayWrap = @('always', 'never', 'character', 'word')
$ArrayBinary = @('no-printing', 'as-text')
$ArrayPrint = @('unicode', 'caret')
function Get-MyThemes(){
$themes = {{PROJECT_EXECUTABLE}} --list-themes | ForEach-Object {$_ -replace "^(.*)$", '''$1'''} | select-object
$themes = {{PROJECT_EXECUTABLE}} --no-paging --list-themes | ForEach-Object {$_ -replace "^(.*)$", '''$1'''} | select-object
return $themes
}
function Get-MyLanguages(){
$themes = {{PROJECT_EXECUTABLE}} --list-languages | ForEach-Object{[pscustomobject]@{MyParameter=$_.Substring(0,$_.IndexOf(":")).Trim();MyDescription=$_.Substring($_.IndexOf(":")+1)}} | select-object
$themes = {{PROJECT_EXECUTABLE}} --no-paging --list-languages | ForEach-Object{[pscustomobject]@{MyParameter=$_.Substring(0,$_.IndexOf(":")).Trim();MyDescription=$_.Substring($_.IndexOf(":")+1)}} | select-object
return $themes
}
@@ -100,6 +100,11 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)}
break
}
'*;--sanitize' {
$ArrayWhen |
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)}
break
}
'*;--strip-ansi' {
$ArrayWhen |
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)}
@@ -135,7 +140,7 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
[CompletionResult]::new('--file-name' , 'file-name' , [CompletionResultType]::ParameterName, 'Specify the name to display for a file.')
[CompletionResult]::new('--diff-context' , 'diff-context' , [CompletionResultType]::ParameterName, 'diff-context')
[CompletionResult]::new('--tabs' , 'tabs' , [CompletionResultType]::ParameterName, 'Set the tab width to T spaces.')
[CompletionResult]::new('--wrap' , 'wrap' , [CompletionResultType]::ParameterName, 'Specify the text-wrapping mode (*auto*, character).')
[CompletionResult]::new('--wrap' , 'wrap' , [CompletionResultType]::ParameterName, 'Specify the text-wrapping mode (*auto*, never, character, word).')
[CompletionResult]::new('--terminal-width' , 'terminal-width' , [CompletionResultType]::ParameterName, 'Explicitly set the width of the terminal instead of determining it automatically. If prefixed with ''+'' or ''-'', the value will be treated as an offset to the actual terminal width. See also: ''--wrap''.')
[CompletionResult]::new('--color' , 'color' , [CompletionResultType]::ParameterName, 'When to use colors (*auto*, never, always).')
[CompletionResult]::new('--italic-text' , 'italic-text' , [CompletionResultType]::ParameterName, 'Use italics in output (always, *never*)')
@@ -158,6 +163,7 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
[CompletionResult]::new('--ignored-suffix' , 'ignored-suffix' , [CompletionResultType]::ParameterName, 'Ignore extension. For example: ''bat --ignored-suffix ".dev" my_file.json.dev'' will use JSON syntax, and ignore ''.dev''')
[CompletionResult]::new('--squeeze-blank' , 'squeeze-blank' , [CompletionResultType]::ParameterName, 'Squeeze consecutive empty lines into a single empty line.')
[CompletionResult]::new('--squeeze-limit' , 'squeeze-limit' , [CompletionResultType]::ParameterName, 'Set the maximum number of consecutive empty lines to be printed.')
[CompletionResult]::new('--sanitize' , 'sanitize' , [CompletionResultType]::ParameterName, 'Specify when to sanitize untrusted input for safe display. Implies --strip-ansi and also replaces terminal-active and bidi / zero-width bytes. (auto, always, *never*).')
[CompletionResult]::new('--strip-ansi' , 'strip-ansi' , [CompletionResultType]::ParameterName, 'Specify when to strip ANSI escape sequences from the input. The automatic mode will remove escape sequences unless the syntax highlighting language is plain text. (auto, always, *never*).')
# [CompletionResult]::new('-p' , 'p' , [CompletionResultType]::ParameterName, 'Show plain style (alias for ''--style=plain'').')
[CompletionResult]::new('--plain' , 'plain' , [CompletionResultType]::ParameterName, 'Show plain style (alias for ''--style=plain'').')
@@ -173,7 +179,7 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
# [CompletionResult]::new('-L' , 'L' , [CompletionResultType]::ParameterName, 'Display all supported languages.')
[CompletionResult]::new('--list-languages' , 'list-languages' , [CompletionResultType]::ParameterName, 'Display all supported languages.')
# [CompletionResult]::new('-u' , 'u' , [CompletionResultType]::ParameterName, 'u')
[CompletionResult]::new('--unbuffered' , 'unbuffered' , [CompletionResultType]::ParameterName, 'unbuffered')
[CompletionResult]::new('--unbuffered' , 'unbuffered' , [CompletionResultType]::ParameterName, 'Enable unbuffered input reading for streaming use cases')
[CompletionResult]::new('--completion' , 'completion' , [CompletionResultType]::ParameterName, 'Show shell completion for a certain shell. [possible values: bash, fish, zsh, ps1]')
[CompletionResult]::new('--no-config' , 'no-config' , [CompletionResultType]::ParameterName, 'Do not use the configuration file')
[CompletionResult]::new('--no-custom-assets' , 'no-custom-assets' , [CompletionResultType]::ParameterName, 'Do not load custom assets')
@@ -186,6 +192,7 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
[CompletionResult]::new('--acknowledgements' , 'acknowledgements' , [CompletionResultType]::ParameterName, 'Show acknowledgements.')
[CompletionResult]::new('--set-terminal-title' , 'set-terminal-title' , [CompletionResultType]::ParameterName, 'Sets terminal title to filenames when using a pager.')
[CompletionResult]::new('--diagnostic' , 'diagnostic' , [CompletionResultType]::ParameterName, 'Show diagnostic information for bug reports.')
[CompletionResult]::new('--quiet-empty' , 'quiet-empty' , [CompletionResultType]::ParameterName, 'Do not produce any output when the input is empty.')
# [CompletionResult]::new('-h' , 'h' , [CompletionResultType]::ParameterName, 'Print this help message.')
[CompletionResult]::new('--help' , 'help' , [CompletionResultType]::ParameterName, 'Print this help message.')
# [CompletionResult]::new('-V' , 'V' , [CompletionResultType]::ParameterName, 'Show version information.')
+12 -5
View File
@@ -80,7 +80,7 @@ _bat() {
-l | --language)
local IFS=$'\n'
COMPREPLY=($(compgen -W "$(
"$1" --list-languages | while IFS=: read -r lang _; do
"$1" --no-paging --list-languages | while IFS=: read -r lang _; do
printf "%s\n" "$lang"
done
)" -- "$cur"))
@@ -100,6 +100,7 @@ _bat() {
--lessopen | \
--no-paging | \
--diagnostic | \
--quiet-empty | \
--acknowledgements | \
-h | --help | \
-V | --version | \
@@ -116,7 +117,7 @@ _bat() {
return 0
;;
--wrap)
COMPREPLY=($(compgen -W "auto never character" -- "$cur"))
COMPREPLY=($(compgen -W "auto never character word" -- "$cur"))
return 0
;;
--binary)
@@ -131,6 +132,10 @@ _bat() {
COMPREPLY=($(compgen -W "auto never always" -- "$cur"))
return 0
;;
--sanitize)
COMPREPLY=($(compgen -W "auto never always" -- "$cur"))
return 0
;;
--completion)
COMPREPLY=($(compgen -W "bash fish zsh ps1" -- "$cur"))
return 0
@@ -149,14 +154,14 @@ _bat() {
;;
--theme)
local IFS=$'\n'
COMPREPLY=($(compgen -W "auto${IFS}auto:always${IFS}auto:system${IFS}dark${IFS}light${IFS}$("$1" --list-themes)" -- "$cur"))
COMPREPLY=($(compgen -W "auto${IFS}auto:always${IFS}auto:system${IFS}dark${IFS}light${IFS}$("$1" --no-paging --list-themes)" -- "$cur"))
__bat_escape_completions
return 0
;;
--theme-dark | \
--theme-light)
local IFS=$'\n'
COMPREPLY=($(compgen -W "$("$1" --list-themes)" -- "$cur"))
COMPREPLY=($(compgen -W "$("$1" --no-paging --list-themes)" -- "$cur"))
__bat_escape_completions
return 0
;;
@@ -189,8 +194,8 @@ _bat() {
$split && return 0
if [[ $cur == -* ]]; then
# --unbuffered excluded intentionally (no-op)
COMPREPLY=($(compgen -W "
--unbuffered
--show-all
--nonprintable-notation
--binary
@@ -220,6 +225,7 @@ _bat() {
--list-themes
--squeeze-blank
--squeeze-limit
--sanitize
--strip-ansi
--style
--line-range
@@ -227,6 +233,7 @@ _bat() {
--lessopen
--completion
--diagnostic
--quiet-empty
--acknowledgements
--set-terminal-title
--help
+10 -7
View File
@@ -15,11 +15,11 @@ function __bat_complete_files -a token
end
function __bat_complete_one_language -a comp
command $bat --list-languages | string split -f1 : | string match -e "$comp"
command $bat --no-paging --list-languages | string split -f1 : | string match -e "$comp"
end
function __bat_complete_list_languages
for spec in (command $bat --list-languages)
for spec in (command $bat --no-paging --list-languages)
set -l name (string split -f1 : $spec)
for ext in (string split -f2 : $spec | string split ,)
test -n "$ext"; or continue
@@ -61,7 +61,7 @@ function __bat_no_excl_args
-s V -l version \
-l acknowledgements \
-l config-dir -l config-file \
-l diagnostic \
-l diagnostic -l quiet-empty \
-l list-languages -l list-themes
end
@@ -118,6 +118,7 @@ set -l wrap_opts '
auto\tdefault
never\t
character\t
word\t
'
# While --tabs theoretically takes any number, most people should be OK with these.
@@ -159,6 +160,7 @@ complete -c $bat -l config-file -f -d "Display location of configuration file" -
complete -c $bat -l decorations -x -a "$decorations_opts" -d "When to use --style decorations" -n __bat_no_excl_args
complete -c $bat -l diagnostic -d "Print diagnostic info for bug reports" -n __fish_is_first_arg
complete -c $bat -l quiet-empty -d "Do not produce any output when the input is empty" -n __fish_is_first_arg
complete -c $bat -s d -l diff -d "Only show lines with Git changes" -n __bat_no_excl_args
@@ -219,6 +221,7 @@ complete -c $bat -s s -l squeeze-blank -d "Squeeze consecutive empty lines into
complete -c $bat -l squeeze-limit -x -d "Set the maximum number of consecutive empty lines to be printed" -n __bat_no_excl_args
complete -c $bat -l strip-ansi -x -a "auto never always" -d "Specify when to strip ANSI escape sequences from the input" -n __bat_no_excl_args
complete -c $bat -l sanitize -x -a "auto never always" -d "Specify when to sanitize untrusted input for safe display" -n __bat_no_excl_args
complete -c $bat -s p -l plain -d "Disable decorations" -n __bat_no_excl_args
@@ -232,13 +235,13 @@ 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 <width>, +<offset>, or -<offset>" -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 -x -a "$special_themes(command $bat --no-paging --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-dark -x -a "(command $bat --no-paging --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 -l theme-light -x -a "(command $bat --no-paging --list-themes | command cat)" -d "Set the syntax highlighting theme for light backgrounds" -n __bat_no_excl_args
complete -c $bat -s u -l unbuffered -d "This option exists for POSIX-compliance reasons" -n __bat_no_excl_args
complete -c $bat -s u -l unbuffered -d "Enable unbuffered input reading for streaming use cases" -n __bat_no_excl_args
complete -c $bat -s V -l version -f -d "Show version information" -n __fish_is_first_arg
+12 -5
View File
@@ -34,7 +34,7 @@ _{{PROJECT_EXECUTABLE}}_main() {
'(-d --diff)'--diff'[only show lines that have been added/removed/modified]'
--diff-context='[specify lines of context around added/removed/modified lines when using `--diff`]:lines'
--tabs='[set the tab width]:tab width [4]'
--wrap='[specify the text-wrapping mode]:mode [auto]:(auto never character)'
--wrap='[specify the text-wrapping mode]:mode [auto]:(auto never character word)'
'!(--wrap)'{-S,--chop-long-lines}
--terminal-width='[explicitly set the width of the terminal instead of determining it automatically]:width'
'(-n --number --diff --diff-context)'{-n,--number}'[show line numbers]'
@@ -54,14 +54,16 @@ _{{PROJECT_EXECUTABLE}}_main() {
--squeeze-blank'[squeeze consecutive empty lines into a single empty line]'
--squeeze-limit='[set the maximum number of consecutive empty lines]:limit:'
--strip-ansi='[specify when to strip ANSI escape sequences]:when:(auto never always)'
--sanitize='[specify when to sanitize untrusted input for safe display]:when:(auto never always)'
--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]'
'(-u --unbuffered)'--unbuffered'[this option exists for POSIX-compliance reasons]'
'(-u --unbuffered)'--unbuffered'[enable unbuffered input reading for streaming use cases]'
--completion='[show shell completion for a certain shell]:shell:(bash fish zsh ps1)'
--set-terminal-title'[sets terminal title to filenames when using a pager]'
--diagnostic'[show diagnostic information for bug reports]'
--quiet-empty'[do not produce any output when the input is empty]'
-P'[disable paging]'
"--no-config[don't use the configuration file]"
"--no-custom-assets[don't load custom assets]"
@@ -89,20 +91,25 @@ _{{PROJECT_EXECUTABLE}}_main() {
languages)
local IFS=$'\n'
local -a languages
languages=( $({{PROJECT_EXECUTABLE}} --list-languages | awk -F':|,' '{ for (i = 1; i <= NF; ++i) printf("%s:%s\n", $i, $1) }') )
# `--list-languages` emits one `name:matchers` line per language,
# which `_describe` parses as `value:description`. Only the
# language name is offered as the completion value; the matchers
# show up as the menu description. See
# https://github.com/sharkdp/bat/issues/3735.
languages=( ${(f)"$({{PROJECT_EXECUTABLE}} --no-paging --color=never --decorations=never --list-languages)"} )
_describe 'language' languages && ret=0
;;
themes)
local -a themes expl
themes=(${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} )
themes=(${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --no-paging --color=never --decorations=never --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)"} )
themes=(auto dark light auto:always auto:system ${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --no-paging --color=never --decorations=never --list-themes)"} )
_wanted themes expl 'theme' compadd -a themes && ret=0
;;
+12 -4
View File
@@ -102,8 +102,10 @@ Set the tab width to T spaces. Use a width of 0 to pass tabs through directly
.HP
\fB\-\-wrap\fR <mode>
.IP
Specify the text\-wrapping mode (*auto*, never, character). The '\-\-terminal\-width' option
can be used in addition to control the output width.
Specify the text\-wrapping mode (*auto*, never, character, word). The '\-\-terminal\-width' option
can be used in addition to control the output width. In \fBword\fR mode, lines are broken at
whitespace boundaries. If a single word exceeds the terminal width, it falls back to
character wrapping.
.HP
\fB\-S\fR, \fB\-\-chop\-long\-lines\fR
.IP
@@ -263,8 +265,10 @@ Display a list of supported languages for syntax highlighting.
.HP
\fB\-u\fR, \fB\-\-unbuffered\fR
.IP
This option exists for POSIX\-compliance reasons ('u' is for 'unbuffered'). The output is
always unbuffered \- this option is simply ignored.
Enable unbuffered input reading. When this flag is set, bat will display data as soon as it
is available, without waiting for a complete line. This is useful for streaming use cases like
\&'tail \-f logfile | bat \-u \-\-paging=never'. Note that line numbers are automatically disabled
in unbuffered mode, and syntax highlighting may be imperfect on partial lines.
.HP
\fB\-\-no\-custom\-assets\fR
.IP
@@ -282,6 +286,10 @@ Show bat's cache directory.
.IP
Show diagnostic information for bug reports.
.HP
\fB\-\-quiet\-empty\fR
.IP
Do not produce any output when the input is empty (e.g. an empty file or empty stdin). This is useful in scripts where silent behavior is preferred for empty input.
.HP
\fB\-\-acknowledgements\fR
.IP
Show acknowledgements.
+9
View File
@@ -2,6 +2,15 @@ diff --git syntaxes/01_Packages/Python/Python.sublime-syntax syntaxes/01_Package
index 2acd86d8..86257f7b 100644
--- syntaxes/01_Packages/Python/Python.sublime-syntax
+++ syntaxes/01_Packages/Python/Python.sublime-syntax
@@ -25,7 +31,7 @@ file_extensions:
- wscript
- bazel
- bzl
-first_line_match: ^#!\s*/.*\bpython(\d(\.\d)?)?\b
+first_line_match: ^#!\s*/.*\b(python(\d(\.\d)?)?|uv)\b
scope: source.python
variables:
@@ -988,10 +988,6 @@ contexts:
- match: \}
scope: punctuation.section.mapping-or-set.end.python
+12
View File
@@ -0,0 +1,12 @@
diff --git syntaxes/01_Packages/TCL/Tcl.sublime-syntax syntaxes/01_Packages/TCL/Tcl.sublime-syntax
index 1234567..abcdefg 100644
--- syntaxes/01_Packages/TCL/Tcl.sublime-syntax
+++ syntaxes/01_Packages/TCL/Tcl.sublime-syntax
@@ -3,6 +3,7 @@
# http://www.sublimetext.com/docs/3/syntax.html
name: Tcl
file_extensions:
- tcl
+first_line_match: ^\#!.*\b(tclsh|wish|expect)\b
scope: source.tcl
variables:
+1 -1
View File
@@ -79,7 +79,7 @@ contexts:
double_quoted_string:
- meta_include_prototype: false
- meta_scope: string.quoted.double.csv
- meta_scope: meta.string.quoted.double.csv
- match: '""'
scope: constant.character.escape.csv
- match: '"'
-398
View File
@@ -1,398 +0,0 @@
%YAML 1.2
---
# http://www.sublimetext.com/docs/3/syntax.html
name: Kotlin
file_extensions:
- kt
- kts
scope: source.Kotlin
contexts:
main:
- include: comments
- match: '^\s*(package)\b(?:\s*([^ ;$]+)\s*)?'
captures:
1: keyword.other.kotlin
2: entity.name.package.kotlin
- include: imports
- include: statements
classes:
- match: (?<!::)(?=\b(?:companion|class|object|interface)\b)
push:
- match: '(?=$|\})'
pop: true
- include: comments
- match: \b(companion\s*)?(class|object|interface)\b
captures:
1: storage.modifier.kotlin
2: storage.modifier.kotlin
push:
- match: '(?=<|\{|\(|:|$)'
pop: true
- include: comments
- match: \w+
scope: entity.name.type.class.kotlin
- match: <
push:
- match: ">"
pop: true
- include: generics
- match: \(
push:
- match: \)
pop: true
- include: parameters
- match: (:)
captures:
1: keyword.operator.declaration.kotlin
push:
- match: "(?={|$)"
pop: true
- match: \w+
scope: entity.other.inherited-class.kotlin
- match: \(
push:
- match: \)
pop: true
- include: expressions
- match: '\{'
push:
- match: '\}'
pop: true
- include: statements
comments:
- match: /\*
captures:
0: punctuation.definition.comment.kotlin
push:
- meta_scope: comment.block.kotlin
- match: \*/
captures:
0: punctuation.definition.comment.kotlin
pop: true
- match: \s*((//).*$\n?)
captures:
1: comment.line.double-slash.kotlin
2: punctuation.definition.comment.kotlin
constants:
- match: \b(true|false|null|this|super)\b
scope: constant.language.kotlin
- match: '\b((0(x|X)[0-9a-fA-F]*)|(([0-9]+\.?[0-9]*)|(\.[0-9]+))((e|E)(\+|-)?[0-9]+)?)([LlFf])?\b'
scope: constant.numeric.kotlin
- match: '\b([A-Z][A-Z0-9_]+)\b'
scope: constant.other.kotlin
expressions:
- match: \(
push:
- match: \)
pop: true
- include: expressions
- include: types
- include: strings
- include: constants
- include: comments
- include: keywords
functions:
- match: (?=\s*\b(?:fun)\b)
push:
- match: '(?=$|\})'
pop: true
- match: \b(fun)\b
captures:
1: keyword.other.kotlin
push:
- match: (?=\()
pop: true
- match: <
push:
- match: ">"
pop: true
- include: generics
- match: '([\.<\?>\w]+\.)?(\w+)'
captures:
2: entity.name.function.kotlin
- match: \(
push:
- match: \)
pop: true
- include: parameters
- match: (:)
captures:
1: keyword.operator.declaration.kotlin
push:
- match: "(?={|=|$)"
pop: true
- include: types
- match: '\{'
push:
- match: '(?=\})'
pop: true
- include: statements
- match: (=)
captures:
1: keyword.operator.assignment.kotlin
push:
- match: (?=$)
pop: true
- include: expressions
generics:
- match: (:)
captures:
1: keyword.operator.declaration.kotlin
push:
- match: (?=,|>)
pop: true
- include: types
- include: keywords
- match: \w+
scope: storage.type.generic.kotlin
getters-and-setters:
- match: \b(get)\b\s*\(\s*\)
captures:
1: entity.name.function.kotlin
push:
- match: '\}|(?=\bset\b)|$'
pop: true
- match: (=)
captures:
1: keyword.operator.assignment.kotlin
push:
- match: (?=$|\bset\b)
pop: true
- include: expressions
- match: '\{'
push:
- match: '\}'
pop: true
- include: expressions
- match: \b(set)\b\s*(?=\()
captures:
1: entity.name.function.kotlin
push:
- match: '\}|(?=\bget\b)|$'
pop: true
- match: \(
push:
- match: \)
pop: true
- include: parameters
- match: (=)
captures:
1: keyword.operator.assignment.kotlin
push:
- match: (?=$|\bset\b)
pop: true
- include: expressions
- match: '\{'
push:
- match: '\}'
pop: true
- include: expressions
imports:
- match: '^\s*(import)\s+[^ $]+\s+(as)?'
captures:
1: keyword.other.kotlin
2: keyword.other.kotlin
keywords:
- match: \b(var|val|public|private|protected|abstract|final|sealed|enum|open|attribute|annotation|override|inline|vararg|in|out|internal|data|tailrec|operator|infix|const|yield|typealias|typeof|reified|suspend)\b
scope: storage.modifier.kotlin
- match: \b(try|catch|finally|throw)\b
scope: keyword.control.catch-exception.kotlin
- match: \b(if|else|while|for|do|return|when|where|break|continue)\b
scope: keyword.control.kotlin
- match: \b(in|is|!in|!is|as|as\?|assert)\b
scope: keyword.operator.kotlin
- match: (==|!=|===|!==|<=|>=|<|>)
scope: keyword.operator.comparison.kotlin
- match: (=)
scope: keyword.operator.assignment.kotlin
- match: (::)
scope: keyword.operator.kotlin
- match: (:)
scope: keyword.operator.declaration.kotlin
- match: \b(by)\b
scope: keyword.other.by.kotlin
- match: (\?\.)
scope: keyword.operator.safenav.kotlin
- match: (\.)
scope: keyword.operator.dot.kotlin
- match: (\?:)
scope: keyword.operator.elvis.kotlin
- match: (\-\-|\+\+)
scope: keyword.operator.increment-decrement.kotlin
- match: (\+=|\-=|\*=|\/=)
scope: keyword.operator.arithmetic.assign.kotlin
- match: (\.\.)
scope: keyword.operator.range.kotlin
- match: (\-|\+|\*|\/|%)
scope: keyword.operator.arithmetic.kotlin
- match: (!|&&|\|\|)
scope: keyword.operator.logical.kotlin
- match: (;)
scope: punctuation.terminator.kotlin
namespaces:
- match: \b(namespace)\b
scope: keyword.other.kotlin
- match: '\{'
push:
- match: '\}'
pop: true
- include: statements
parameters:
- match: (:)
captures:
1: keyword.operator.declaration.kotlin
push:
- match: (?=,|\)|=)
pop: true
- include: types
- match: (=)
captures:
1: keyword.operator.declaration.kotlin
push:
- match: (?=,|\))
pop: true
- include: expressions
- include: keywords
- match: \w+
scope: variable.parameter.function.kotlin
statements:
- include: namespaces
- include: typedefs
- include: classes
- include: functions
- include: variables
- include: getters-and-setters
- include: expressions
strings:
- match: '"""'
captures:
0: punctuation.definition.string.begin.kotlin
push:
- meta_scope: string.quoted.third.kotlin
- match: '"""'
captures:
0: punctuation.definition.string.end.kotlin
pop: true
- match: '(\$\w+|\$\{[^\}]+\})'
scope: variable.parameter.template.kotlin
- match: \\.
scope: constant.character.escape.kotlin
- match: '"'
captures:
0: punctuation.definition.string.begin.kotlin
push:
- meta_scope: string.quoted.double.kotlin
- match: '"'
captures:
0: punctuation.definition.string.end.kotlin
pop: true
- match: '(\$\w+|\$\{[^\}]+\})'
scope: variable.parameter.template.kotlin
- match: \\.
scope: constant.character.escape.kotlin
- match: "'"
captures:
0: punctuation.definition.string.begin.kotlin
push:
- meta_scope: string.quoted.single.kotlin
- match: "'"
captures:
0: punctuation.definition.string.end.kotlin
pop: true
- match: \\.
scope: constant.character.escape.kotlin
- match: "`"
captures:
0: punctuation.definition.string.begin.kotlin
push:
- meta_scope: string.quoted.single.kotlin
- match: "`"
captures:
0: punctuation.definition.string.end.kotlin
pop: true
typedefs:
- match: (?=\s*(?:type))
push:
- match: (?=$)
pop: true
- match: \b(type)\b
scope: keyword.other.kotlin
- match: <
push:
- match: ">"
pop: true
- include: generics
- include: expressions
types:
- match: \b(Nothing|Any|Unit|String|CharSequence|Int|Boolean|Char|Long|Double|Float|Short|Byte|dynamic)\b
scope: storage.type.buildin.kotlin
- match: \b(IntArray|BooleanArray|CharArray|LongArray|DoubleArray|FloatArray|ShortArray|ByteArray)\b
scope: storage.type.buildin.array.kotlin
- match: \b(Array|Collection|List|Map|Set|MutableList|MutableMap|MutableSet|Sequence)<\b
captures:
1: storage.type.buildin.collection.kotlin
push:
- match: ">"
pop: true
- include: types
- include: keywords
- match: \w+<
push:
- match: ">"
pop: true
- include: types
- include: keywords
- match: '\{'
push:
- match: '\}'
pop: true
- include: statements
- match: \(
push:
- match: \)
pop: true
- include: types
- match: (->)
scope: keyword.operator.declaration.kotlin
variables:
- match: (?=\s*\b(?:var|val)\b)
push:
- match: (?=:|=|(\b(by)\b)|$)
pop: true
- match: \b(var|val)\b
captures:
1: keyword.other.kotlin
push:
- match: (?=:|=|(\b(by)\b)|$)
pop: true
- match: <
push:
- match: ">"
pop: true
- include: generics
- match: '([\.<\?>\w]+\.)?(\w+)'
captures:
2: entity.name.variable.kotlin
- match: (:)
captures:
1: keyword.operator.declaration.kotlin
push:
- match: (?==|$)
pop: true
- include: types
- include: getters-and-setters
- match: \b(by)\b
captures:
1: keyword.other.kotlin
push:
- match: (?=$)
pop: true
- include: expressions
- match: (=)
captures:
1: keyword.operator.assignment.kotlin
push:
- match: (?=$)
pop: true
- include: expressions
- include: getters-and-setters
+2 -1
View File
@@ -9,6 +9,7 @@ scope: source.man
variables:
section_heading: '^(?!#)\S.*$'
command_line_option: '(--?[A-Za-z0-9][_A-Za-z0-9-]*)'
ansi_escape_sequence: '\e\[[\?=]?(?:\d+;?)*[A-Za-z]'
contexts:
prototype:
@@ -69,7 +70,7 @@ contexts:
escape: '(?={{section_heading}})'
function-call:
- match: '\b([A-Za-z0-9_\-]+\.)?([A-Za-z0-9_\-]+)(\()([^)]*)(\))'
- match: '(?<!\e\[)(?:\b|\s)(?:{{ansi_escape_sequence}})?([A-Za-z0-9_\-]+\.)?([A-Za-z0-9_\-]+)(?:{{ansi_escape_sequence}})?(\()([^)]*)(\))'
captures:
1: entity.name.function.man
2: entity.name.function.man
+118 -53
View File
@@ -47,12 +47,49 @@ impl ToTokens for MappingTarget {
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr)]
/// Helper type for deserializing a `Matcher` from either a plain string or a
/// `{ glob = "...", case_sensitive = true }` struct.
#[derive(Deserialize)]
#[serde(untagged)]
enum RawMatcher {
Simple(String),
Full {
glob: String,
case_sensitive: Option<bool>,
},
}
/// Whether a glob pattern should be matched case-sensitively or case-insensitively.
///
/// Mirrors the runtime `Case` type in `src/syntax_mapping.rs`.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
enum Case {
Sensitive,
Insensitive,
}
impl ToTokens for Case {
fn to_tokens(&self, tokens: &mut TokenStream) {
let t = match self {
Self::Sensitive => quote! { Case::Sensitive },
Self::Insensitive => quote! { Case::Insensitive },
};
tokens.append_all(t);
}
}
/// A single matcher.
///
/// Codegen converts this into a `Lazy<Option<GlobMatcher>>`.
struct Matcher(Vec<MatcherSegment>);
/// Parse a matcher.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)]
#[serde(try_from = "RawMatcher")]
struct Matcher {
segments: Vec<MatcherSegment>,
/// Whether the glob pattern should be matched case-sensitively.
///
/// Defaults to `Case::Insensitive` for backwards compatibility.
case: Case,
}
/// Parse the glob pattern of a matcher.
///
/// Note that this implementation is rather strict: it will greedily interpret
/// every valid environment variable replacement as such, then immediately
@@ -66,68 +103,95 @@ struct Matcher(Vec<MatcherSegment>);
///
/// Revision history:
/// - 2024-02-20: allow `{` and `}` (glob brace expansion)
impl FromStr for Matcher {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use MatcherSegment as Seg;
static VAR_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\$\{([\w\d_]+)\}").unwrap());
fn parse_glob(s: &str) -> Result<Vec<MatcherSegment>, anyhow::Error> {
use MatcherSegment as Seg;
static VAR_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\$\{([\w\d_]+)\}").unwrap());
let mut segments = vec![];
let mut text_start = 0;
for capture in VAR_REGEX.captures_iter(s) {
let match_0 = capture.get(0).unwrap();
let mut segments = vec![];
let mut text_start = 0;
for capture in VAR_REGEX.captures_iter(s) {
let match_0 = capture.get(0).unwrap();
// text before this var
let text_end = match_0.start();
segments.push(Seg::Text(s[text_start..text_end].into()));
text_start = match_0.end();
// text before this var
let text_end = match_0.start();
segments.push(Seg::Text(s[text_start..text_end].into()));
text_start = match_0.end();
// this var
segments.push(Seg::Env(capture.get(1).unwrap().as_str().into()));
// this var
segments.push(Seg::Env(capture.get(1).unwrap().as_str().into()));
}
// possible trailing text
segments.push(Seg::Text(s[text_start..].into()));
// cleanup empty text segments
let non_empty_segments = segments
.into_iter()
.filter(|seg| seg.text().map(|t| !t.is_empty()).unwrap_or(true))
.collect_vec();
// sanity check
if non_empty_segments
.windows(2)
.any(|segs| segs[0].is_text() && segs[1].is_text())
{
unreachable!("Parsed into consecutive text segments: {non_empty_segments:?}");
}
// guard empty case
if non_empty_segments.is_empty() {
bail!(r#"Parsed an empty matcher: "{s}""#);
}
// guard variable syntax leftover fragments
if non_empty_segments
.iter()
.filter_map(Seg::text)
.any(|t| t.contains('$'))
{
bail!(r#"Invalid matcher: "{s}""#);
}
Ok(non_empty_segments)
}
impl TryFrom<RawMatcher> for Matcher {
type Error = anyhow::Error;
fn try_from(raw: RawMatcher) -> Result<Self, Self::Error> {
const DEFAULT_CASE: Case = Case::Insensitive;
match &raw {
RawMatcher::Simple(s) => {
let segments = parse_glob(s)?;
Ok(Self {
segments,
case: DEFAULT_CASE,
})
}
RawMatcher::Full {
glob,
case_sensitive,
} => {
let segments = parse_glob(glob)?;
let case = match case_sensitive {
None => DEFAULT_CASE,
Some(false) => Case::Insensitive,
Some(true) => Case::Sensitive,
};
Ok(Self { segments, case })
}
}
// possible trailing text
segments.push(Seg::Text(s[text_start..].into()));
// cleanup empty text segments
let non_empty_segments = segments
.into_iter()
.filter(|seg| seg.text().map(|t| !t.is_empty()).unwrap_or(true))
.collect_vec();
// sanity check
if non_empty_segments
.windows(2)
.any(|segs| segs[0].is_text() && segs[1].is_text())
{
unreachable!("Parsed into consecutive text segments: {non_empty_segments:?}");
}
// guard empty case
if non_empty_segments.is_empty() {
bail!(r#"Parsed an empty matcher: "{s}""#);
}
// guard variable syntax leftover fragments
if non_empty_segments
.iter()
.filter_map(Seg::text)
.any(|t| t.contains('$'))
{
bail!(r#"Invalid matcher: "{s}""#);
}
Ok(Self(non_empty_segments))
}
}
impl ToTokens for Matcher {
fn to_tokens(&self, tokens: &mut TokenStream) {
let t = match self.0.as_slice() {
let case = &self.case;
let t = match self.segments.as_slice() {
[] => unreachable!("0-length matcher should never be created"),
[MatcherSegment::Text(text)] => {
quote! { Lazy::new(|| Some(build_matcher_fixed(#text))) }
quote! { Lazy::new(|| Some(build_matcher_fixed(#text, #case))) }
}
// parser logic ensures that this case can only happen when there are dynamic segments
segs @ [_, ..] => quote! { Lazy::new(|| build_matcher_dynamic(&[ #(#segs),* ])) },
segs @ [_, ..] => {
quote! { Lazy::new(|| build_matcher_dynamic(&[ #(#segs),* ], #case)) }
}
};
tokens.append_all(t);
}
@@ -175,6 +239,7 @@ impl MatcherSegment {
/// A struct that models a single .toml file in /src/syntax_mapping/builtins/.
#[derive(Clone, Debug, Deserialize)]
struct MappingDefModel {
#[serde(default)]
mappings: IndexMap<MappingTarget, Vec<Matcher>>,
}
impl MappingDefModel {
+1 -1
View File
@@ -97,7 +97,7 @@ The following files have been manually modified after converting from a `.tmLang
as it is not kept in a standalone repository. The file is generated from
https://github.com/open-policy-agent/opa/blob/master/misc/syntax/textmate/Rego.tmLanguage
* `SML.sublime_syntax` has been added manually from
https://github.com/seanjames777/SML-Language-Definitiona as it is not
https://github.com/seanjames777/SML-Language-Definition as it is not
kept in a standalone repository. The file generated is from
https://github.com/seanjames777/SML-Language-Definition/blob/master/sml.tmLanguage
* `Cabal.sublime_syntax` has been added manually from
+28 -5
View File
@@ -37,6 +37,13 @@ Options:
name (like 'C++' or 'LaTeX') or possible file extension (like 'cpp', 'hpp' or 'md'). Use
'--list-languages' to show all supported language names and file extensions.
--fallback-syntax <fallback-syntax>
Set a fallback language for syntax highlighting when auto-detection fails. Unlike
'--language', this is only used when no syntax could be detected from filename, custom
syntax mappings, or first-line detection.
[aliases: --fallback-language]
-H, --highlight-line <N:M>
Highlight the specified line ranges with a different background color For example:
'--highlight-line 40' highlights line 40
@@ -61,8 +68,8 @@ Options:
Set the tab width to T spaces. Use a width of 0 to pass tabs through directly
--wrap <mode>
Specify the text-wrapping mode (*auto*, never, character). The '--terminal-width' option
can be used in addition to control the output width.
Specify the text-wrapping mode (*auto*, never, character, word). The '--terminal-width'
option can be used in addition to control the output width.
-S, --chop-long-lines
Truncate all lines longer than screen width. Alias for '--wrap=never'.
@@ -70,7 +77,8 @@ Options:
--terminal-width <width>
Explicitly set the width of the terminal instead of determining it automatically. If
prefixed with '+' or '-', the value will be treated as an offset to the actual terminal
width. See also: '--wrap'.
width. This can also be configured via the BAT_WIDTH environment variable (e.g. export
BAT_WIDTH="100"). See also: '--wrap'.
-n, --number
Only show line numbers, no other decorations. This is an alias for '--style=numbers'
@@ -156,6 +164,14 @@ Options:
escape sequences unless the syntax highlighting language is plain text. Possible values:
auto, always, *never*.
--sanitize <when>
Specify when to sanitize input bytes for safe terminal display. Implies --strip-ansi to
the same value, and additionally substitutes terminal-active control bytes (cursor moves,
charset switches, beep, etc.) and Unicode bidi / zero-width formatting characters with the
Unicode replacement character (U+FFFD). Tab, LF, FF, and CRLF pass through. Useful for
displaying untrusted file content (e.g. file-manager preview panes). Possible values:
auto, always, *never*.
--style <components>
Configure which elements (line numbers, file headers, grid borders, Git modifications, ..)
to display in addition to the file contents. The argument is a comma-separated list of
@@ -203,8 +219,11 @@ Options:
Display a list of supported languages for syntax highlighting.
-u, --unbuffered
This option exists for POSIX-compliance reasons ('u' is for 'unbuffered'). The output is
always unbuffered - this option is simply ignored.
Enable unbuffered input reading. When this flag is set, bat will display data as soon as
it is available, without waiting for a complete line. This is useful for streaming use
cases like 'tail -f logfile | bat -u --paging=never'. Note that line numbers are
automatically disabled in unbuffered mode, and syntax highlighting may be imperfect on
partial lines.
--completion <SHELL>
Show shell completion for a certain shell. [possible values: bash, fish, zsh, ps1]
@@ -212,6 +231,10 @@ Options:
--diagnostic
Show diagnostic information for bug reports.
-E, --quiet-empty
When this flag is set, bat will produce no output at all when the input is empty. This is
useful when piping commands that may produce empty output, like 'git diff'.
--acknowledgements
Show acknowledgements.
+7 -1
View File
@@ -17,6 +17,8 @@ Options:
Show plain style (alias for '--style=plain').
-l, --language <language>
Set the language for syntax highlighting.
--fallback-syntax <fallback-syntax>
Set a fallback language for undetected syntaxes. [aliases: --fallback-language]
-H, --highlight-line <N:M>
Highlight lines N through M.
--file-name <name>
@@ -26,7 +28,7 @@ Options:
--tabs <T>
Set the tab width to T spaces.
--wrap <mode>
Specify the text-wrapping mode (*auto*, never, character).
Specify the text-wrapping mode (*auto*, never, character, word).
-S, --chop-long-lines
Truncate all lines longer than screen width. Alias for '--wrap=never'.
-n, --number
@@ -58,8 +60,12 @@ Options:
Only print the lines from N to M.
-L, --list-languages
Display all supported languages.
-u, --unbuffered
Enable unbuffered input reading for streaming use cases.
--completion <SHELL>
Show shell completion for a certain shell. [possible values: bash, fish, zsh, ps1]
-E, --quiet-empty
Produce no output when the input is empty.
-h, --help
Print help (see more with '--help')
-V, --version
-2
View File
@@ -10,5 +10,3 @@ No issue will have a different priority based on sponsorship status of the
reporter.
Contributions from anybody are most welcomed, please see our [`CONTRIBUTING.md`](../CONTRIBUTING.md) guide.
If you want to see our biggest sponsors, check the top of [`README.md`](../README.md#sponsors).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Generated
+27
View File
@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1769789167,
"narHash": "sha256-kKB3bqYJU5nzYeIROI82Ef9VtTbu4uA3YydSk/Bioa8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "62c8382960464ceb98ea593cb8321a2cf8f9e3e5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+40
View File
@@ -0,0 +1,40 @@
{
description = "bat";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs =
{ self, ... }@inputs:
let
supportedSystems = [
"x86_64-linux" # 64-bit Intel/AMD Linux
"aarch64-linux" # 64-bit ARM Linux
"aarch64-darwin" # 64-bit ARM macOS
"x86_64-darwin" # 64-bit Intel macOS
];
forEachSupportedSystem =
f:
inputs.nixpkgs.lib.genAttrs supportedSystems (
system:
f {
inherit system;
pkgs = import inputs.nixpkgs {
inherit system;
};
}
);
in
{
devShells = forEachSupportedSystem (
{ pkgs, system }:
{
default = pkgs.mkShellNoCC {
packages = with pkgs; [
cargo
];
};
}
);
};
}
+204 -28
View File
@@ -158,7 +158,9 @@ impl HighlightingAssets {
let syntax_match = mapping.get_syntax_for(path);
if let Some(MappingTarget::MapToUnknown) = syntax_match {
return Err(Error::UndetectedSyntax(path.to_string_lossy().into()));
return Err(Error::UndetectedSyntax(
crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy()),
));
}
if let Some(MappingTarget::MapTo(syntax_name)) = syntax_match {
@@ -175,13 +177,17 @@ impl HighlightingAssets {
) {
(Some(syntax), _) => Ok(syntax),
(_, Some(MappingTarget::MapExtensionToUnknown)) => {
Err(Error::UndetectedSyntax(path.to_string_lossy().into()))
}
(_, Some(MappingTarget::MapExtensionToUnknown)) => Err(Error::UndetectedSyntax(
crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy()),
)),
_ => self
.get_syntax_for_file_extension(file_name, &mapping.ignored_suffixes)?
.ok_or_else(|| Error::UndetectedSyntax(path.to_string_lossy().into())),
.ok_or_else(|| {
Error::UndetectedSyntax(crate::preprocessor::sanitize_for_terminal(
&path.to_string_lossy(),
))
}),
}
}
@@ -210,6 +216,7 @@ impl HighlightingAssets {
pub(crate) fn get_syntax(
&self,
language: Option<&str>,
fallback_syntax: Option<&str>,
input: &mut OpenedInput,
mapping: &SyntaxMapping,
) -> Result<SyntaxReferenceInSet<'_>> {
@@ -222,21 +229,50 @@ impl HighlightingAssets {
}
let path = input.path();
let path_syntax = if let Some(path) = path {
self.get_syntax_for_path(
PathAbs::new(path).map_or_else(|_| path.to_owned(), |p| p.as_path().to_path_buf()),
mapping,
)
let absolute_path = path.and_then(|p| {
PathAbs::new(p)
.ok()
.map(|abs| abs.as_path().to_path_buf())
.or_else(|| Some(p.to_owned()))
});
let path_syntax = if let Some(ref path) = absolute_path {
self.get_syntax_for_path(path, mapping).or_else(|e| {
// If syntax detection failed on the given path, retry with the
// canonicalized path (which resolves symlinks). This handles
// cases like `Aliases/0install -> ../Formula/zero-install.rb`
// where the symlink name has no extension but the target does.
// See #1001.
if matches!(e, Error::UndetectedSyntax(_)) {
if let Ok(resolved) = fs::canonicalize(path) {
if resolved != *path {
return match self.get_syntax_for_path(&resolved, mapping) {
Ok(syntax) => Ok(syntax),
Err(Error::UndetectedSyntax(_)) => Err(e),
Err(err) => Err(err),
};
}
}
}
Err(e)
})
} else {
Err(Error::UndetectedSyntax("[unknown]".into()))
};
// If a path wasn't provided, or if path based syntax detection
// above failed, we fall back to first-line syntax detection.
match path_syntax {
// If a path wasn't provided, or if path based syntax detection
// above failed, we fall back to first-line syntax detection.
Err(Error::UndetectedSyntax(path)) => self
.get_first_line_syntax(&mut input.reader)?
.ok_or(Error::UndetectedSyntax(path)),
Err(Error::UndetectedSyntax(path)) => {
if let Some(syntax_in_set) = self.get_first_line_syntax(&mut input.reader)? {
Ok(syntax_in_set)
} else if let Some(language) = fallback_syntax {
self.find_syntax_by_token(language)?
.ok_or_else(|| Error::UnknownSyntax(language.to_owned()))
} else {
Err(Error::UndetectedSyntax(path))
}
}
_ => path_syntax,
}
}
@@ -262,6 +298,24 @@ impl HighlightingAssets {
.map(|syntax| SyntaxReferenceInSet { syntax, syntax_set }))
}
fn find_syntax_by_hidden_file_name(
&self,
file_name: &OsStr,
) -> Result<Option<SyntaxReferenceInSet<'_>>> {
let Some(hidden_file_extension) = file_name
.to_str()
.and_then(|name| name.strip_prefix('.'))
.filter(|name| !name.is_empty())
else {
return Ok(None);
};
// syntect stores `hidden_file_extensions` in the same extension list as
// regular file extensions, but dotfiles must be queried without the
// leading period.
self.find_syntax_by_extension(Some(OsStr::new(hidden_file_extension)))
}
fn find_syntax_by_token(&self, token: &str) -> Result<Option<SyntaxReferenceInSet<'_>>> {
let syntax_set = self.get_syntax_set()?;
Ok(syntax_set
@@ -275,6 +329,9 @@ impl HighlightingAssets {
ignored_suffixes: &IgnoredSuffixes,
) -> Result<Option<SyntaxReferenceInSet<'_>>> {
let mut syntax = self.find_syntax_by_extension(Some(file_name))?;
if syntax.is_none() {
syntax = self.find_syntax_by_hidden_file_name(file_name)?;
}
if syntax.is_none() {
syntax =
ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| {
@@ -290,15 +347,15 @@ impl HighlightingAssets {
file_name: &OsStr,
ignored_suffixes: &IgnoredSuffixes,
) -> Result<Option<SyntaxReferenceInSet<'_>>> {
let mut syntax = self.find_syntax_by_extension(Path::new(file_name).extension())?;
if syntax.is_none() {
syntax =
ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| {
// Note: recursion
self.get_syntax_for_file_extension(stripped_file_name, ignored_suffixes)
})?;
let stripped =
ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| {
self.get_syntax_for_file_extension(stripped_file_name, ignored_suffixes)
.map(Some)
})?;
match stripped {
Some(syntax) => Ok(syntax),
None => self.find_syntax_by_extension(Path::new(file_name).extension()),
}
Ok(syntax)
}
fn get_first_line_syntax(
@@ -395,11 +452,12 @@ mod tests {
fn get_syntax_name(
&self,
language: Option<&str>,
fallback_syntax: Option<&str>,
input: &mut OpenedInput,
mapping: &SyntaxMapping,
) -> String {
self.assets
.get_syntax(language, input, mapping)
.get_syntax(language, fallback_syntax, input, mapping)
.map(|syntax_in_set| syntax_in_set.syntax.name.clone())
.unwrap_or_else(|_| "!no syntax!".to_owned())
}
@@ -419,7 +477,7 @@ mod tests {
let dummy_stdin: &[u8] = &[];
let mut opened_input = input.open(dummy_stdin, None).unwrap();
self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
self.get_syntax_name(None, None, &mut opened_input, &self.syntax_mapping)
}
fn syntax_for_file_with_content_os(&self, file_name: &OsStr, first_line: &str) -> String {
@@ -429,7 +487,7 @@ mod tests {
let dummy_stdin: &[u8] = &[];
let mut opened_input = input.open(dummy_stdin, None).unwrap();
self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
self.get_syntax_name(None, None, &mut opened_input, &self.syntax_mapping)
}
#[cfg(unix)]
@@ -449,7 +507,7 @@ mod tests {
let input = Input::stdin().with_name(Some(file_name));
let mut opened_input = input.open(content, None).unwrap();
self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
self.get_syntax_name(None, None, &mut opened_input, &self.syntax_mapping)
}
fn syntax_is_same_for_inputkinds(&self, file_name: &str, content: &str) -> bool {
@@ -628,6 +686,48 @@ mod tests {
);
}
#[test]
fn syntax_detection_ignored_suffix_falls_back_to_first_line() {
let mut test = SyntaxDetectionTest::new();
// By default a `.txt` file uses Plain Text, even with a shebang: the
// `.txt` extension wins and first-line detection is not reached.
assert_eq!(
test.syntax_for_file_with_content("test.txt", "#!/usr/bin/env bash"),
"Plain Text"
);
// Once `.txt` is an ignored suffix it is stripped before detection. The
// stripped name (`test`) has no extension, so detection falls back to the
// first line -- even though `.txt` is itself a registered extension that
// would otherwise match as Plain Text. See #2745.
test.syntax_mapping.insert_ignored_suffix(".txt");
assert_eq!(
test.syntax_for_file_with_content("test.txt", "#!/usr/bin/env bash"),
"Bourne Again Shell (bash)"
);
assert_eq!(
test.syntax_for_file_with_content("test.txt", "<?php"),
"PHP"
);
// A `.txt` file without a recognizable first line keeps no syntax (the
// caller renders it as plain text); we must not resurrect the shadowed
// `.txt` -> Plain Text match here.
assert_eq!(
test.syntax_for_file_with_content("notes.txt", "just some prose"),
"!no syntax!"
);
// Stripping that exposes a real extension still works: `.dev` is ignored,
// and the remaining `.json` extension is detected as usual.
test.syntax_mapping.insert_ignored_suffix(".dev");
assert_eq!(
test.syntax_for_file_with_content("config.json.dev", ""),
"JSON"
);
}
#[test]
fn syntax_detection_is_case_insensitive() {
let mut test = SyntaxDetectionTest::new();
@@ -661,6 +761,55 @@ mod tests {
);
}
#[cfg(feature = "build-assets")]
#[test]
fn syntax_detection_hidden_file_extensions() {
let source_dir = TempDir::new().expect("creation of temporary source directory");
let cache_dir = TempDir::new().expect("creation of temporary cache directory");
let syntax_dir = source_dir.path().join("syntaxes");
std::fs::create_dir_all(&syntax_dir).expect("creation of syntax directory succeeds");
std::fs::write(
syntax_dir.join("HiddenFileExtension.sublime-syntax"),
r#"%YAML 1.2
---
name: Hidden File Extension
hidden_file_extensions:
- testrc
scope: source.hiddenfileextension
contexts:
main:
- match: .
scope: source.hiddenfileextension
"#,
)
.expect("custom syntax can be written");
build(
source_dir.path(),
false,
false,
cache_dir.path(),
env!("CARGO_PKG_VERSION"),
)
.expect("custom assets can be built");
let test = SyntaxDetectionTest {
assets: HighlightingAssets::from_cache(cache_dir.path())
.expect("custom syntax cache can be loaded"),
syntax_mapping: SyntaxMapping::new(),
temp_dir: TempDir::new().expect("creation of temporary directory"),
};
assert_eq!(test.syntax_for_file(".testrc"), "Hidden File Extension");
assert_eq!(
test.syntax_for_stdin_with_content(".testrc", b""),
"Hidden File Extension"
);
assert!(test.syntax_is_same_for_inputkinds(".testrc", ""));
}
#[cfg(unix)]
#[test]
fn syntax_detection_for_symlinked_file() {
@@ -682,8 +831,35 @@ mod tests {
let mut opened_input = input.open(dummy_stdin, None).unwrap();
assert_eq!(
test.get_syntax_name(None, &mut opened_input, &test.syntax_mapping),
test.get_syntax_name(None, None, &mut opened_input, &test.syntax_mapping),
"SSH Config"
);
}
#[cfg(unix)]
#[test]
fn syntax_detection_for_symlinked_file_by_target_extension() {
use std::os::unix::fs::symlink;
let test = SyntaxDetectionTest::new();
let formula_dir = test.temp_dir.path().join("Formula");
std::fs::create_dir(&formula_dir).unwrap();
let target = formula_dir.join("zero-install.rb");
File::create(&target).unwrap();
let aliases_dir = test.temp_dir.path().join("Aliases");
std::fs::create_dir(&aliases_dir).unwrap();
let link = aliases_dir.join("0install");
symlink(&target, &link).unwrap();
let input = Input::ordinary_file(&link);
let dummy_stdin: &[u8] = &[];
let mut opened_input = input.open(dummy_stdin, None).unwrap();
assert_eq!(
test.get_syntax_name(None, None, &mut opened_input, &test.syntax_mapping),
"Ruby"
);
}
}
+79 -29
View File
@@ -38,6 +38,15 @@ pub fn env_no_color() -> bool {
env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty())
}
fn parse_strip_ansi_value(raw: Option<&str>, flag_name: &str) -> StripAnsiMode {
match raw {
Some("never") | None => StripAnsiMode::Never,
Some("always") => StripAnsiMode::Always,
Some("auto") => StripAnsiMode::Auto,
_ => unreachable!("other values for {flag_name} are not allowed"),
}
}
enum HelpType {
Short,
Long,
@@ -110,16 +119,18 @@ impl App {
_ => interactive_output, // auto: use color if interactive
};
let custom_pager = matches.get_one::<String>("pager").map(|s| s.to_string());
let pager = matches.get_one::<String>("pager").map(|s| s.as_str());
let theme_options = Self::theme_options_from_matches(&matches);
let use_custom_assets = !matches.get_flag("no-custom-assets");
Self::display_help(
interactive_output,
help_type,
use_pager,
use_color,
custom_pager,
pager,
theme_options,
use_custom_assets,
)?;
std::process::exit(0);
}
@@ -136,8 +147,9 @@ impl App {
help_type: HelpType,
use_pager: bool,
use_color: bool,
custom_pager: Option<String>,
pager: Option<&str>,
theme_options: ThemeOptions,
use_custom_assets: bool,
) -> Result<()> {
use crate::assets::assets_from_cache_or_binary;
use crate::directories::PROJECT_DIRS;
@@ -164,12 +176,10 @@ impl App {
PagingMode::Never
};
let pager = bat::config::get_pager_executable(custom_pager.as_deref());
let help_config = Config {
style_components: StyleComponents::new(StyleComponent::Plain.components(false)),
paging_mode,
pager: pager.as_deref(),
pager,
colored_output: use_color,
true_color: use_color,
language: if use_color { Some("help") } else { None },
@@ -178,7 +188,7 @@ impl App {
};
let cache_dir = PROJECT_DIRS.cache_dir();
let assets = assets_from_cache_or_binary(false, cache_dir)?;
let assets = assets_from_cache_or_binary(use_custom_assets, cache_dir)?;
Controller::new(&help_config, &assets)
.run(inputs, None)
.ok();
@@ -383,6 +393,10 @@ impl App {
None
}
}),
fallback_syntax: self
.matches
.get_one::<String>("fallback-syntax")
.map(|s| s.as_str()),
show_nonprintable: self.matches.get_flag("show-all"),
nonprintable_notation: match self
.matches
@@ -398,27 +412,30 @@ impl App {
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") {
wrapping_mode: {
if self.matches.get_flag("chop-long-lines") {
WrappingMode::NoWrapping(true)
} else {
match self.matches.get_one::<String>("wrap").map(|s| s.as_str()) {
Some("character") => WrappingMode::Character,
Some("word") => WrappingMode::Word,
Some("never") => WrappingMode::NoWrapping(true),
Some("auto") | None => {
if style_components.plain() {
WrappingMode::NoWrapping(false)
if self.interactive_output || maybe_term_width.is_some() {
if style_components.plain() && maybe_term_width.is_none() {
WrappingMode::NoWrapping(false)
} else {
WrappingMode::Character
}
} else {
WrappingMode::Character
// We don't have the tty width when piping to another program.
// There's no point in wrapping when this is the case.
WrappingMode::NoWrapping(false)
}
}
_ => unreachable!("other values for --wrap are not allowed"),
}
} else {
WrappingMode::NoWrapping(true)
}
} else {
// We don't have the tty width when piping to another program.
// There's no point in wrapping when this is the case.
WrappingMode::NoWrapping(false)
},
colored_output: self.matches.get_flag("force-colorization")
|| match self.matches.get_one::<String>("color").map(|s| s.as_str()) {
@@ -450,16 +467,34 @@ impl App {
4
},
),
strip_ansi: match self
.matches
.get_one::<String>("strip-ansi")
.map(|s| s.as_str())
{
Some("never") => StripAnsiMode::Never,
Some("always") => StripAnsiMode::Always,
Some("auto") => StripAnsiMode::Auto,
_ => unreachable!("other values for --strip-ansi are not allowed"),
strip_ansi: {
let sanitize = parse_strip_ansi_value(
self.matches
.get_one::<String>("sanitize")
.map(|s| s.as_str()),
"--sanitize",
);
let strip_ansi = parse_strip_ansi_value(
self.matches
.get_one::<String>("strip-ansi")
.map(|s| s.as_str()),
"--strip-ansi",
);
// --sanitize implies --strip-ansi to the same value.
if sanitize != StripAnsiMode::Never {
sanitize
} else {
strip_ansi
}
},
sanitize: parse_strip_ansi_value(
self.matches
.get_one::<String>("sanitize")
.map(|s| s.as_str()),
"--sanitize",
),
quiet_empty: self.matches.get_flag("quiet-empty"),
unbuffered: self.matches.get_flag("unbuffered"),
theme: theme(self.theme_options()).to_string(),
visible_lines: match self.matches.try_contains_id("diff").unwrap_or_default()
&& self.matches.get_flag("diff")
@@ -580,7 +615,17 @@ impl App {
// Plain if `--plain` is specified at least once.
if self.matches.get_count("plain") > 0 {
return Some(StyleComponents(HashSet::from([StyleComponent::Plain])));
let mut components = HashSet::from([StyleComponent::Plain]);
// When --diff is active, preserve change markers and snip separators
// so that diff output remains visually useful.
if self.matches.try_contains_id("diff").unwrap_or_default()
&& self.matches.get_flag("diff")
{
#[cfg(feature = "git")]
components.insert(StyleComponent::Changes);
components.insert(StyleComponent::Snip);
}
return Some(StyleComponents(components));
}
// Default behavior.
@@ -617,10 +662,15 @@ impl App {
bat_warning!("Style 'rule' is a subset of style 'grid', 'rule' will not be visible.");
}
// Auto-disable line numbers in unbuffered mode to avoid confusion with partial lines
if self.matches.get_flag("unbuffered") {
styled_components.0.remove(&StyleComponent::LineNumbers);
}
Ok(styled_components)
}
fn theme_options(&self) -> ThemeOptions {
pub(crate) fn theme_options(&self) -> ThemeOptions {
Self::theme_options_from_matches(&self.matches)
}
+68 -17
View File
@@ -1,7 +1,5 @@
use bat::style::StyleComponentList;
use clap::{
crate_name, crate_version, value_parser, Arg, ArgAction, ArgGroup, ColorChoice, Command,
};
use clap::{crate_name, crate_version, value_parser, Arg, ArgAction, ColorChoice, Command};
use once_cell::sync::Lazy;
use std::env;
use std::path::{Path, PathBuf};
@@ -122,6 +120,17 @@ pub fn build_app(interactive_output: bool) -> Command {
language names and file extensions.",
),
)
.arg(
Arg::new("fallback-syntax")
.long("fallback-syntax")
.visible_alias("fallback-language")
.help("Set a fallback language for undetected syntaxes.")
.long_help(
"Set a fallback language for syntax highlighting when auto-detection fails. \
Unlike '--language', this is only used when no syntax could be detected from \
filename, custom syntax mappings, or first-line detection.",
),
)
.arg(
Arg::new("highlight-line")
.long("highlight-line")
@@ -213,11 +222,11 @@ pub fn build_app(interactive_output: bool) -> Command {
.long("wrap")
.overrides_with("wrap")
.value_name("mode")
.value_parser(["auto", "never", "character"])
.value_parser(["auto", "never", "character", "word"])
.default_value("auto")
.hide_default_value(true)
.help("Specify the text-wrapping mode (*auto*, never, character).")
.long_help("Specify the text-wrapping mode (*auto*, never, character). \
.help("Specify the text-wrapping mode (*auto*, never, character, word).")
.long_help("Specify the text-wrapping mode (*auto*, never, character, word). \
The '--terminal-width' option can be used in addition to \
control the output width."),
)
@@ -231,6 +240,7 @@ pub fn build_app(interactive_output: bool) -> Command {
.arg(
Arg::new("terminal-width")
.long("terminal-width")
.overrides_with("terminal-width")
.value_name("width")
.hide_short_help(true)
.allow_hyphen_values(true)
@@ -246,10 +256,13 @@ pub fn build_app(interactive_output: bool) -> Command {
})
.map_err(|e| e.to_string())
})
.help(
.help("Explicitly set the width of the terminal instead of determining it automatically.")
.long_help(
"Explicitly set the width of the terminal instead of determining it \
automatically. If prefixed with '+' or '-', the value will be treated \
as an offset to the actual terminal width. See also: '--wrap'.",
as an offset to the actual terminal width. This can also be configured \
via the BAT_WIDTH environment variable (e.g. export BAT_WIDTH=\"100\"). \
See also: '--wrap'.",
),
)
.arg(
@@ -467,6 +480,24 @@ pub fn build_app(interactive_output: bool) -> Command {
language is plain text. Possible values: auto, always, *never*.")
.hide_short_help(true)
)
.arg(
Arg::new("sanitize")
.long("sanitize")
.overrides_with("sanitize")
.value_name("when")
.value_parser(["auto", "always", "never"])
.default_value("never")
.hide_default_value(true)
.help("Sanitize untrusted input for safe display (auto, always, *never*)")
.long_help("Specify when to sanitize input bytes for safe terminal display. \
Implies --strip-ansi to the same value, and additionally substitutes \
terminal-active control bytes (cursor moves, charset switches, beep, etc.) \
and Unicode bidi / zero-width formatting characters with the Unicode \
replacement character (U+FFFD). Tab, LF, FF, and CRLF pass through. Useful \
for displaying untrusted file content (e.g. file-manager preview panes). \
Possible values: auto, always, *never*.")
.hide_short_help(true)
)
.arg(
Arg::new("style")
.long("style")
@@ -550,11 +581,14 @@ pub fn build_app(interactive_output: bool) -> Command {
.short('u')
.long("unbuffered")
.action(ArgAction::SetTrue)
.hide_short_help(true)
.help("Enable unbuffered input reading for streaming use cases.")
.long_help(
"This option exists for POSIX-compliance reasons ('u' is for \
'unbuffered'). The output is always unbuffered - this option \
is simply ignored.",
"Enable unbuffered input reading. When this flag is set, bat will \
display data as soon as it is available, without waiting for a \
complete line. This is useful for streaming use cases like \
'tail -f logfile | bat -u --paging=never'. Note that line numbers \
are automatically disabled in unbuffered mode, and syntax \
highlighting may be imperfect on partial lines.",
),
)
.arg(
@@ -643,6 +677,18 @@ pub fn build_app(interactive_output: bool) -> Command {
.hide_short_help(true)
.help("Show diagnostic information for bug reports."),
)
.arg(
Arg::new("quiet-empty")
.long("quiet-empty")
.short('E')
.action(ArgAction::SetTrue)
.help("Produce no output when the input is empty.")
.long_help(
"When this flag is set, bat will produce no output at all when \
the input is empty. This is useful when piping commands that may \
produce empty output, like 'git diff'.",
),
)
.arg(
Arg::new("acknowledgements")
.long("acknowledgements")
@@ -682,11 +728,20 @@ pub fn build_app(interactive_output: bool) -> Command {
Command::new("cache")
.hide(true)
.about("Modify the syntax-definition and theme cache")
.arg_required_else_help(true)
.arg(
Arg::new("help")
.short('h')
.long("help")
.action(ArgAction::Help)
.help("Print help"),
)
.arg(
Arg::new("build")
.long("build")
.short('b')
.action(ArgAction::SetTrue)
.conflicts_with("clear")
.help("Initialize (or update) the syntax/theme cache.")
.long_help(
"Initialize (or update) the syntax/theme cache by loading from \
@@ -698,13 +753,9 @@ pub fn build_app(interactive_output: bool) -> Command {
.long("clear")
.short('c')
.action(ArgAction::SetTrue)
.conflicts_with("build")
.help("Remove the cached syntax definitions and themes."),
)
.group(
ArgGroup::new("cache-actions")
.args(["build", "clear"])
.required(true),
)
.arg(
Arg::new("source")
.long("source")
+56 -4
View File
@@ -2,7 +2,7 @@ use std::env;
use std::ffi::OsString;
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use crate::directories::PROJECT_DIRS;
@@ -104,18 +104,32 @@ pub fn generate_config_file() -> bat::error::Result<()> {
pub fn get_args_from_config_file() -> Result<Vec<OsString>, shell_words::ParseError> {
let mut config = String::new();
if let Ok(c) = fs::read_to_string(system_config_file()) {
let system_config = system_config_file();
let user_config = config_file();
if let Ok(c) = fs::read_to_string(&system_config) {
config.push_str(&c);
config.push('\n');
}
if let Ok(c) = fs::read_to_string(config_file()) {
config.push_str(&c);
// Skip the user config if it resolves to the same file as the system config,
// which can happen when BAT_CONFIG_DIR is set to e.g. "/etc/bat". See #3589.
if !same_file(&system_config, &user_config) {
if let Ok(c) = fs::read_to_string(&user_config) {
config.push_str(&c);
}
}
get_args_from_str(&config)
}
fn same_file(a: &Path, b: &Path) -> bool {
match (fs::canonicalize(a), fs::canonicalize(b)) {
(Ok(a), Ok(b)) => a == b,
_ => a == b,
}
}
pub fn get_args_from_env_opts_var() -> Option<Result<Vec<OsString>, shell_words::ParseError>> {
env::var("BAT_OPTS").ok().map(|s| get_args_from_str(&s))
}
@@ -139,6 +153,7 @@ fn get_args_from_str(content: &str) -> Result<Vec<OsString>, shell_words::ParseE
pub fn get_args_from_env_vars() -> Vec<OsString> {
[
("--tabs", "BAT_TABS"),
("--terminal-width", "BAT_WIDTH"),
("--theme", bat::theme::env::BAT_THEME),
("--theme-dark", bat::theme::env::BAT_THEME_DARK),
("--theme-light", bat::theme::env::BAT_THEME_LIGHT),
@@ -214,3 +229,40 @@ fn comments() {
get_args_from_str(config).unwrap()
);
}
#[test]
fn same_file_identical_paths() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("config");
fs::write(&file, "").unwrap();
assert!(same_file(&file, &file));
}
#[test]
fn same_file_different_paths() {
let dir = tempfile::tempdir().unwrap();
let a = dir.path().join("a");
let b = dir.path().join("b");
fs::write(&a, "").unwrap();
fs::write(&b, "").unwrap();
assert!(!same_file(&a, &b));
}
#[test]
fn same_file_nonexistent() {
let dir = tempfile::tempdir().unwrap();
let a = dir.path().join("a");
let b = dir.path().join("b");
assert!(!same_file(&a, &b));
}
#[cfg(unix)]
#[test]
fn same_file_via_symlink() {
let dir = tempfile::tempdir().unwrap();
let original = dir.path().join("config");
let link = dir.path().join("link");
fs::write(&original, "").unwrap();
std::os::unix::fs::symlink(&original, &link).unwrap();
assert!(same_file(&original, &link));
}
+44 -13
View File
@@ -17,7 +17,6 @@ use std::path::Path;
use std::process;
use bat::output::{OutputHandle, OutputType};
use bat::theme::DetectColorScheme;
use nu_ansi_term::Color::Green;
use nu_ansi_term::Style;
@@ -39,7 +38,7 @@ use bat::{
error::*,
input::Input,
style::{StyleComponent, StyleComponents},
theme::{color_scheme, default_theme, ColorScheme},
theme::{default_theme, theme, ColorScheme, ThemeOptions},
MappingTarget, PagingMode,
};
@@ -153,7 +152,14 @@ pub fn get_languages(config: &Config, cache_dir: &Path) -> Result<String> {
let comma_separator = ", ";
let separator = " ";
// Line-wrapping for the possible file extension overflow.
let desired_width = config.term_width - longest - separator.len();
// Clamp instead of subtracting: a tiny `--terminal-width` (smaller than the
// longest language name) would otherwise underflow `usize` and wrap to a huge
// value, silently disabling wrapping (and panicking in debug/overflow-checked
// builds). Mirrors the `saturating_sub` fix applied to `print_snip` in #3804.
let desired_width = config
.term_width
.saturating_sub(longest)
.saturating_sub(separator.len());
let style = if config.colored_output {
Green.normal()
@@ -197,7 +203,7 @@ pub fn list_themes(
cfg: &Config,
config_dir: &Path,
cache_dir: &Path,
detect_color_scheme: DetectColorScheme,
theme_options: ThemeOptions,
) -> Result<()> {
let assets = assets_from_cache_or_binary(cfg.use_custom_assets, cache_dir)?;
let mut config = cfg.clone();
@@ -206,7 +212,7 @@ pub fn list_themes(
config.language = Some("Rust");
config.style_components = StyleComponents(style);
let default_theme_name = default_theme(color_scheme(detect_color_scheme).unwrap_or_default());
let default_theme_name = theme(theme_options).to_string();
let mut buf = String::new();
let mut handle = OutputHandle::FmtWrite(&mut buf);
@@ -259,7 +265,9 @@ pub fn list_themes(
fn set_terminal_title_to(new_terminal_title: String) {
let osc_command_for_setting_terminal_title = "\x1b]0;";
let osc_end_command = "\x07";
print!("{osc_command_for_setting_terminal_title}{new_terminal_title}{osc_end_command}");
// Prevent BEL/ESC/C1 bytes in the title from terminating or nesting the OSC.
let safe_title = bat::sanitize_for_terminal(&new_terminal_title);
print!("{osc_command_for_setting_terminal_title}{safe_title}{osc_end_command}");
io::stdout().flush().unwrap();
}
@@ -285,7 +293,28 @@ fn run_controller(inputs: Vec<Input>, config: &Config, cache_dir: &Path) -> Resu
#[cfg(feature = "bugreport")]
fn invoke_bugreport(app: &App, cache_dir: &Path) {
use bugreport::{bugreport, collector::*, format::Markdown};
use bugreport::{bugreport, collector::*, format::Markdown, report::ReportEntry};
struct ColorSchemeCollector;
impl Collector for ColorSchemeCollector {
fn description(&self) -> &str {
"Detected terminal color scheme"
}
fn collect(
&mut self,
_: &bugreport::CrateInfo,
) -> std::result::Result<ReportEntry, CollectionError> {
let color_scheme = bat::theme::color_scheme(bat::theme::DetectColorScheme::Always);
let text = match color_scheme {
Some(bat::theme::ColorScheme::Dark) => "dark",
Some(bat::theme::ColorScheme::Light) => "light",
None => "not detected",
};
Ok(ReportEntry::Text(text.to_string()))
}
}
let pager = bat::config::get_pager_executable(
app.matches.get_one::<String>("pager").map(|s| s.as_str()),
)
@@ -307,6 +336,9 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) {
"BAT_STYLE",
"BAT_TABS",
"BAT_THEME",
"BAT_WIDTH",
bat::theme::env::BAT_THEME_DARK,
bat::theme::env::BAT_THEME_LIGHT,
"COLORTERM",
"LANG",
"LC_ALL",
@@ -326,6 +358,7 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) {
custom_assets_metadata,
))
.info(DirectoryEntries::new("Custom assets", cache_dir))
.info(ColorSchemeCollector)
.info(CompileTimeInformation::default());
#[cfg(feature = "paging")]
@@ -394,14 +427,12 @@ fn run() -> Result<bool> {
if app.matches.get_flag("list-languages") {
let languages: String = get_languages(&config, cache_dir)?;
let inputs: Vec<Input> = vec![Input::from_reader(Box::new(languages.as_bytes()))];
let plain_config = Config {
style_components: StyleComponents::new(StyleComponent::Plain.components(false)),
paging_mode: PagingMode::QuitIfOneScreen,
..Default::default()
};
let mut plain_config = config.clone();
plain_config.style_components =
StyleComponents::new(StyleComponent::Plain.components(false));
run_controller(inputs, &plain_config, cache_dir)
} else if app.matches.get_flag("list-themes") {
list_themes(&config, config_dir, cache_dir, DetectColorScheme::default())?;
list_themes(&config, config_dir, cache_dir, app.theme_options())?;
Ok(true)
} else if app.matches.get_flag("config-file") {
println!("{}", config_file().to_string_lossy());
+14 -2
View File
@@ -38,6 +38,9 @@ pub struct Config<'a> {
/// The explicitly configured language, if any
pub language: Option<&'a str>,
/// The fallback syntax used when auto-detection fails
pub fallback_syntax: Option<&'a str>,
/// Whether or not to show/replace non-printable characters like space, tab and newline.
pub show_nonprintable: bool,
@@ -99,14 +102,23 @@ pub struct Config<'a> {
#[cfg(feature = "lessopen")]
pub use_lessopen: bool,
// Weather or not to set terminal title when using a pager
// Whether or not to set terminal title when using a pager
pub set_terminal_title: bool,
/// The maximum number of consecutive empty lines to display
pub squeeze_lines: Option<usize>,
// Weather or not to set terminal title when using a pager
// Whether or not to strip ANSI escape codes from the input
pub strip_ansi: StripAnsiMode,
// Substitute terminal-active and spoofing-relevant bytes; implies strip_ansi.
pub sanitize: StripAnsiMode,
/// Whether or not to produce no output when input is empty
pub quiet_empty: bool,
/// Whether or not to use unbuffered input reading for streaming use cases
pub unbuffered: bool,
}
#[cfg(all(feature = "minimal-application", feature = "paging"))]
+4
View File
@@ -158,6 +158,7 @@ impl Controller<'_> {
#[cfg(not(feature = "lessopen"))]
input.open(stdin, stdout_identifier)?
};
opened_input.reader.unbuffered = self.config.unbuffered;
#[cfg(feature = "git")]
let line_changes = if self.config.visible_lines.diff_mode()
|| (!self.config.loop_through && self.config.style_components.changes())
@@ -327,6 +328,9 @@ impl Controller<'_> {
}
printer.print_line(false, writer, line_nr, &line, max_buffered_line_number)?;
if self.config.unbuffered {
writer.flush()?;
}
}
RangeCheckResult::AfterLastRange => {
break;
+176 -62
View File
@@ -1,12 +1,13 @@
#![cfg(feature = "git")]
use gix::diff::blob::pipeline::{Mode, WorktreeRoots};
use gix::diff::blob::{Algorithm, HunkIter, ResourceKind};
use gix::object::tree::EntryKind;
use path_abs::PathInfo;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use git2::{DiffOptions, IntoCString, Repository};
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum LineChange {
Added,
RemovedAbove,
@@ -16,68 +17,181 @@ pub enum LineChange {
pub type LineChanges = HashMap<u32, LineChange>;
pub fn get_git_diff(filename: &Path) -> Option<LineChanges> {
let repo = Repository::discover(filename).ok()?;
let repo_path_absolute = fs::canonicalize(repo.workdir()?).ok()?;
let filepath_absolute = fs::canonicalize(filename).ok()?;
let filepath_relative_to_repo = filepath_absolute.strip_prefix(&repo_path_absolute).ok()?;
let mut diff_options = DiffOptions::new();
let pathspec = filepath_relative_to_repo.into_c_string().ok()?;
diff_options.pathspec(pathspec);
diff_options.context_lines(0);
let diff = repo
.diff_index_to_workdir(None, Some(&mut diff_options))
.ok()?;
let mut line_changes: LineChanges = HashMap::new();
let mark_section =
|line_changes: &mut LineChanges, start: u32, end: i32, change: LineChange| {
for line in start..=end as u32 {
line_changes.insert(line, change);
fn collect_changes_from_hunks(hunks: HunkIter) -> Option<LineChanges> {
let mut changes: LineChanges = HashMap::new();
for hunk in hunks {
if hunk.before.is_empty() && !hunk.after.is_empty() {
for line in hunk.after {
changes.insert(line + 1, LineChange::Added);
}
};
let _ = diff.foreach(
&mut |_, _| true,
None,
Some(&mut |delta, hunk| {
let path = delta.new_file().path().unwrap_or_else(|| Path::new(""));
if filepath_relative_to_repo != path {
return false;
}
let old_lines = hunk.old_lines();
let new_start = hunk.new_start();
let new_lines = hunk.new_lines();
let new_end = (new_start + new_lines) as i32 - 1;
if old_lines == 0 && new_lines > 0 {
mark_section(&mut line_changes, new_start, new_end, LineChange::Added);
} else if new_lines == 0 && old_lines > 0 {
if new_start == 0 {
mark_section(&mut line_changes, 1, 1, LineChange::RemovedAbove);
} else {
mark_section(
&mut line_changes,
new_start,
new_start as i32,
LineChange::RemovedBelow,
);
}
} else if hunk.after.is_empty() && !hunk.before.is_empty() {
if hunk.after.start == 0 {
changes.insert(1, LineChange::RemovedAbove);
} else {
mark_section(&mut line_changes, new_start, new_end, LineChange::Modified);
changes.insert(hunk.after.start, LineChange::RemovedBelow);
}
} else {
for line in hunk.after {
changes.insert(line + 1, LineChange::Modified);
}
}
}
true
}),
None,
Some(changes)
}
pub fn get_git_diff(filename: &Path) -> Option<LineChanges> {
let filepath_absolute = filename.canonicalize().ok()?;
let repository = gix::discover(filepath_absolute.parent().ok()?).ok()?;
let repo_path_absolute = repository.workdir()?.canonicalize().ok()?;
let filepath_relative_to_repo = gix::path::to_unix_separators_on_windows(gix::path::into_bstr(
filepath_absolute.strip_prefix(&repo_path_absolute).ok()?,
));
let index = repository.index_or_load_from_head_or_empty().ok()?;
let index_entry = index.entry_by_path(filepath_relative_to_repo.as_ref())?;
let mut cache = repository
.diff_resource_cache(
Mode::ToGit,
WorktreeRoots {
old_root: None,
new_root: repository.workdir().map(Path::to_path_buf),
},
)
.ok()?;
cache
.set_resource(
index_entry.id,
index_entry.mode.to_tree_entry_mode()?.kind(),
filepath_relative_to_repo.as_ref(),
ResourceKind::OldOrSource,
&repository,
)
.ok()?;
cache
.set_resource(
repository.object_hash().null(),
EntryKind::Blob,
filepath_relative_to_repo.as_ref(),
ResourceKind::NewOrDestination,
&repository,
)
.ok()?;
let diff = gix::diff::blob::diff_with_slider_heuristics(
Algorithm::Histogram,
&cache.prepare_diff().ok()?.interned_input(),
);
Some(line_changes)
collect_changes_from_hunks(diff.hunks())
}
#[cfg(test)]
mod tests {
use super::{get_git_diff, LineChange};
use std::path::{Path, PathBuf};
use std::process::Command;
fn git(repo: &Path, args: &[&str]) {
let output = Command::new("git")
.args(args)
.current_dir(repo)
.output()
.expect("git command can run");
assert!(
output.status.success(),
"git {args:?} failed\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn setup_repo() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("can create temporary directory");
let repo = dir.path();
git(repo, &["init"]);
git(repo, &["config", "user.email", "test@test.test"]);
git(repo, &["config", "user.name", "Test"]);
dir
}
fn create_and_track_file(repo: &tempfile::TempDir, filename: &str) -> PathBuf {
let filepath = repo.path().join(filename);
std::fs::write(&filepath, "file\n").expect("can write file");
git(repo.path(), &["add", filename]);
git(repo.path(), &["commit", "-m", "initial"]);
filepath.to_owned()
}
#[test]
fn not_a_git_repository() {
let dir = tempfile::tempdir().expect("can create temporary directory");
let file = dir.path().join("file.txt");
std::fs::write(&file, "line 1\n").expect("can write file");
assert_eq!(get_git_diff(&file), None);
}
#[test]
fn diff_is_calculated_against_index() {
let repo = setup_repo();
let file = create_and_track_file(&repo, "file.txt");
std::fs::write(&file, "line 1\nline 2 modified\n").expect("can write file");
// Add modified file to index -> should find 0 changes but not none
git(repo.path(), &["add", "file.txt"]);
assert_eq!(get_git_diff(&file).expect("empty map").len(), 0);
}
#[test]
fn diff_is_calculated_against_index_2() {
let repo = setup_repo();
let file = create_and_track_file(&repo, "file.txt");
std::fs::write(&file, "line 1\nline 2 modified\n").expect("can write file");
git(repo.path(), &["add", "file.txt"]);
// modify the second line again which should show a single change
std::fs::write(&file, "line 1\nline 2 modified again\n").expect("can write file");
assert_eq!(get_git_diff(&file).expect("one change").len(), 1);
}
#[test]
fn diff_is_calculated_correctly() {
let repo = setup_repo();
create_and_track_file(&repo, "committed.txt");
let filename = "committed2.txt";
let file = create_and_track_file(&repo, filename);
std::fs::write(&file, "file\nline 2 added\n").expect("can write file");
let mut changes = get_git_diff(&file).expect("multiple changes");
assert_eq!(changes.get(&2), Some(&LineChange::Added));
git(repo.path(), &["add", filename]);
std::fs::write(&file, "file\nline 2 modified\n").expect("can write file");
changes = get_git_diff(&file).expect("multiple changes");
assert_eq!(changes.get(&2), Some(&LineChange::Modified));
git(repo.path(), &["add", filename]);
std::fs::write(&file, "line 2 modified\nline 3 added").expect("can write file");
changes = get_git_diff(&file).expect("multiple changes");
assert_eq!(changes.get(&1), Some(&LineChange::RemovedAbove));
assert_eq!(changes.get(&2), Some(&LineChange::Added));
}
#[test]
fn faulty_git_version_does_not_panic() {
let repo = setup_repo();
let file = create_and_track_file(&repo, "file.txt");
std::fs::write(&file, "line 1\nline 2 modified\n").expect("can write file");
// changes are detected
assert_eq!(get_git_diff(&file).expect("one change").len(), 2);
// write invalid repositoryformatversion
git(
repo.path(),
&["config", "core.repositoryformatversion", "one"],
);
// changes are no longer detected
assert_eq!(get_git_diff(&file), None);
}
}
+185 -23
View File
@@ -207,7 +207,7 @@ impl<'a> Input<'a> {
kind: OpenedInputKind::StdIn,
description,
metadata: self.metadata,
reader: InputReader::new(stdin),
reader: InputReader::try_new(stdin)?,
})
}
@@ -216,34 +216,34 @@ impl<'a> Input<'a> {
description,
metadata: self.metadata,
reader: {
let mut file = File::open(&path)
.map_err(|e| format!("'{}': {e}", path.to_string_lossy()))?;
let path_display =
crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy());
let mut file =
File::open(&path).map_err(|e| format!("'{path_display}': {e}"))?;
if file.metadata()?.is_dir() {
return Err(format!("'{}' is a directory.", path.to_string_lossy()).into());
return Err(format!("'{path_display}' is a directory.").into());
}
if let Some(stdout) = stdout_identifier {
let input_identifier = Identifier::try_from(file).map_err(|e| {
format!("{}: Error identifying file: {e}", path.to_string_lossy())
})?;
let input_identifier = Identifier::try_from(file)
.map_err(|e| format!("{path_display}: Error identifying file: {e}"))?;
if stdout.surely_conflicts_with(&input_identifier) {
return Err(format!(
"IO circle detected. The input from '{}' is also an output. Aborting to avoid infinite loop.",
path.to_string_lossy()
"IO circle detected. The input from '{path_display}' is also an output. Aborting to avoid infinite loop.",
)
.into());
}
file = input_identifier.into_inner().expect("The file was lost in the clircle::Identifier, this should not have happened...");
}
InputReader::new(BufReader::new(file))
InputReader::try_new(BufReader::new(file))?
},
}),
InputKind::CustomReader(reader) => Ok(OpenedInput {
description,
kind: OpenedInputKind::CustomReader,
metadata: self.metadata,
reader: InputReader::new(BufReader::new(reader)),
reader: InputReader::try_new(BufReader::new(reader))?,
}),
}
}
@@ -253,30 +253,33 @@ pub(crate) struct InputReader<'a> {
inner: Box<dyn BufRead + 'a>,
pub(crate) first_line: Vec<u8>,
pub(crate) content_type: Option<ContentType>,
pub(crate) unbuffered: bool,
}
impl<'a> InputReader<'a> {
pub(crate) fn new<R: BufRead + 'a>(mut reader: R) -> InputReader<'a> {
let mut first_line = vec![];
reader.read_until(b'\n', &mut first_line).ok();
#[cfg(test)]
pub(crate) fn new<R: BufRead + 'a>(reader: R) -> InputReader<'a> {
Self::try_new(reader).expect("reading the first line failed")
}
let content_type = if first_line.is_empty() {
None
} else {
Some(content_inspector::inspect(&first_line[..]))
};
pub(crate) fn try_new<R: BufRead + 'a>(mut reader: R) -> io::Result<InputReader<'a>> {
let mut first_line = vec![];
reader.read_until(b'\n', &mut first_line)?;
let content_type = inspect_content_type(&first_line);
if content_type == Some(ContentType::UTF_16LE) {
read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A).ok();
read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A)?;
} else if content_type == Some(ContentType::UTF_16BE) {
read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00).ok();
read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00)?;
}
InputReader {
Ok(InputReader {
inner: Box::new(reader),
first_line,
content_type,
}
unbuffered: false,
})
}
pub(crate) fn read_line(&mut self, buf: &mut Vec<u8>) -> io::Result<bool> {
@@ -292,9 +295,48 @@ impl<'a> InputReader<'a> {
return read_utf16_line(&mut self.inner, buf, 0x0A, 0x00);
}
if self.unbuffered {
return self.read_line_unbuffered(buf);
}
let res = self.inner.read_until(b'\n', buf).map(|size| size > 0)?;
Ok(res)
}
fn read_line_unbuffered(&mut self, buf: &mut Vec<u8>) -> io::Result<bool> {
let available = self.inner.fill_buf()?;
if available.is_empty() {
return Ok(!buf.is_empty());
}
if let Some(pos) = available.iter().position(|&b| b == b'\n') {
buf.extend_from_slice(&available[..=pos]);
self.inner.consume(pos + 1);
} else {
let len = available.len();
buf.extend_from_slice(available);
self.inner.consume(len);
}
Ok(true)
}
}
fn inspect_content_type(first_line: &[u8]) -> Option<ContentType> {
if first_line.is_empty() {
return None;
}
let content_type = content_inspector::inspect(first_line);
if content_type == ContentType::UTF_8 && has_zip_signature(first_line) {
Some(ContentType::BINARY)
} else {
Some(content_type)
}
}
fn has_zip_signature(bytes: &[u8]) -> bool {
[b"PK\x03\x04", b"PK\x05\x06", b"PK\x07\x08"]
.into_iter()
.any(|signature| bytes.starts_with(signature))
}
fn read_utf16_line<R: BufRead>(
@@ -352,6 +394,43 @@ fn basic() {
assert!(buffer.is_empty());
}
#[test]
fn zip_magic_headers_are_treated_as_binary() {
for content in [b"PK\x03\x04hello", b"PK\x05\x06hello", b"PK\x07\x08hello"] {
let reader = InputReader::new(&content[..]);
assert_eq!(Some(ContentType::BINARY), reader.content_type);
}
}
#[test]
fn non_zip_pk_prefix_is_not_treated_as_binary() {
assert_eq!(
Some(ContentType::UTF_8),
inspect_content_type(b"PK\x03\x03hello")
);
}
#[test]
fn input_open_returns_initial_read_errors() {
struct FailingRead;
impl Read for FailingRead {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::other("initial read failed"))
}
}
let input = Input::from_reader(Box::new(FailingRead));
let result = input.open(io::empty(), None);
assert!(result.is_err());
assert!(result
.err()
.unwrap()
.to_string()
.contains("initial read failed"));
}
#[test]
fn utf16le() {
let content = b"\xFF\xFE\x73\x00\x0A\x00\x64\x00";
@@ -381,6 +460,89 @@ fn utf16le() {
assert!(buffer.is_empty());
}
#[test]
fn unbuffered_returns_partial_data() {
use std::io::Cursor;
let content = b"first line\npartial";
let mut reader = InputReader::new(Cursor::new(&content[..]));
reader.unbuffered = true;
// First call returns first_line (buffered during new())
let mut buffer = vec![];
let res = reader.read_line(&mut buffer);
assert!(res.is_ok());
assert!(res.unwrap());
assert_eq!(b"first line\n", &buffer[..]);
// Subsequent calls use unbuffered reading
buffer.clear();
let res = reader.read_line(&mut buffer);
assert!(res.is_ok());
assert!(res.unwrap());
assert_eq!(b"partial", &buffer[..]);
// EOF
buffer.clear();
let res = reader.read_line(&mut buffer);
assert!(res.is_ok());
assert!(!res.unwrap());
assert!(buffer.is_empty());
}
#[test]
fn unbuffered_returns_complete_lines() {
use std::io::Cursor;
let content = b"line1\nline2\n";
let mut reader = InputReader::new(Cursor::new(&content[..]));
reader.unbuffered = true;
// First call returns first_line
let mut buffer = vec![];
let res = reader.read_line(&mut buffer);
assert!(res.is_ok());
assert!(res.unwrap());
assert_eq!(b"line1\n", &buffer[..]);
// Second call returns line2 (complete line with newline)
buffer.clear();
let res = reader.read_line(&mut buffer);
assert!(res.is_ok());
assert!(res.unwrap());
assert_eq!(b"line2\n", &buffer[..]);
// EOF
buffer.clear();
let res = reader.read_line(&mut buffer);
assert!(res.is_ok());
assert!(!res.unwrap());
assert!(buffer.is_empty());
}
#[test]
fn unbuffered_eof_handling() {
use std::io::Cursor;
let content = b"only line\n";
let mut reader = InputReader::new(Cursor::new(&content[..]));
reader.unbuffered = true;
// First call returns first_line
let mut buffer = vec![];
let res = reader.read_line(&mut buffer);
assert!(res.is_ok());
assert!(res.unwrap());
assert_eq!(b"only line\n", &buffer[..]);
// EOF - empty buffer returns false
buffer.clear();
let res = reader.read_line(&mut buffer);
assert!(res.is_ok());
assert!(!res.unwrap());
assert!(buffer.is_empty());
}
#[test]
fn utf16le_issue3367() {
let content = b"\xFF\xFE\x0A\x4E\x00\x4E\x0A\x4F\x00\x52\x0A\x00\
+115 -7
View File
@@ -14,6 +14,53 @@ use crate::{
input::{Input, InputKind, InputReader, OpenedInput, OpenedInputKind},
};
/// Wrap `s` in POSIX single quotes. An embedded `'` is encoded as `'\''`
/// (close, escaped quote, reopen) since `'...'` has no escape character.
fn shell_quote(s: &str) -> String {
if !s.contains('\'') {
let mut quoted = String::with_capacity(s.len() + 2);
quoted.push('\'');
quoted.push_str(s);
quoted.push('\'');
return quoted;
}
let mut quoted = String::with_capacity(s.len() + 2);
quoted.push('\'');
for c in s.chars() {
if c == '\'' {
quoted.push_str("'\\''");
} else {
quoted.push(c);
}
}
quoted.push('\'');
quoted
}
/// Substitute the first `%s` in a $LESSOPEN/$LESSCLOSE template with a quoted value.
fn shell_substitute(template: &str, replacement: &str) -> String {
template.replacen("%s", &shell_quote(replacement), 1)
}
/// Substitute the first two `%s` occurrences positionally in one pass; chaining
/// `shell_substitute` twice would mis-target if `first` itself contains `%s`.
fn shell_substitute_two(template: &str, first: &str, second: &str) -> String {
let mut out = String::with_capacity(template.len() + first.len() + second.len() + 4);
let mut remaining = template;
let mut to_substitute = [Some(first), Some(second)];
for slot in &mut to_substitute {
if let Some(idx) = remaining.find("%s") {
out.push_str(&remaining[..idx]);
out.push_str(&shell_quote(slot.take().unwrap()));
remaining = &remaining[idx + 2..];
} else {
break;
}
}
out.push_str(remaining);
out
}
/// Preprocess files and/or stdin using $LESSOPEN and $LESSCLOSE
pub(crate) struct LessOpenPreprocessor {
lessopen: String,
@@ -87,7 +134,7 @@ impl LessOpenPreprocessor {
None => return input.open(stdin, stdout_identifier),
};
let mut lessopen_command = shell(self.lessopen.replacen("%s", path_str, 1));
let mut lessopen_command = shell(shell_substitute(&self.lessopen, path_str));
lessopen_command.stdout(Stdio::piped());
let lessopen_output = match lessopen_command.execute_output() {
@@ -122,7 +169,7 @@ impl LessOpenPreprocessor {
let mut stdin_buffer = Vec::new();
stdin.read_to_end(&mut stdin_buffer)?;
let mut lessopen_command = shell(self.lessopen.replacen("%s", "-", 1));
let mut lessopen_command = shell(shell_substitute(&self.lessopen, "-"));
lessopen_command.stdout(Stdio::piped());
let lessopen_output = match lessopen_command.execute_input_output(&stdin_buffer)
@@ -155,7 +202,7 @@ impl LessOpenPreprocessor {
Ok(OpenedInput {
kind,
reader: InputReader::new(BufReader::new(
reader: InputReader::try_new(BufReader::new(
if matches!(self.kind, LessOpenKind::TempFile) {
let lessopen_string = match String::from_utf8(lessopen_stdout) {
Ok(string) => string,
@@ -181,7 +228,7 @@ impl LessOpenPreprocessor {
lessclose: self
.lessclose
.as_ref()
.map(|s| s.replacen("%s", &path_str, 1).replacen("%s", &stdout, 1)),
.map(|s| shell_substitute_two(s, &path_str, &stdout)),
}
} else {
Preprocessed {
@@ -189,10 +236,10 @@ impl LessOpenPreprocessor {
lessclose: self
.lessclose
.as_ref()
.map(|s| s.replacen("%s", &path_str, 1).replacen("%s", "-", 1)),
.map(|s| shell_substitute_two(s, &path_str, "-")),
}
},
)),
))?,
metadata: input.metadata,
description: input.description,
})
@@ -261,7 +308,7 @@ impl Drop for Preprocessed {
}
};
if lessclose_output.status.success() {
if !lessclose_output.status.success() {
bat_warning!("$LESSCLOSE exited with nonzero exit code",)
};
}
@@ -384,4 +431,65 @@ mod tests {
Ok(())
}
#[test]
fn shell_quote_plain_filename() {
assert_eq!(super::shell_quote("file.txt"), "'file.txt'");
assert_eq!(super::shell_quote("with space.txt"), "'with space.txt'");
assert_eq!(super::shell_quote(""), "''");
assert_eq!(super::shell_quote("-"), "'-'");
}
#[test]
fn shell_quote_metacharacters_neutralised() {
assert_eq!(super::shell_quote("; rm -rf ~ ;"), "'; rm -rf ~ ;'");
assert_eq!(super::shell_quote("$(payload)"), "'$(payload)'");
assert_eq!(super::shell_quote("`payload`"), "'`payload`'");
assert_eq!(super::shell_quote("a|b&c;d"), "'a|b&c;d'");
assert_eq!(super::shell_quote("..>/dev/null"), "'..>/dev/null'");
assert_eq!(super::shell_quote("\n\t"), "'\n\t'");
}
#[test]
fn shell_quote_embedded_single_quote() {
assert_eq!(super::shell_quote("it's"), "'it'\\''s'");
assert_eq!(super::shell_quote("'"), "''\\'''");
assert_eq!(super::shell_quote("a'b'c"), "'a'\\''b'\\''c'");
}
#[test]
fn shell_substitute_only_replaces_first_percent_s() {
assert_eq!(super::shell_substitute("echo %s", "x"), "echo 'x'");
assert_eq!(super::shell_substitute("echo %s %s", "x"), "echo 'x' %s");
}
#[test]
fn shell_substitute_protects_against_filename_injection() {
let template = "|preproc %s";
let attacker_filename = "; rm -rf ~ ;";
let result = super::shell_substitute(template, attacker_filename);
assert_eq!(result, "|preproc '; rm -rf ~ ;'");
}
#[test]
fn shell_substitute_two_replaces_first_two_placeholders() {
assert_eq!(
super::shell_substitute_two("echo %s and %s done", "a", "b"),
"echo 'a' and 'b' done"
);
// A third %s is left alone.
assert_eq!(
super::shell_substitute_two("echo %s %s %s", "a", "b"),
"echo 'a' 'b' %s"
);
}
#[test]
fn shell_substitute_two_handles_percent_s_in_first_argument() {
// The chained-substitute approach would replace the injected `%s` here.
assert_eq!(
super::shell_substitute_two("a %s b %s c", "%s", "second"),
"a '%s' b 'second' c"
);
}
}
+1
View File
@@ -54,6 +54,7 @@ mod vscreen;
pub(crate) mod wrapping;
pub use nonprintable_notation::{BinaryBehavior, NonprintableNotation};
pub use preprocessor::sanitize_for_terminal;
pub use preprocessor::StripAnsiMode;
pub use pretty_printer::{Input, PrettyPrinter, Syntax};
pub use syntax_mapping::{MappingTarget, SyntaxMapping};
+39 -5
View File
@@ -23,6 +23,18 @@ pub struct BuiltinPager {
impl BuiltinPager {
fn new() -> Self {
let pager = minus::Pager::new();
let mut input_register = minus::input::HashedEventRegister::default();
input_register.add_key_events(&["home"], |_, _| {
minus::input::InputEvent::UpdateUpperMark(0)
});
input_register.add_key_events(&["end"], |_, _| {
minus::input::InputEvent::UpdateUpperMark(usize::MAX)
});
pager
.set_input_classifier(Box::new(input_register))
.expect("failed to set input classifier on newly created pager");
let handle = {
let pager = pager.clone();
Some(spawn(move || {
@@ -105,6 +117,10 @@ impl OutputType {
let resolved_path = match grep_cli::resolve_binary(&pager.bin) {
Ok(path) => path,
Err(_) => {
crate::bat_warning!(
"Pager '{}' not found, outputting to stdout instead",
pager.bin
);
return Ok(OutputType::stdout());
}
};
@@ -131,8 +147,13 @@ impl OutputType {
p.arg("-S"); // Short version of --chop-long-lines for compatibility
}
let less_version = retrieve_less_version(&pager.bin);
// Ensures that 'less' quits together with 'bat'
p.arg("-K"); // Short version of '--quit-on-intr'
// The BusyBox version of less does not support -K
if less_version != Some(LessVersion::BusyBox) {
p.arg("-K"); // Short version of '--quit-on-intr'
}
// Passing '--no-init' fixes a bug with '--quit-if-one-screen' in older
// versions of 'less'. Unfortunately, it also breaks mouse-wheel support.
@@ -142,7 +163,7 @@ impl OutputType {
// For newer versions (530 or 558 on Windows), we omit '--no-init' as it
// is not needed anymore.
if single_screen_action == SingleScreenAction::Quit {
match retrieve_less_version(&pager.bin) {
match less_version {
None => {
p.arg("--no-init");
}
@@ -169,7 +190,13 @@ impl OutputType {
Ok(p.stdin(Stdio::piped())
.spawn()
.map(OutputType::Pager)
.unwrap_or_else(|_| OutputType::stdout()))
.unwrap_or_else(|_| {
crate::bat_warning!(
"Pager '{}' not found, outputting to stdout instead",
&pager.bin
);
OutputType::stdout()
}))
}
pub(crate) fn stdout() -> Self {
@@ -210,8 +237,8 @@ impl Drop for OutputType {
let _ = command.wait();
}
OutputType::BuiltinPager(ref mut pager) => {
if pager.handle.is_some() {
let _ = pager.handle.take().unwrap().join().unwrap();
if let Some(handle) = pager.handle.take() {
let _ = handle.join();
}
}
OutputType::Stdout(_) => (),
@@ -231,4 +258,11 @@ impl OutputHandle<'_> {
Self::FmtWrite(handle) => handle.write_fmt(args).map_err(Into::into),
}
}
pub fn flush(&mut self) -> Result<()> {
match self {
Self::IoWrite(handle) => handle.flush().map_err(Into::into),
Self::FmtWrite(_) => Ok(()),
}
}
}
+320 -2
View File
@@ -139,16 +139,169 @@ pub fn replace_nonprintable(
/// Strips ANSI escape sequences from the input.
pub fn strip_ansi(line: &str) -> String {
let mut buffer = String::with_capacity(line.len());
for seq in EscapeSequenceOffsetsIterator::new(line) {
if let EscapeSequenceOffsets::Text { .. } = seq {
buffer.push_str(&line[seq.index_of_start()..seq.index_past_end()]);
}
}
buffer
}
/// Strips ANSI escape sequences and substitutes terminal-active control bytes
/// and visual-spoofing Unicode codepoints (bidi, zero-width) with U+FFFD.
pub fn sanitize(line: &str) -> String {
let stripped = strip_ansi(line);
let mut buffer = String::with_capacity(stripped.len());
let bytes = stripped.as_bytes();
let mut start = 0;
let mut i = 0;
// Skip directly to the next trigger byte instead of testing each one.
while let Some(off) = bytes[i..].iter().position(|&b| is_sanitize_trigger(b)) {
i += off;
let len = sanitize_at(bytes, i, &stripped, &mut buffer, &mut start);
i += len;
}
buffer.push_str(&stripped[start..]);
buffer
}
#[inline]
fn is_sanitize_trigger(b: u8) -> bool {
// C0 controls minus \t \n \f; DEL; UTF-8 leads with dangerous codepoints.
matches!(b, 0x00..=0x08 | 0x0B | 0x0D..=0x1F | 0x7F | 0xC2 | 0xD8 | 0xE2 | 0xEF)
}
/// Substitutes the byte/sequence at `bytes[i]` (or passes it through on
/// false-alarm trigger), flushing the prefix from `start`. Returns bytes consumed.
fn sanitize_at(
bytes: &[u8],
i: usize,
full: &str,
buffer: &mut String,
start: &mut usize,
) -> usize {
buffer.push_str(&full[*start..i]);
let consumed = match bytes[i] {
b'\r' if bytes.get(i + 1) == Some(&b'\n') => {
buffer.push_str("\r\n");
2
}
// 0xC2 leads U+0080..U+00FF; filter the C1 range.
0xC2 if matches!(bytes.get(i + 1), Some(0x80..=0x9F)) => {
buffer.push('\u{FFFD}');
2
}
0xE2 if is_dangerous_e2(bytes, i) => {
buffer.push('\u{FFFD}');
3
}
// 0xEF 0xBB 0xBF = U+FEFF (BOM / zero-width no-break space).
0xEF if bytes.get(i + 1) == Some(&0xBB) && bytes.get(i + 2) == Some(&0xBF) => {
buffer.push('\u{FFFD}');
3
}
// 0xD8 0x9C = U+061C (Arabic letter mark, a bidi control). The rest of
// the 0xD8 block is ordinary Arabic text.
0xD8 if bytes.get(i + 1) == Some(&0x9C) => {
buffer.push('\u{FFFD}');
2
}
// False-alarm trigger: pass the full UTF-8 sequence through.
lead @ (0xC2 | 0xD8 | 0xE2 | 0xEF) => {
let n = utf8_len_from_lead(lead);
buffer.push_str(&full[i..i + n]);
n
}
_ => {
buffer.push('\u{FFFD}');
1
}
};
*start = i + consumed;
consumed
}
#[inline]
fn is_dangerous_e2(bytes: &[u8], i: usize) -> bool {
// U+200B..D (zero-width), U+200E..F (LRM/RLM), U+202A..E (bidi embedding
// and override), U+2066..9 (bidi isolates).
matches!(
(bytes.get(i + 1), bytes.get(i + 2)),
(Some(0x80), Some(0x8B..=0x8F | 0xAA..=0xAE)) | (Some(0x81), Some(0xA6..=0xA9))
)
}
#[inline]
fn utf8_len_from_lead(lead: u8) -> usize {
if lead < 0x80 {
1
} else if lead < 0xE0 {
2
} else if lead < 0xF0 {
3
} else {
4
}
}
/// Escape C0, DEL, and C1 control characters so a string from an untrusted
/// filename or path can be safely written to the terminal.
pub fn sanitize_for_terminal(input: &str) -> String {
if !input
.chars()
.any(|c| matches!(c, '\x00'..='\x08' | '\x0A'..='\x1F' | '\x7F'..='\u{9F}'))
{
return input.to_owned();
}
let mut out = String::with_capacity(input.len() + 8);
for c in input.chars() {
match c {
'\t' => out.push('\t'),
'\x00'..='\x1F' => {
out.push('^');
out.push(char::from_u32(0x40 + c as u32).unwrap_or('?'));
}
'\x7F' => out.push_str("^?"),
'\u{80}'..='\u{9F}' => {
use std::fmt::Write as _;
let _ = write!(out, "\\u{{{:x}}}", c as u32);
}
other => out.push(other),
}
}
out
}
/// Strips overstrike sequences (backspace formatting) from input.
///
/// Overstrike formatting is used by man pages and some help output:
/// - Bold: `X\x08X` (character, backspace, same character)
/// - Underline: `_\x08X` (underscore, backspace, character)
///
/// This function removes these sequences, keeping only the visible character.
/// `first_backspace` is the position of the first backspace in the line.
pub fn strip_overstrike(line: &str, first_backspace: usize) -> String {
let mut output = String::with_capacity(line.len());
output.push_str(&line[..first_backspace]);
output.pop();
let mut remaining = &line[first_backspace + 1..];
loop {
if let Some(pos) = remaining.find('\x08') {
output.push_str(&remaining[..pos]);
output.pop();
remaining = &remaining[pos + 1..];
} else {
output.push_str(remaining);
break;
}
}
output
}
#[derive(Debug, PartialEq, Clone, Copy, Default)]
pub enum StripAnsiMode {
#[default]
@@ -211,3 +364,168 @@ fn test_strip_ansi() {
"multiple sequences"
);
}
#[test]
fn test_strip_ansi_8bit_c1_introducers() {
assert_eq!(strip_ansi("a\u{9B}31mRED\u{9B}0mb"), "aREDb");
assert_eq!(strip_ansi("a\x1bP1;0|payload\x1b\\b"), "ab");
assert_eq!(strip_ansi("a\u{90}body\u{9C}b"), "ab");
}
#[test]
fn test_strip_ansi_single_char_esc() {
// strip_ansi must consume both bytes of single-byte ESC sequences (RIS, DECSC, keypad, VT52).
assert_eq!(strip_ansi("a\x1bcb"), "ab");
assert_eq!(strip_ansi("a\x1b7b\x1b8c"), "abc");
assert_eq!(strip_ansi("a\x1b=b\x1b>c"), "abc");
assert_eq!(strip_ansi("a\x1bZb"), "ab");
}
#[test]
fn test_strip_ansi_preserves_control_bytes() {
// strip_ansi removes only ANSI escape sequences; control bytes pass through.
assert_eq!(strip_ansi("safe\rEVIL"), "safe\rEVIL");
assert_eq!(strip_ansi("a\x08b\x07c\x0E\x0Fd"), "a\x08b\x07c\x0E\x0Fd");
}
#[test]
fn test_sanitize_substitutes_dangerous_bytes() {
let r = '\u{FFFD}';
assert_eq!(sanitize("safe\rEVIL"), format!("safe{r}EVIL"));
assert_eq!(sanitize("a\x08b"), format!("a{r}b"));
assert_eq!(sanitize("a\x07b"), format!("a{r}b"));
assert_eq!(sanitize("a\x0Bb"), format!("a{r}b"));
assert_eq!(sanitize("a\x0Eb\x0Fc"), format!("a{r}b{r}c"));
assert_eq!(sanitize("a\u{8D}b"), format!("a{r}b"));
assert_eq!(sanitize("a\u{85}b"), format!("a{r}b"));
assert_eq!(sanitize("trailing\r"), format!("trailing{r}"));
assert_eq!(sanitize("a\x7Fb"), format!("a{r}b"));
}
#[test]
fn test_sanitize_substitutes_bidi_and_zero_width() {
let r = '\u{FFFD}';
// Trojan-Source bidi formatting: U+202A..U+202E
assert_eq!(sanitize("a\u{202A}b"), format!("a{r}b"));
assert_eq!(sanitize("a\u{202E}b"), format!("a{r}b"));
// Bidi isolates: U+2066..U+2069
assert_eq!(sanitize("a\u{2066}b"), format!("a{r}b"));
assert_eq!(sanitize("a\u{2069}b"), format!("a{r}b"));
// Zero-width: U+200B..U+200D
assert_eq!(sanitize("a\u{200B}b"), format!("a{r}b"));
assert_eq!(sanitize("a\u{200D}b"), format!("a{r}b"));
// BOM in middle of file: U+FEFF
assert_eq!(sanitize("a\u{FEFF}b"), format!("a{r}b"));
}
#[test]
fn test_sanitize_substitutes_every_bidi_control() {
// Unicode Bidi_Control is exactly these 12 codepoints. Covering only some
// of them leaves a reordering attack available through the rest.
let r = '\u{FFFD}';
for c in [
'\u{061C}', '\u{200E}', '\u{200F}', '\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}',
'\u{202E}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{2069}',
] {
assert_eq!(
sanitize(&format!("a{c}b")),
format!("a{r}b"),
"U+{:04X} was not substituted",
c as u32
);
}
}
#[test]
fn test_sanitize_preserves_arabic_sharing_the_alm_lead_byte() {
// U+061C is reached via lead byte 0xD8, which also leads ordinary Arabic.
assert_eq!(
sanitize("\u{0600}\u{061F}\u{06FF}"),
"\u{0600}\u{061F}\u{06FF}"
);
assert_eq!(sanitize("مرحبا بالعالم"), "مرحبا بالعالم");
}
#[test]
fn test_sanitize_preserves_legitimate_bytes() {
assert_eq!(sanitize("crlf\r\nline\r\n"), "crlf\r\nline\r\n");
assert_eq!(sanitize("a\tb\nc"), "a\tb\nc");
assert_eq!(sanitize("plain ascii"), "plain ascii");
assert_eq!(sanitize("üñíçödé"), "üñíçödé");
// FF (U+000C) passes through; section separator in C source / Emacs Lisp.
assert_eq!(sanitize("section1\x0Csection2"), "section1\x0Csection2");
// Common Unicode that shares a UTF-8 lead byte with dangerous codepoints
// must pass through unchanged.
assert_eq!(sanitize("snowman ☃ moon ☾"), "snowman ☃ moon ☾");
assert_eq!(sanitize("emoji 🎉 ☃"), "emoji 🎉 ☃");
assert_eq!(sanitize("0xC2 lead: ÿ ñ ç"), "0xC2 lead: ÿ ñ ç");
assert_eq!(sanitize("CJK 漢字 emoji 🦀"), "CJK 漢字 emoji 🦀");
}
#[test]
fn test_sanitize_strips_ansi() {
let r = '\u{FFFD}';
// sanitize is a strict superset of strip_ansi.
assert_eq!(sanitize("a\x1B[31mb\x1B[0mc"), "abc");
assert_eq!(sanitize("a\u{9B}31mb\u{9B}0mc"), "abc");
// ANSI then dangerous byte: ANSI gone, byte substituted.
assert_eq!(sanitize("\x1B[31mhello\rEVIL"), format!("hello{r}EVIL"));
}
#[test]
fn test_strip_overstrike() {
// Bold: X\x08X (same char repeated)
assert_eq!(strip_overstrike("H\x08Hello", 1), "Hello");
// Underline: _\x08X (underscore before char)
assert_eq!(strip_overstrike("_\x08Hello", 1), "Hello");
// Multiple overstrike sequences
assert_eq!(strip_overstrike("B\x08Bo\x08ol\x08ld\x08d", 1), "Bold");
// Backspace at start of line (nothing to pop)
assert_eq!(strip_overstrike("\x08Hello", 0), "Hello");
// Multiple consecutive backspaces
assert_eq!(strip_overstrike("ABC\x08\x08\x08XYZ", 3), "XYZ");
// Unicode with overstrike
assert_eq!(strip_overstrike("ä\x08äöü", 2), "äöü");
}
#[test]
fn test_sanitize_for_terminal_passthrough() {
assert_eq!(sanitize_for_terminal(""), "");
assert_eq!(sanitize_for_terminal("hello.txt"), "hello.txt");
assert_eq!(sanitize_for_terminal("résumé.pdf"), "résumé.pdf");
assert_eq!(sanitize_for_terminal("日本語.md"), "日本語.md");
assert_eq!(
sanitize_for_terminal("path/with spaces/file.log"),
"path/with spaces/file.log"
);
assert_eq!(sanitize_for_terminal("a\tb"), "a\tb");
}
#[test]
fn test_sanitize_for_terminal_c0_controls() {
assert_eq!(
sanitize_for_terminal("\x1b[31mINJECTED\x1b[0m.txt"),
"^[[31mINJECTED^[[0m.txt"
);
assert_eq!(sanitize_for_terminal("bad\x07rest"), "bad^Grest");
assert_eq!(sanitize_for_terminal("\x00\x08\n\r\x7F"), "^@^H^J^M^?");
assert_eq!(sanitize_for_terminal("\u{9b}31m"), "\\u{9b}31m");
assert_eq!(
sanitize_for_terminal("\u{9d}0;pwned\x07"),
"\\u{9d}0;pwned^G"
);
}
#[test]
fn test_sanitize_for_terminal_idempotent_on_sanitized() {
let dirty = "\x1b]0;pwned\x07file.txt";
let clean = sanitize_for_terminal(dirty);
assert_eq!(sanitize_for_terminal(&clean), clean);
assert!(!clean.contains('\x1b'));
assert!(!clean.contains('\x07'));
}
+9
View File
@@ -192,6 +192,15 @@ impl<'a> PrettyPrinter<'a> {
self
}
/// Whether to sanitize untrusted input for safe display (default: never)
///
/// Strips ANSI escape sequences and additionally substitutes terminal-active
/// control bytes and bidi / zero-width codepoints with U+FFFD.
pub fn sanitize(&mut self, mode: StripAnsiMode) -> &mut Self {
self.config.sanitize = mode;
self
}
/// Text wrapping mode (default: do not wrap)
pub fn wrapping_mode(&mut self, mode: WrappingMode) -> &mut Self {
self.config.wrapping_mode = mode;
+131 -18
View File
@@ -29,8 +29,10 @@ use crate::error::*;
use crate::input::OpenedInput;
use crate::line_range::{MaxBufferedLineNumber, RangeCheckResult};
use crate::output::OutputHandle;
use crate::preprocessor::strip_ansi;
use crate::preprocessor::{expand_tabs, replace_nonprintable};
use crate::preprocessor::{
expand_tabs, replace_nonprintable, sanitize, sanitize_for_terminal, strip_ansi,
strip_overstrike,
};
use crate::style::StyleComponent;
use crate::terminal::{as_terminal_escaped, to_ansi_color};
use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator};
@@ -38,6 +40,16 @@ use crate::wrapping::WrappingMode;
use crate::BinaryBehavior;
use crate::StripAnsiMode;
// Return the displayed width of a character.
//
// Control characters (0x00..=0x1F and 0x7F) are rendered by the terminal
// in caret notation (e.g. ^@, ^A, ..., ^?), which occupies two columns.
// UnicodeWidthChar::width() returns None for these, so we map them to 2
// here instead of the previous default of 0.
fn char_width(c: char) -> usize {
c.width().unwrap_or(if c.is_control() { 2 } else { 0 })
}
const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI {
raw_sequence: "\x1B[4m",
parameters: "4",
@@ -199,6 +211,8 @@ pub(crate) struct InteractivePrinter<'a> {
background_color_highlight: Option<Color>,
consecutive_empty_lines: usize,
strip_ansi: bool,
sanitize: bool,
strip_overstrike: bool,
}
impl<'a> InteractivePrinter<'a> {
@@ -261,19 +275,31 @@ impl<'a> InteractivePrinter<'a> {
let needs_to_match_syntax = (!is_printing_binary
|| matches!(config.binary, BinaryBehavior::AsText))
&& (config.colored_output || config.strip_ansi == StripAnsiMode::Auto);
&& (config.colored_output
|| config.strip_ansi == StripAnsiMode::Auto
|| config.sanitize == StripAnsiMode::Auto);
let (is_plain_text, highlighter_from_set) = if needs_to_match_syntax {
let (is_plain_text, strip_overstrike, highlighter_from_set) = if needs_to_match_syntax {
// Determine the type of syntax for highlighting
const PLAIN_TEXT_SYNTAX: &str = "Plain Text";
match assets.get_syntax(config.language, input, &config.syntax_mapping) {
const MANPAGE_SYNTAX: &str = "Manpage";
const COMMAND_HELP_SYNTAX: &str = "Command Help";
match assets.get_syntax(
config.language,
config.fallback_syntax,
input,
&config.syntax_mapping,
) {
Ok(syntax_in_set) => (
syntax_in_set.syntax.name == PLAIN_TEXT_SYNTAX,
syntax_in_set.syntax.name == MANPAGE_SYNTAX
|| syntax_in_set.syntax.name == COMMAND_HELP_SYNTAX,
Some(HighlighterFromSet::new(syntax_in_set, theme)),
),
Err(Error::UndetectedSyntax(_)) => (
true,
false,
Some(
assets
.find_syntax_by_name(PLAIN_TEXT_SYNTAX)?
@@ -285,7 +311,7 @@ impl<'a> InteractivePrinter<'a> {
Err(e) => return Err(e),
}
} else {
(false, None)
(false, false, None)
};
// Determine when to strip ANSI sequences
@@ -297,6 +323,14 @@ impl<'a> InteractivePrinter<'a> {
_ => false,
};
let sanitize = match config.sanitize {
_ if config.show_nonprintable => false,
StripAnsiMode::Always => true,
StripAnsiMode::Auto if is_plain_text => false,
StripAnsiMode::Auto => true,
_ => false,
};
Ok(InteractivePrinter {
panel_width,
colors,
@@ -310,6 +344,8 @@ impl<'a> InteractivePrinter<'a> {
background_color_highlight,
consecutive_empty_lines: 0,
strip_ansi,
sanitize,
strip_overstrike,
})
}
@@ -448,6 +484,11 @@ impl Printer for InteractivePrinter<'_> {
input: &OpenedInput,
add_header_padding: bool,
) -> Result<()> {
// If input is empty and quiet_empty is enabled, skip all output
if self.content_type.is_none() && self.config.quiet_empty {
return Ok(());
}
if add_header_padding && self.config.style_components.rule() {
self.print_horizontal_line_term(handle, self.colors.rule)?;
}
@@ -463,7 +504,7 @@ impl Printer for InteractivePrinter<'_> {
(but will be present if the output of 'bat' is piped). You can use 'bat -A' \
to show the binary file contents.",
Yellow.paint("[bat warning]"),
input.description.summary(),
sanitize_for_terminal(&input.description.summary()),
)?;
} else if self.config.style_components.grid() {
self.print_horizontal_line(handle, '┬')?;
@@ -517,9 +558,11 @@ impl Printer for InteractivePrinter<'_> {
"{}{}{mode}",
description
.kind()
.map(|kind| format!("{kind}: "))
.map(|kind| format!("{}: ", sanitize_for_terminal(kind)))
.unwrap_or_else(|| "".into()),
self.colors.header_value.paint(description.title()),
self.colors
.header_value
.paint(sanitize_for_terminal(description.title())),
);
self.print_header_multiline_component(handle, &header_filename)
}
@@ -550,6 +593,11 @@ impl Printer for InteractivePrinter<'_> {
}
fn print_footer(&mut self, handle: &mut OutputHandle, _input: &OpenedInput) -> Result<()> {
// If input is empty and quiet_empty is enabled, skip footer
if self.content_type.is_none() && self.config.quiet_empty {
return Ok(());
}
if self.config.style_components.grid()
&& (self.content_type.is_some_and(|c| c.is_text())
|| self.config.show_nonprintable
@@ -568,11 +616,23 @@ impl Printer for InteractivePrinter<'_> {
let title = "8<";
let title_count = title.chars().count();
let snip_left = "".repeat((self.config.term_width - panel_count - (title_count / 2)) / 4);
let snip_left = "".repeat(
self.config
.term_width
.saturating_sub(panel_count)
.saturating_sub(title_count / 2)
/ 4,
);
let snip_left_count = snip_left.chars().count(); // Can't use .len() with Unicode.
let snip_right =
"".repeat((self.config.term_width - panel_count - snip_left_count - title_count) / 2);
let snip_right = "".repeat(
self.config
.term_width
.saturating_sub(panel_count)
.saturating_sub(snip_left_count)
.saturating_sub(title_count)
/ 2,
);
writeln!(
handle,
@@ -622,8 +682,16 @@ impl Printer for InteractivePrinter<'_> {
}
};
// If ANSI escape sequences are supposed to be stripped, do it before syntax highlighting.
if self.strip_ansi {
if self.strip_overstrike {
if let Some(pos) = line.find('\x08') {
line = strip_overstrike(&line, pos).into();
}
}
// Sanitize is the strict superset; otherwise strip-ansi alone.
if self.sanitize {
line = sanitize(&line).into()
} else if self.strip_ansi {
line = strip_ansi(&line).into()
}
@@ -759,11 +827,21 @@ impl Printer for InteractivePrinter<'_> {
// Displayed width of line_buf
let mut current_width = 0;
let word_wrap = matches!(self.config.wrapping_mode, WrappingMode::Word);
// For word wrapping, track last whitespace position.
let mut last_ws_idx: Option<usize> = None;
for c in text.chars() {
// calculate the displayed width for next character
let cw = c.width().unwrap_or(0);
let cw = char_width(c);
current_width += cw;
// Track whitespace positions for word wrapping.
if word_wrap && c.is_whitespace() {
last_ws_idx = Some(line_buf.len());
}
// if next character cannot be printed on this line,
// flush the buffer.
if current_width > max_width {
@@ -785,13 +863,37 @@ impl Printer for InteractivePrinter<'_> {
}
}
// Determine the break point and remainder
// for word wrapping.
let (emit_end, rest_start) = if word_wrap {
if let Some(ws_idx) = last_ws_idx {
// Skip the whitespace character itself
// and carry the rest to the next line.
let rs = ws_idx
+ line_buf[ws_idx..]
.chars()
.next()
.map(|ch| ch.len_utf8())
.unwrap_or(0);
(ws_idx, Some(rs))
} else {
(line_buf.len(), None)
}
} else {
(line_buf.len(), None)
};
// It wraps.
write!(
handle,
"{}{}\n{}",
as_terminal_escaped(
style,
&format!("{}{line_buf}", self.ansi_style),
&format!(
"{}{}",
self.ansi_style,
&line_buf[..emit_end]
),
self.config.true_color,
self.config.colored_output,
self.config.use_italic_text,
@@ -804,8 +906,19 @@ impl Printer for InteractivePrinter<'_> {
cursor = 0;
max_width = cursor_max;
line_buf.clear();
current_width = cw;
if let Some(rs) = rest_start {
// Word wrap: carry remainder to next line.
let remainder = line_buf[rs..].to_string();
let rem_width: usize =
remainder.chars().map(char_width).sum();
line_buf.clear();
line_buf.push_str(&remainder);
current_width = rem_width + cw;
} else {
line_buf.clear();
current_width = cw;
}
last_ws_idx = None;
}
line_buf.push(c);
+54 -3
View File
@@ -17,9 +17,16 @@ use ignored_suffixes::IgnoredSuffixes;
mod builtin;
pub mod ignored_suffixes;
fn make_glob_matcher(from: &str) -> Result<GlobMatcher> {
/// Whether a glob pattern should be matched case-sensitively or case-insensitively.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Case {
Sensitive,
Insensitive,
}
fn make_glob_matcher(from: &str, case: Case) -> Result<GlobMatcher> {
let matcher = GlobBuilder::new(from)
.case_insensitive(true)
.case_insensitive(matches!(case, Case::Insensitive))
.literal_separator(true)
.build()?
.compile_matcher();
@@ -97,7 +104,14 @@ impl<'a> SyntaxMapping<'a> {
}
pub fn insert(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> {
let matcher = make_glob_matcher(from)?;
let matcher = make_glob_matcher(from, Case::Insensitive)?;
self.custom_mappings.push((matcher, to));
Ok(())
}
/// Like [`Self::insert`], but the glob pattern is matched case-sensitively.
pub fn insert_case_sensitive(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> {
let matcher = make_glob_matcher(from, Case::Sensitive)?;
self.custom_mappings.push((matcher, to));
Ok(())
}
@@ -261,4 +275,41 @@ mod tests {
Some(MappingTarget::MapTo("alpha"))
);
}
#[test]
fn case_sensitive_custom_mappings_work() {
let mut map = SyntaxMapping::new();
map.insert_case_sensitive("MY_SPECIAL_FILE", MappingTarget::MapTo("Python"))
.ok();
// Exact case matches
assert_eq!(
map.get_syntax_for("/path/to/MY_SPECIAL_FILE"),
Some(MappingTarget::MapTo("Python"))
);
// Different case should NOT match the case-sensitive rule
assert_eq!(map.get_syntax_for("/path/to/my_special_file"), None);
assert_eq!(map.get_syntax_for("/path/to/My_Special_File"), None);
}
#[test]
fn builtin_mappings_build_is_case_sensitive() {
let map = SyntaxMapping::new();
// "BUILD" (uppercase) should map to Python via case-sensitive builtin
assert_eq!(
map.get_syntax_for("/path/to/BUILD"),
Some(MappingTarget::MapTo("Python"))
);
// "build" (lowercase) should still map to MapToUnknown
assert_eq!(
map.get_syntax_for("/path/to/build"),
Some(MappingTarget::MapToUnknown)
);
// Mixed case should NOT match the Python rule
assert_eq!(
map.get_syntax_for("/path/to/Build"),
Some(MappingTarget::MapToUnknown)
);
}
}
+5 -5
View File
@@ -3,7 +3,7 @@ use std::env;
use globset::GlobMatcher;
use once_cell::sync::Lazy;
use crate::syntax_mapping::{make_glob_matcher, MappingTarget};
use crate::syntax_mapping::{make_glob_matcher, Case, MappingTarget};
// Static syntax mappings generated from /src/syntax_mapping/builtins/ by the
// build script (/build/syntax_mapping.rs).
@@ -53,8 +53,8 @@ include!(concat!(
/// A failure to compile is a fatal error.
///
/// Used internally by `Lazy<Option<GlobMatcher>>`'s lazy evaluation closure.
fn build_matcher_fixed(from: &str) -> GlobMatcher {
make_glob_matcher(from).expect("A builtin fixed glob matcher failed to compile")
fn build_matcher_fixed(from: &str, case: Case) -> GlobMatcher {
make_glob_matcher(from, case).expect("A builtin fixed glob matcher failed to compile")
}
/// Join a list of matcher segments to create a glob string, replacing all
@@ -64,7 +64,7 @@ fn build_matcher_fixed(from: &str) -> GlobMatcher {
/// to compile.
///
/// Used internally by `Lazy<Option<GlobMatcher>>`'s lazy evaluation closure.
fn build_matcher_dynamic(segs: &[MatcherSegment]) -> Option<GlobMatcher> {
fn build_matcher_dynamic(segs: &[MatcherSegment], case: Case) -> Option<GlobMatcher> {
// join segments
let mut buf = String::new();
for seg in segs {
@@ -77,7 +77,7 @@ fn build_matcher_dynamic(segs: &[MatcherSegment]) -> Option<GlobMatcher> {
}
}
// compile glob matcher
let matcher = make_glob_matcher(&buf).ok()?;
let matcher = make_glob_matcher(&buf, case).ok()?;
Some(matcher)
}
+12 -5
View File
@@ -20,12 +20,10 @@ syntax mappings defined by all TOML files, and embed them into the binary.
## File syntax
Each TOML file should contain a single section named `mappings`, with each of
its keys being a language identifier (first column of `bat -L`; also referred to
as "target").
Each TOML file should contain a single section named `mappings`, with each of its keys being a language
identifier (first column of `bat -L`; also referred to as "target").
The value of each key should be an array of strings, with each item being a glob
matcher. We will call each of these items a "rule".
The value of each key should be an array of "rules". The rules are expected to be objects with a `glob` string and a `case_sensitive` boolean. For simplification, a rule can be just a glob string, which is shorthand for the default case insensitive mode.
For example, if `foo-application` uses both TOML and YAML configuration files,
we could write something like this:
@@ -98,6 +96,15 @@ like this:
]
```
### Case sensitivity
By default, all glob patterns are matched case-insensitively. To match a pattern case-sensitively, use the object form of the rule with the `case_sensitive` option:
```toml
[mappings]
"Python" = [{ glob = "BUILD", case_sensitive = true }]
```
## Ordering
At compile time, all TOML files applicable to the target are processed in
@@ -0,0 +1,2 @@
[mappings]
"Python" = [{ glob = "BUILD", case_sensitive = true }]
@@ -1,2 +1,2 @@
[mappings]
"XML" = ["*.csproj", "*.vbproj", "*.props", "*.targets"]
"XML" = ["*.csproj", "*.vbproj", "*.props", "*.targets", "*.slnx"]
@@ -0,0 +1,3 @@
[mappings]
"INI" = [".boto", "**/gcloud/configurations/config_*"]
"Git Ignore" = [".gcloudignore"]
@@ -1,3 +1,3 @@
# JSON Lines is a simple variation of JSON #2535
[mappings]
"JSON" = ["*.jsonl", "*.jsonc", "*.jsonld", "*.geojson", "*.ndjson"]
"JSON" = ["*.jsonl", "*.jsonc", "*.jsonld", "*.geojson", "*.ndjson", "*.code-workspace"]
@@ -0,0 +1,2 @@
[mappings]
"Makefile" = ["justfile", "Justfile", ".justfile", "*.justfile"]
@@ -1,2 +1,2 @@
[mappings]
"SSH Config" = ["**/.ssh/config"]
"SSH Config" = ["**/.ssh/**/config"]
@@ -0,0 +1,2 @@
[mappings]
"Git Ignore" = [".?*ignore"]
@@ -0,0 +1,2 @@
[mappings]
"INI" = ["/etc/dnf/dnf.conf", "/etc/yum.repos.d/*.repo"]
@@ -0,0 +1,2 @@
[mappings]
"INI" = ["**/ghostty/**/*.ghostty", "**/ghostty/themes/*"]
+203 -13
View File
@@ -386,12 +386,23 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
}
fn next_text(&mut self) -> Option<EscapeSequenceOffsets> {
self.chars_take_while(|c| c != '\x1B')
self.chars_take_while(|c| !is_sequence_introducer(c))
.map(|(start, end)| EscapeSequenceOffsets::Text { start, end })
}
fn next_sequence(&mut self) -> Option<EscapeSequenceOffsets> {
let (start_sequence, c) = self.chars.next().expect("to not be finished");
// Handle 8-bit C1 introducers as their 7-bit `ESC <x>` equivalents.
match c {
'\u{9B}' => return self.next_csi_body(start_sequence),
'\u{9D}' => return self.next_osc_body(start_sequence),
'\u{90}' | '\u{98}' | '\u{9E}' | '\u{9F}' => {
return self.next_string_terminated_body(start_sequence)
}
_ => {}
}
match self.chars.peek() {
None => Some(EscapeSequenceOffsets::Unknown {
start: start_sequence,
@@ -400,12 +411,24 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
Some((_, ']')) => self.next_osc(start_sequence),
Some((_, '[')) => self.next_csi(start_sequence),
Some((i, c)) => match c {
// 7-bit DCS/SOS/PM/APC: `ESC P/X/^/_` introduces a string body.
Some((_, 'P' | 'X' | '^' | '_')) => {
self.chars.next();
self.next_string_terminated_body(start_sequence)
}
Some(&(i, c)) => match c {
'\x20'..='\x2F' => self.next_nf(start_sequence),
c => Some(EscapeSequenceOffsets::Unknown {
start: start_sequence,
end: i + c.len_utf8(),
}),
c => {
// Single-byte ESC sequence (RIS, DECSC/DECRC, keypad, VT52 etc.).
let end = match self.chars.next() {
Some((j, fc)) => j + fc.len_utf8(),
None => i + c.len_utf8(),
};
Some(EscapeSequenceOffsets::Unknown {
start: start_sequence,
end,
})
}
},
}
}
@@ -413,12 +436,27 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
fn next_osc(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
let (osc_open_index, osc_open_char) = self.chars.next().expect("to not be finished");
debug_assert_eq!(osc_open_char, ']');
let start_command = osc_open_index + osc_open_char.len_utf8();
Some(self.read_osc_body(start_sequence, start_command))
}
/// OSC body parser entered after the 8-bit introducer U+009D was consumed.
fn next_osc_body(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
let start_command = start_sequence + '\u{9D}'.len_utf8();
Some(self.read_osc_body(start_sequence, start_command))
}
fn read_osc_body(
&mut self,
start_sequence: usize,
start_command: usize,
) -> EscapeSequenceOffsets {
let mut start_terminator: usize;
let mut end_sequence: usize;
loop {
match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B')) {
// ST is BEL, ESC `\\`, or U+009C.
match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B' | '\u{9C}')) {
None => {
start_terminator = self.text.len();
end_sequence = start_terminator;
@@ -437,6 +475,11 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
break;
}
Some((ti, '\u{9C}')) => {
end_sequence = ti + '\u{9C}'.len_utf8();
break;
}
Some((ti, '\x1B')) => {
match self.chars.next() {
Some((i, '\\')) => {
@@ -466,20 +509,82 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
}
}
Some(EscapeSequenceOffsets::OSC {
EscapeSequenceOffsets::OSC {
start_sequence,
start_command: osc_open_index + osc_open_char.len_utf8(),
start_command,
start_terminator,
end: end_sequence,
}
}
/// DCS/SOS/PM/APC body parser. Emitted as `Unknown` so the body is stripped.
fn next_string_terminated_body(
&mut self,
start_sequence: usize,
) -> Option<EscapeSequenceOffsets> {
let mut end_sequence: usize;
loop {
match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B' | '\u{9C}')) {
None => {
end_sequence = self.text.len();
break;
}
Some((_, end)) => {
end_sequence = end;
}
}
match self.chars.next() {
Some((ti, '\x07')) => {
end_sequence = ti + '\x07'.len_utf8();
break;
}
Some((ti, '\u{9C}')) => {
end_sequence = ti + '\u{9C}'.len_utf8();
break;
}
Some((ti, '\x1B')) => match self.chars.next() {
Some((i, '\\')) => {
end_sequence = i + '\\'.len_utf8();
break;
}
None => {
end_sequence = ti + '\x1B'.len_utf8();
break;
}
_ => {}
},
None => break,
Some((_, tc)) => {
panic!("this should not be reached: char {tc:?}")
}
}
}
Some(EscapeSequenceOffsets::Unknown {
start: start_sequence,
end: end_sequence,
})
}
fn next_csi(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
let (csi_open_index, csi_open_char) = self.chars.next().expect("to not be finished");
debug_assert_eq!(csi_open_char, '[');
Some(self.read_csi_body(start_sequence, csi_open_index + csi_open_char.len_utf8()))
}
let start_parameters: usize = csi_open_index + csi_open_char.len_utf8();
/// CSI body parser entered after the 8-bit introducer U+009B was consumed.
fn next_csi_body(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
let start_parameters = start_sequence + '\u{9B}'.len_utf8();
Some(self.read_csi_body(start_sequence, start_parameters))
}
fn read_csi_body(
&mut self,
start_sequence: usize,
start_parameters: usize,
) -> EscapeSequenceOffsets {
// Keep iterating while within the range of `0x30-0x3F`.
let mut start_intermediates: usize = start_parameters;
if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x30'..='\x3F')) {
@@ -498,13 +603,13 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
Some((i, c)) => i + c.len_utf8(),
};
Some(EscapeSequenceOffsets::CSI {
EscapeSequenceOffsets::CSI {
start_sequence,
start_parameters,
start_intermediates,
start_final_byte,
end: end_of_sequence,
})
}
}
fn next_nf(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
@@ -543,13 +648,25 @@ impl Iterator for EscapeSequenceOffsetsIterator<'_> {
type Item = EscapeSequenceOffsets;
fn next(&mut self) -> Option<Self::Item> {
match self.chars.peek() {
Some((_, '\x1B')) => self.next_sequence(),
Some((_, c)) if is_sequence_introducer(*c) => self.next_sequence(),
Some((_, _)) => self.next_text(),
None => None,
}
}
}
/// True for ESC and the 8-bit C1 sequence introducers (DCS/SOS/CSI/OSC/PM/APC).
#[inline]
fn is_sequence_introducer(c: char) -> bool {
if (c as u32) < 0x80 {
return c == '\x1B';
}
matches!(
c,
'\u{90}' | '\u{98}' | '\u{9B}' | '\u{9D}' | '\u{9E}' | '\u{9F}'
)
}
/// An iterator over ANSI/VT escape sequences within a string.
///
/// ## Example
@@ -717,6 +834,79 @@ mod tests {
);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_8bit_csi() {
let mut iter = EscapeSequenceOffsetsIterator::new("\u{9B}31m");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::CSI {
start_sequence: 0,
start_parameters: 2,
start_intermediates: 4,
start_final_byte: 4,
end: 5,
})
);
assert_eq!(iter.next(), None);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_8bit_osc_with_8bit_st() {
let mut iter = EscapeSequenceOffsetsIterator::new("\u{9D}0;title\u{9C}");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::OSC {
start_sequence: 0,
start_command: 2,
start_terminator: 9,
end: 11,
})
);
assert_eq!(iter.next(), None);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_7bit_dcs_consumes_body() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1BP1;0;|payload\x1B\\rest");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Unknown { start: 0, end: 16 })
);
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Text { start: 16, end: 20 })
);
assert_eq!(iter.next(), None);
}
#[test]
fn test_escape_sequence_offsets_iterator_8bit_dcs_consumes_body() {
let mut iter = EscapeSequenceOffsetsIterator::new("\u{90}body\u{9C}rest");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Unknown { start: 0, end: 8 })
);
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Text { start: 8, end: 12 })
);
}
#[test]
fn test_escape_sequence_offsets_iterator_truncated_dcs_consumes_to_eof() {
// Unterminated DCS must still be consumed, not emitted as `Text`.
let input = "\x1BPno-terminator";
let mut iter = EscapeSequenceOffsetsIterator::new(input);
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Unknown {
start: 0,
end: input.len(),
})
);
assert_eq!(iter.next(), None);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_csi() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[m");
+1
View File
@@ -1,6 +1,7 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WrappingMode {
Character,
Word,
// The bool specifies whether wrapping has been explicitly disabled by the user via --wrap=never
NoWrapping(bool),
}
-1
View File
@@ -34,7 +34,6 @@ fn all_themes_are_present() {
"Solarized (light)",
"Sublime Snazzy",
"TwoDark",
"Visual Studio Dark+",
"ansi",
"base16",
"base16-256",
+30
View File
@@ -0,0 +1,30 @@
GIT-COMMIT(1) Git Manual GIT-COMMIT(1)
NNAAMMEE
git-commit - Record changes to the repository
SSYYNNOOPPSSIISS
ggiitt ccoommmmiitt [--aa | ----iinntteerraaccttiivvee | ----ppaattcchh] [--ss] [--vv] [--uu[_<_m_o_d_e_>]] [----aammeenndd]
[----ddrryy--rruunn] [(--cc | --CC | ----ssqquuaasshh) _<_c_o_m_m_i_t_> | ----ffiixxuupp [(aammeenndd|rreewwoorrdd)::]_<_c_o_m_m_i_t_>]
[--FF _<_f_i_l_e_> | --mm _<_m_s_g_>] [----rreesseett--aauutthhoorr] [----aallllooww--eemmppttyy]
[----aallllooww--eemmppttyy--mmeessssaaggee] [----nnoo--vveerriiffyy] [--ee] [----aauutthhoorr==_<_a_u_t_h_o_r_>]
[----ddaattee==_<_d_a_t_e_>] [----cclleeaannuupp==_<_m_o_d_e_>] [----[nnoo--]ssttaattuuss]
[--ii | --oo] [----ppaatthhssppeecc--ffrroomm--ffiillee==_<_f_i_l_e_> [----ppaatthhssppeecc--ffiillee--nnuull]]
[(----ttrraaiilleerr _<_t_o_k_e_n_>[(==|::)_<_v_a_l_u_e_>])...] [--SS[_<_k_e_y_i_d_>]]
[----] [_<_p_a_t_h_s_p_e_c_>...]
DDEESSCCRRIIPPTTIIOONN
Create a new commit containing the current contents of the index and
the given log message describing the changes. The new commit is a
direct child of HEAD, usually the tip of the current branch, and the
branch is updated to point to it (unless no branch is associated with
the working tree, in which case HHEEAADD is "detached" as described in ggiitt--
cchheecckkoouutt(1)).
The content to be committed can be specified in several ways:
1. by using ggiitt--aadddd(1) to incrementally "add" changes to the index
before using the ccoommmmiitt command (Note: even modified files must be
"added");
2. by using ggiitt--rrmm(1) to remove files from the working tree and the
+1
View File
@@ -0,0 +1 @@
BBold tteexxtt and _u_n_d_e_r_l_i_n_e
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
NAME="${0##*/}"
for i in {1..3}; do
echo "hello $NAME $i"
done
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/expect -f
set timeout 30
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env tclsh
puts "Hello from tclsh"
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/wish
button .b -text "Click"
+3
View File
@@ -0,0 +1,3 @@
The quick brown fox jumps over the lazy dog and then runs away
superlongwordthatdefinitelyexceedstheterminalwidthandshouldfallbacktocharacterwrapping
short words here
File diff suppressed because it is too large Load Diff
Vendored Executable → Regular
+12 -10
View File
@@ -4,6 +4,7 @@ import itertools
import subprocess
import pathlib
import shutil
from typing import Iterable
def generate_snapshots():
@@ -19,22 +20,23 @@ def generate_snapshots():
def generate_style_snapshot(style):
generate_snapshot(style.replace(",", "_"), "--style={}".format(style))
generate_snapshot(style.replace(",", "_"), ["--style={}".format(style)])
def generate_snapshot(name, arguments):
command = "cargo run -- --paging=never --color=never --decorations=always "
command += "{args} sample.rs > output/{name}.snapshot.txt".format(
name=name,
args=arguments
)
def generate_snapshot(name: str, arguments: Iterable[str]):
output_file = "output/{name}.snapshot.txt".format(name=name)
command = [
"cargo", "run", "--", "--paging=never", "--color=never",
"--decorations=always", *arguments, "sample.rs"
]
print("generating snapshot for {}".format(name))
subprocess.call(command, shell=True)
with open(output_file, "w") as f:
subprocess.call(command, stdout=f)
def build_bat():
print("building bat")
subprocess.call("cargo build", cwd="../..", shell=True)
subprocess.call(["cargo", "build"], cwd="../..")
def prepare_output_dir():
@@ -49,7 +51,7 @@ def modify_sample_file():
def undo_sample_file_modification():
print("undoing sample.rs modifications")
subprocess.call("git checkout -- sample.rs", shell=True)
subprocess.call(["git", "checkout", "--", "sample.rs"])
build_bat()
+61
View File
@@ -0,0 +1,61 @@
IDENTIFICATION DIVISION.
PROGRAM-ID. PAYROLL-CALC.
DATA DIVISION.
WORKING-STORAGE SECTION.
 01 EMPLOYEE-DETAILS.
 05 EMPLOYEE-NAME PIC X(30).
 05 HOURS-WORKED PIC 99V9.
 05 HOURLY-RATE PIC 99V99.
 01 PAY-CALCULATIONS.
 05 GROSS-PAY PIC 9(5)V99.
 05 OVERTIME-HOURS PIC 99V9.
 05 OVERTIME-PAY PIC 9(5)V99.
 05 TAX-RATE PIC V99 VALUE 0.10.
 05 TAX-AMOUNT PIC 9(5)V99.
 05 NET-PAY PIC 9(5)V99.
PROCEDURE DIVISION.
MAIN-LOGIC.
 DISPLAY "--- COBOL Payroll Calculator ---".
 DISPLAY "Enter Employee Name: ".
 ACCEPT EMPLOYEE-NAME.
 DISPLAY "Enter Hours Worked (e.g., 40.5): ".
 ACCEPT HOURS-WORKED.
 DISPLAY "Enter Hourly Rate (e.g., 15.75): ".
 ACCEPT HOURLY-RATE.
 PERFORM CALCULATE-GROSS-PAY.
 PERFORM CALCULATE-TAX.
 PERFORM CALCULATE-NET-PAY.
 PERFORM DISPLAY-RESULTS.
 STOP RUN.
CALCULATE-GROSS-PAY.
 IF HOURS-WORKED > 40 THEN
 COMPUTE OVERTIME-HOURS = HOURS-WORKED - 40
 COMPUTE OVERTIME-PAY = OVERTIME-HOURS * HOURLY-RATE * 1.5
 COMPUTE GROSS-PAY = (40 * HOURLY-RATE) + OVERTIME-PAY
 ELSE
 COMPUTE GROSS-PAY = HOURS-WORKED * HOURLY-RATE
 END-IF.
CALCULATE-TAX.
 COMPUTE TAX-AMOUNT = GROSS-PAY * TAX-RATE.
CALCULATE-NET-PAY.
 COMPUTE NET-PAY = GROSS-PAY - TAX-AMOUNT.
DISPLAY-RESULTS.
 DISPLAY "----------------------------------".
 DISPLAY "Employee Name: " EMPLOYEE-NAME.
 DISPLAY "Hours Worked: " HOURS-WORKED.
 DISPLAY "Hourly Rate: " HOURLY-RATE.
 DISPLAY "Gross Pay: " GROSS-PAY.
 DISPLAY "Tax (10%): " TAX-AMOUNT.
 DISPLAY "Net Pay: " NET-PAY.
 DISPLAY "----------------------------------".
+61
View File
@@ -0,0 +1,61 @@
IDENTIFICATION DIVISION.
PROGRAM-ID. PAYROLL-CALC.
DATA DIVISION.
WORKING-STORAGE SECTION.
 01 EMPLOYEE-DETAILS.
 05 EMPLOYEE-NAME PIC X(30).
 05 HOURS-WORKED PIC 99V9.
 05 HOURLY-RATE PIC 99V99.
 01 PAY-CALCULATIONS.
 05 GROSS-PAY PIC 9(5)V99.
 05 OVERTIME-HOURS PIC 99V9.
 05 OVERTIME-PAY PIC 9(5)V99.
 05 TAX-RATE PIC V99 VALUE 0.10.
 05 TAX-AMOUNT PIC 9(5)V99.
 05 NET-PAY PIC 9(5)V99.
PROCEDURE DIVISION.
MAIN-LOGIC.
 DISPLAY "--- COBOL Payroll Calculator ---".
 DISPLAY "Enter Employee Name: ".
 ACCEPT EMPLOYEE-NAME.
 DISPLAY "Enter Hours Worked (e.g., 40.5): ".
 ACCEPT HOURS-WORKED.
 DISPLAY "Enter Hourly Rate (e.g., 15.75): ".
 ACCEPT HOURLY-RATE.
 PERFORM CALCULATE-GROSS-PAY.
 PERFORM CALCULATE-TAX.
 PERFORM CALCULATE-NET-PAY.
 PERFORM DISPLAY-RESULTS.
 STOP RUN.
CALCULATE-GROSS-PAY.
 IF HOURS-WORKED > 40 THEN
 COMPUTE OVERTIME-HOURS = HOURS-WORKED - 40
 COMPUTE OVERTIME-PAY = OVERTIME-HOURS * HOURLY-RATE * 1.5
 COMPUTE GROSS-PAY = (40 * HOURLY-RATE) + OVERTIME-PAY
 ELSE
 COMPUTE GROSS-PAY = HOURS-WORKED * HOURLY-RATE
 END-IF.
CALCULATE-TAX.
 COMPUTE TAX-AMOUNT = GROSS-PAY * TAX-RATE.
CALCULATE-NET-PAY.
 COMPUTE NET-PAY = GROSS-PAY - TAX-AMOUNT.
DISPLAY-RESULTS.
 DISPLAY "----------------------------------".
 DISPLAY "Employee Name: " EMPLOYEE-NAME.
 DISPLAY "Hours Worked: " HOURS-WORKED.
 DISPLAY "Hourly Rate: " HOURLY-RATE.
 DISPLAY "Gross Pay: " GROSS-PAY.
 DISPLAY "Tax (10%): " TAX-AMOUNT.
 DISPLAY "Net Pay: " NET-PAY.
 DISPLAY "----------------------------------".
+5 -5
View File
@@ -1,7 +1,7 @@
first,last,address,city,zip
John,Doe,120 any st.,"Anytown, WW",08123
John,Doe,120 any st.,"Anytown, WW",08123
a,b
1,"ha 
""ha"" 
ha",120 any st.,"Anytown, WW",08123
3,4,120 any st.,"Anytown, WW",08123
1,"ha 
""ha"" 
ha",120 any st.,"Anytown, WW",08123
3,4,120 any st.,"Anytown, WW",08123
Can't render this file because it contains an unexpected character in line 2 and column 177.
+1 -1
View File
@@ -1,3 +1,3 @@
foo bar baz|;, test hello world tsv
1,2 1,7 2,7 a b c "hello again" tsv
";|," ;|, baz test "hello world" tsv
";|," ;|, baz test "hello world" tsv
Can't render this file because it contains an unexpected character in line 2 and column 218.
+25
View File
@@ -0,0 +1,25 @@
(logging) {
 log {
 output file /var/log/caddy.log
 }
}
# a comment
localhost:8080, example.com {
 root * /var/www/site
 file_server
 reverse_proxy /.well-known/matrix/* localhost:8008 {
 header_up Host {upstream_hostport}
 }
 import logging
}
www.example.com {
 redir https://example.com{uri} permanent
 import logging
}
status.example.com {
 reverse_proxy localhost:3002
 import logging
}
+52 -52
View File
@@ -1,85 +1,85 @@
import kotlin.math.*
import kotlin.math.*
data class Example(
 val name: String,
 val numbers: List<Int?>
data class Example(
 val name: String,
 val numbers: List<Int?>
)
fun interface JokeInterface {
 fun isFunny(): Boolean
fun interface JokeInterface {
 fun isFunny(): Boolean
}
abstract class AbstractJoke : JokeInterface {
 override fun isFunny() = false
 abstract fun content(): String
abstract class AbstractJoke : JokeInterface {
 override fun isFunny() = false
 abstract fun content(): String
}
class Joke : AbstractJoke() {
 override fun isFunny(): Boolean {
class Joke : AbstractJoke() {
 override fun isFunny(): Boolean {
 return true
 }
 override fun content(): String = "content of joke here, haha"
 }
 override fun content(): String = "content of joke here, haha"
}
class DelegatedJoke(val joke: Joke) : JokeInterface by joke {
 val number: Long = 123L
class DelegatedJoke(val joke: Joke) : JokeInterface by joke {
 val number: Long = 123L
 companion object {
 const val someConstant = "some constant text"
 }
 companion object {
 const val someConstant = "some constant text"
 }
}
object SomeSingleton
object SomeSingleton
sealed class Shape {
 abstract fun area(): Double
sealed class Shape {
 abstract fun area(): Double
}
data class Square(val sideLength: Double) : Shape() {
 override fun area(): Double = sideLength.pow(2)
data class Square(val sideLength: Double) : Shape() {
 override fun area(): Double = sideLength.pow(2)
}
object Point : Shape() {
 override fun area() = .0
object Point : Shape() {
 override fun area() = .0
}
class Circle(val radius: Double) : Shape() {
 override fun area(): Double {
 return PI * radius * radius
 }
class Circle(val radius: Double) : Shape() {
 override fun area(): Double {
 return PI * radius * radius
 }
}
fun String.extensionMethod() = "test"
fun String.extensionMethod() = "test"
fun main() {
 val name = """
fun main() {
 val name = """
 multiline
 string
 
 some numbers: 123123 42
 """.trimIndent()
 val example = Example(name = name, numbers = listOf(512, 42, null, -1))
 """.trimIndent()
 val example = Example(name = name, numbers = listOf(512, 42, null, -1))
 example.numbers
 .filterNotNull()
 .forEach { println(it) }
 example.numbers
 .filterNotNull()
 .forEach { println(it) }
 setOf(Joke(), DelegatedJoke(Joke()).joke)
 .filter(JokeInterface::isFunny)
 .map(AbstractJoke::content)
 .forEachIndexed { index: Int, joke ->
 println("I heard a funny joke(#${index + 1}): $joke")
 }
 setOf(Joke(), DelegatedJoke(Joke()).joke)
 .filter(JokeInterface::isFunny)
 .map(AbstractJoke::content)
 .forEachIndexed { index: Int, joke ->
 println("I heard a funny joke(#${index + 1}): $joke")
 }
 listOf(Square(12.3), Point, Circle(5.2))
 .associateWith(Shape::area)
 .toList()
 .sortedBy { it.second }
 .forEach {
 println("${it.first}: ${it.second}")
 }
 listOf(Square(12.3), Point, Circle(5.2))
 .associateWith(Shape::area)
 .toList()
 .sortedBy { it.second }
 .forEach {
 println("${it.first}: ${it.second}")
 }
 println("some string".extensionMethod())
 println("some string".extensionMethod())
 require(SomeSingleton::class.simpleName == "SomeSingletonName") { "something does not seem right..." }
 require(SomeSingleton::class.simpleName == "SomeSingletonName") { "something does not seem right..." }
}
+399
View File
@@ -0,0 +1,399 @@
UWSM(1) General Commands Manual UWSM(1)
NAME
 UWSM - Universal Wayland Session Manager.
SYNOPSIS
 uwsm [-h|-v] {subcommand} [options ...]
DESCRIPTION
 Launches arbitrary wayland compositor via a set of systemd user units to provide graphical user
 session with environment management, XDG autostart support, clean shutdown. Provides helpers
 for launching applications as scopes or services.
SUBCOMMANDS
 select Select default compositor Entry.
 start Start compositor and graphical session.
 finalize Send compositor-set variables and unit startup notification to systemd user manager.
 stop Stop graphical session and compositor.
 app Application unit launcher (with Desktop Entry support).
 check Perform state checks (for scripting and info).
 aux Technical functions for use inside units.
 See corresponding SUBCOMMANDS subsections below for further info.
 Help for each subcommand is accessible by running "uwsm {subcommand} -h".
CONFIGURATION
 Files
 In XDG config hierarchy:
 uwsm/env
 uwsm/env.d/*
 uwsm/env-${compositor}
 uwsm/env-${compositor}.d/* Environment (shell) to be sourced for the graphical session.
 Sourced from directories of increasing priority, in each directory
 common file is sourced first, then suffixed files in the order of
 items listed in XDG_CURRENT_SESSION var (lowercased).
 uwsm/default-id Stores Desktop Entry ID of default compositor.
 Fallback is also extended into the system part of XDG data hierarchy, this can be used for dis
 tro level defaults.
 Environment vars
 UWSM_UNIT_RUNG (run|home)
 Which rung of systemd/user/ hierarchy to manage generated unit
 and drop-in files in: $XDG_RUNTIME_DIR or $XDG_CONFIG_HOME.
 UWSM_TWEAKS (boolean value)
 Set to False to remove and not generate tweak drop-ins for
 other software.
 UWSM_FINALIZE_VARNAMES (whitespace-separated names of env vars)
 Additional variables for "uwsm finalize".
 UWSM_WAIT_VARNAMES (whitespace-separated names of env vars)
 Variables to wait for in activation environment before proceed
 ing to graphical session (in addition to WAYLAND_DISPLAY).
 UWSM_WAIT_VARNAMES_TIMEOUT (int value)
 Seconds to wait for variables to appear in activation environ
 ment. Essentially, startup timeout (default: 10).
 UWSM_WAIT_VARNAMES_SETTLETIME (float value)
 Seconds to pause after all expected vars found in activation
 environment (default: 0.2).
 UWSM_APP_UNIT_TYPE (scope|service)
 Default unit type for launching apps (default: scope).
 UWSM_SILENT_START (int or boolean value)
 True or 1 to inhibit stdout messages from "uwsm start". 2 to
 also inhibit warnings.
 DEBUG (int or boolean value)
 True or positive number to dump debug info to stderr.
OPERATION OVERVIEW
 Login Sequence Integration
 uwsm can be launched by using conditional exec in shell profile to replace login shell (see
 Shell Profile Integration section).
 Alternatively "uwsm start ..." command can be put into wayland session's Desktop Entry to be
 launched by a display manager (see Use Inside Desktop Entry section).
 Compositor Selection
 uwsm can run arbitrary compositor command line or a Desktop Entry by ID (specifying Action ID
 is also supported).
 Desktop Entry can also be selected via a whiptail menu (see select subcommand section).
 Startup
 See start subcommand section for command syntax.
 UWSM uses a set of units bound to standard user session targets:
 • wayland-session-pre@.target (bound to graphical-session-pre.target)
 • wayland-wm-env@.service (environment preloader service)
 • wayland-session@.target (bound to graphical-session.target)
 • wayland-wm@.service (service for the selected compositor)
 • wayland-session-xdg-autostart@.target (bound to xdg-desktop-autostart.target)
 • wayland-session-envelope@.target (lives through entire lifecycle)
 • wayland-session-shutdown.target (conflicts with targets above for shutdown)
 • wayland-session-bindpid@.service (PID-tracking session killswitch)
 • wayland-session-waitenv.service (delays graphical session until vars appear)
 Compositor ID (Desktop Entry ID or executable name) becomes the specifier for all templated
 units.
 At the stage of graphical-session-pre.target, the environment saved from "uwsm start" context
 is loaded (or POSIX shell profile is sourced), uwsm environment files are sourced. The delta is
 exported to the systemd and D-Bus activation environments by the environment preloader service
 and is marked for cleanup at shutdown stage. Preloader shell context for convenience has
 IN_UWSM_ENV_PRELOADER var set to true.
 At the stage of graphical-session.target (before it) the main compositor unit wayland-
 wm@${ID}.service and wayland-session-waitenv.service are started.
 Compositor should at least put WAYLAND_DISPLAY variable to systemd activation environment. This
 will trigger uwsm's automatic finalization logic. Without WAYLAND_DISPLAY in activation envi
 ronment startup will timeout in 10 seconds.
 Manual finalization is possible by running "uwsm finalize" (see finalize subcommand section),
 also in combination with tweaking UWSM_WAIT_VARNAMES and UWSM_WAIT_VARNAMES_SETTLETIME vars
 (see Environment vars section).
 Successful activation of compositor unit and existence of WAYLAND_DISPLAY in activation envi
 ronment will allow graphical-session.target to be declared reached.
 Finally, xdg-desktop-autostart.target is activated.
 Inside session
 It is highly recommended to configure the compositor or app launcher to launch apps as scopes
 or services in special user session slices (app.slice, background.slice, session.slice). uwsm
 provides custom nested slices for apps to live in and be terminated on session end:
 • app-graphical.slice
 • background-graphical.slice
 • session-graphical.slice
 A helper app subcommand is provided to handle all the systemd-run invocations for you (see app
 subcommand section).
 The compositor is launched in session.slice by default (as recommended by systemd.special(7)).
 Shutdown
 Can be initiated by either:
 • running uwsm stop
 • stopping wayland-wm@*.service or wayland-session-envelope@*.target
 • starting wayland-session-shutdown.target
 Systemd stops all user units in reverse, as it usually does. During deactivation of graphical-
 session-pre.target, the environment preloader service cleans activation environments by unset
 ting all variables that were marked for removal during startup and finalization stages.
 Do not use compositor's native exit mechanism or kill its process directly.
SUBCOMMANDS
 select
 Selects default wayland session compositor Desktop Entry.
 uwsm select
 Invokes a whiptail menu to select default session among Desktop Entries in wayland-sessions XDG
 data hierarchy. Writes to ${XDG_CONFIG_HOME}/uwsm/default-id. Nothing else is done. Returns 1
 if selection is cancelled. Can be used for scripting launch condition in shell profile.
 check
 Performs tests, returns 0 on success, 1 on failure.
 is-active:
 uwsm check is-active [-h] [-v] [compositor]
 -v show additional info
 compositor check for specific compositor
 Checks if unit of specific compositor or graphical-session*.target in general is in active or
 activating state.
 may-start:
 uwsm check may-start [-h] [-g [S]] [-v|-q] [N ...]
 N ... allowed VT numbers (default: 1)
 -g S wait S seconds for graphical.target in queue (default: 60; 0 or less disables
 check).
 -i do not check for login shell
 -r do not check for local session (allow remote session)
 -v show all failed tests
 -q be quiet
 Checks whether it is OK to launch a wayland session via the following conditions:
 • DBUS_SESSION_BUS_ADDRESS is set
 • Running from login shell
 • System is at graphical.target
 • User graphical-session*.target units are not yet active
 • Foreground VT is among allowed (default: 1)
 • Login session's VT is matching
 start
 Generates units for given compositor command line or Desktop Entry and starts them.
 uwsm start [-h] [-D name[:name...]] [-a|-e] [-N Name] [-C Comment] [-U {run|home}] [-t]
 [-o] [-n] -- compositor [args ...]
 -F Hardcode mode, always write command line to unit drop-ins and use full
 paths.
 -D name[:name...] Names to fill XDG_CURRENT_DESKTOP with (:-separated). Existing var con
 tent is a starting point if no active session is running.
 -a Append desktop names set by -D to other sources (default).
 -e Use desktop names set by -D exclusively, discard other sources.
 -N Name Fancy name for compositor (filled from Desktop Entry by default).
 -C Comment Fancy description for compositor (filled from Desktop Entry by de
 fault).
 -U {run|home} Select rung for generated unit files: run: $XDG_RUNTIME_DIR/sys
 temd/user (default), or home: $XDG_CONFIG_HOME/systemd/user. Permanent
 destination will save some time by removing need for reloading systemd.
 Managed files from other rung will be removed. Can be preset with
 UWSM_UNIT_RUNG environment var.
 -t Do not generate (and remove) tweak unit files. Can be preset with
 UWSM_TWEAKS=false environment var.
 -T Generate tweak unit files for other software. This is default behavior.
 -g S Wait for S seconds for system graphical.target in queue and warn if
 timed out or not in queue (default: 60, negative to disable).
 -G S Wait for S seconds for system graphical.target in queue and abort if
 timed out or not in queue (overrides -g, default: -1, (disabled)).
 -o Only generate units, but do not start.
 -n Dry run, do not write or start anything.
 The first argument of the compositor command line acts as an ID and should be either one of:
 • Executable name
 • Desktop Entry ID (optionally with ":"-delimited action ID)
 • Special value:
 • select - invoke menu to select compositor.
 • default - run previously selected compositor (or select if no selection was saved).
 If given as path, hardcode mode will be used implicitly.
 Always use "--" to disambiguate dashed arguments intended for compositor itself.
 After units are (re)generated, wayland-session-bindpid@${PID}.service is started, to track the
 PID of invoking uwsm, then uwsm process replaces itself with systemctl execution that starts
 wayland-wm@${ID}.service and waits for it to finish.
 In order to complete the startup sequence, the compositor has to put WAYLAND_DISPLAY into the
 systemd activation environment. This can be done explicitly by making compositor run "uwsm fi
 nalize" command (see the next subsection).
 finalize
 For running by a compositor on startup.
 uwsm finalize [-h] [VAR_NAME ...]
 Exports WAYLAND_DISPLAY, DISPLAY and any defined vars mentioned by names in arguments or in
 UWSM_FINALIZE_VARNAMES variable (whitespace-separated). Then sends startup notification for the
 unit to systemd user manager.
 This is required if compositor itself does not put WAYLAND_DISPLAY to systemd activation envi
 ronment, otherwise wayland-session@.service unit or a dedicated wayland-session-waitenv.service
 unit will terminate due to startup timeout.
 UWSM_FINALIZE_VARNAMES variable can be prefilled by plugins.
 Direct assignment as VAR_NAME=value is also possible, but recommended only for creating flags
 for UWSM_WAIT_VARNAMES mechanism.
 stop
 Stops compositor and optionally removes generated units.
 uwsm stop [-h] [-r [compositor] [-U {run|home}] [-n]
 -r [compositor] Also remove units (all or only compositor-specific).
 -U {run|home} Select rung for generated unit files: run: $XDG_RUNTIME_DIR/systemd/user
 (default), or home: $XDG_CONFIG_HOME/systemd/user. Permanent destination
 will save some time by removing need for reloading systemd. Managed files
 from other rung will be removed. Can be preset with UWSM_UNIT_RUNG envi
 ronment var.
 -n Dry run, do not stop or remove anything.
 app
 Application-to-unit launcher with Desktop Entry support.
 uwsm app [-h] [-s {a,b,s,custom.slice}] [-t {scope,service}] [-a app_name] [-u unit_name]
 [-d unit_description] [-S ] [-T] -- application [args ...]
 -s {a,b,s,custom.slice} Slice selector (default: a):
 a - app-graphical.slice
 b - background-graphical.slice
 s - session-graphical.slice
 any slice by full name
 -t {scope,service} Type of unit to launch (default: scope, can be preset by
 UWSM_APP_UNIT_TYPE env var).
 -a app_name Override app name (a substring in unit name).
 -u unit_name Override the whole autogenerated unit name.
 -d unit_description Unit Description.
 -p Property=value Set additional unit property (option is repeatable).
 -S {out,err,both} Silence stdout, stderr, or both.
 -T Launch app in a terminal. Allows command to be empty to just
 launch a terminal.
 Application can be provided as a command with optional arguments, or a Desktop Entry ID, op
 tionally suffixed with ":"-delimited Action ID. If Desktop Entry is being launched, arguments
 should be compatible with it.
 Always use "--" to disambiguate dashed arguments intended for application itself.
 aux
 For use in systemd user services. Can only be called by systemd user manager.
 prepare-env Prepares environment (for use in ExecStart in wayland-wm-env@.service bound to
 wayland-session-pre@.target).
 cleanup-env Cleans up environment (for use ExecStop in in wayland-wm-env@.service bound to
 wayland-session-pre@.target).
 exec Executes a command with arguments or a desktop entry (for use in Exec in wayland-
 wm@.service bound to wayland-session@.target).
 app-daemon Daemon for faster app argument generation, used by uwsm-app client.
APP DAEMON
 Provided as wayland-wm-app-daemon.service to be started on-demand.
 Daemon receives app arguments from ${XDG_RUNTIME_DIR}/uwsm-app-daemon-in pipe. Resulting argu
 ments are formatted as shell code and written to ${XDG_RUNTIME_DIR}/uwsm-app-daemon-out pipe.
 Arguments are expected to be \0-delimited, leading \0 are stripped. One command is received per
 write+close.
 The first argument determines the behavior:
 • app the rest is processed the same as in "uwsm app"
 • ping just "pong" is returnedn
 • stop daemon is stoppedn
 Single commands are prepended with exec, iterated commands are assembled with trailing & each,
 followed by wait.
 The purpose of all this is to skip all the expensive Python startup and import routines that
 slow things down every time "uwsm app" is called. Instead the daemon does it once and then lis
 tens for requests, while a simple shell script may dump arguments to one pipe and run the code
 received from another via eval, which is much faster.
 The simplest script is:
 #!/bin/sh
 printf '0%s' app "$@" > "${XDG_RUNTIME_DIR}/uwsm-app-daemon-in"
 IFS='' read -r cmd < "${XDG_RUNTIME_DIR}/uwsm-app-daemon-out"
 eval "$cmd"
 Provided uwsm-app client script is a bit smarter: it can start the daemon, applies timeouts,
 and supports newlines in returned args.
SHELL PROFILE INTEGRATION
 To launch uwsm automatically on login, add one of constructs below (or similar) to shell pro
 file.
 This asks to select a compositor (or refuse and continue with login shell) when logged in on VT
 1:
 if uwsm check may-start && uwsm select; then
 exec systemd-cat -t uwsm_start uwsm start default
 fi
 This just starts a specific compositor depending on foreground VT:
 if uwsm check may-start 1; then
 exec systemd-cat -t uwsm_start uwsm start sway.desktop
 elif uwsm check may-start 2; then
 exec systemd-cat -t uwsm_start uwsm start labwc.desktop
 fi
 Using "uwsm check may-start" as a condition is essential, not only to prevent accidental
 startup attempts where they are not expected, but also since startup may involve sourcing shell
 profile, which might lead to nasty loops.
 See check subcommand section for info on may-start checker.
 exec allows uwsm to replace login shell in order to properly bind to user session and handle
 session termination.
 "systemd-cat -t uwsm_start" (optional) executes the command given to it (uwsm) with its stdout
 and stderr connected to the systemd journal, tagged with identifier "uwsm_start". See systemd-
 cat(1) for more options.
USE INSIDE DESKTOP ENTRY
 To launch uwsm from a display/login manager, "uwsm start" can be used inside Desktop Entries.
 Example /usr/local/share/wayland-sessions/my-compositor.desktop:
 [Desktop Entry]
 Name=My compositor (with UWSM)
 Comment=My cool compositor
 Exec=uwsm start -N "My compositor" -D mycompositor -C "My cool compositor" mywm
 DesktopNames=mycompositor
 Type=Application
 Things to keep in mind:
 • For consistency, command line arguments should mirror the keys of the entry
 • Command in Exec= should start with "uwsm start"
 • It should not point to itself (as a combination of Desktop Entry ID and Action ID)
 • It should not point to a Desktop Entry ID and Action ID that also uses uwsm
 Potentially such entries may be found and used by uwsm itself, i.e. in shell profile integra
 tion situation, or when launched manually. Following the principles above ensures uwsm will
 properly recognize itself and parse requested arguments inside the entry without any side ef
 fects.
SEE ALSO
 uwsm-plugins(3), systemd-run(1), systemd-cat(1), systemd.special(7)
 2026-02-14 UWSM(1)
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/expect -f
# Expect script detected via expect shebang
set timeout 30
spawn ssh user@host
expect "password:"
send "secret\r"
expect eof
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env tclsh
# Tcl script detected via tclsh shebang
puts "Hello from tclsh"
set x 42
if {$x > 0} {
 puts "positive"
}
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/wish
# Tk script detected via wish shebang
package require Tk
button .b -text "Click" -command {puts "clicked"}
pack .b
+7
View File
@@ -0,0 +1,7 @@
<Solution>
 <Folder Name="/Build/">
 <File Path="Directory.Build.props" />
 <File Path="projectname.targets" />
 </Folder>
 <Project Path="console.csproj" />
</Solution>
+61
View File
@@ -0,0 +1,61 @@
IDENTIFICATION DIVISION.
PROGRAM-ID. PAYROLL-CALC.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 EMPLOYEE-DETAILS.
05 EMPLOYEE-NAME PIC X(30).
05 HOURS-WORKED PIC 99V9.
05 HOURLY-RATE PIC 99V99.
01 PAY-CALCULATIONS.
05 GROSS-PAY PIC 9(5)V99.
05 OVERTIME-HOURS PIC 99V9.
05 OVERTIME-PAY PIC 9(5)V99.
05 TAX-RATE PIC V99 VALUE 0.10.
05 TAX-AMOUNT PIC 9(5)V99.
05 NET-PAY PIC 9(5)V99.
PROCEDURE DIVISION.
MAIN-LOGIC.
DISPLAY "--- COBOL Payroll Calculator ---".
DISPLAY "Enter Employee Name: ".
ACCEPT EMPLOYEE-NAME.
DISPLAY "Enter Hours Worked (e.g., 40.5): ".
ACCEPT HOURS-WORKED.
DISPLAY "Enter Hourly Rate (e.g., 15.75): ".
ACCEPT HOURLY-RATE.
PERFORM CALCULATE-GROSS-PAY.
PERFORM CALCULATE-TAX.
PERFORM CALCULATE-NET-PAY.
PERFORM DISPLAY-RESULTS.
STOP RUN.
CALCULATE-GROSS-PAY.
IF HOURS-WORKED > 40 THEN
COMPUTE OVERTIME-HOURS = HOURS-WORKED - 40
COMPUTE OVERTIME-PAY = OVERTIME-HOURS * HOURLY-RATE * 1.5
COMPUTE GROSS-PAY = (40 * HOURLY-RATE) + OVERTIME-PAY
ELSE
COMPUTE GROSS-PAY = HOURS-WORKED * HOURLY-RATE
END-IF.
CALCULATE-TAX.
COMPUTE TAX-AMOUNT = GROSS-PAY * TAX-RATE.
CALCULATE-NET-PAY.
COMPUTE NET-PAY = GROSS-PAY - TAX-AMOUNT.
DISPLAY-RESULTS.
DISPLAY "----------------------------------".
DISPLAY "Employee Name: " EMPLOYEE-NAME.
DISPLAY "Hours Worked: " HOURS-WORKED.
DISPLAY "Hourly Rate: " HOURLY-RATE.
DISPLAY "Gross Pay: " GROSS-PAY.
DISPLAY "Tax (10%): " TAX-AMOUNT.
DISPLAY "Net Pay: " NET-PAY.
DISPLAY "----------------------------------".
+61
View File
@@ -0,0 +1,61 @@
IDENTIFICATION DIVISION.
PROGRAM-ID. PAYROLL-CALC.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 EMPLOYEE-DETAILS.
05 EMPLOYEE-NAME PIC X(30).
05 HOURS-WORKED PIC 99V9.
05 HOURLY-RATE PIC 99V99.
01 PAY-CALCULATIONS.
05 GROSS-PAY PIC 9(5)V99.
05 OVERTIME-HOURS PIC 99V9.
05 OVERTIME-PAY PIC 9(5)V99.
05 TAX-RATE PIC V99 VALUE 0.10.
05 TAX-AMOUNT PIC 9(5)V99.
05 NET-PAY PIC 9(5)V99.
PROCEDURE DIVISION.
MAIN-LOGIC.
DISPLAY "--- COBOL Payroll Calculator ---".
DISPLAY "Enter Employee Name: ".
ACCEPT EMPLOYEE-NAME.
DISPLAY "Enter Hours Worked (e.g., 40.5): ".
ACCEPT HOURS-WORKED.
DISPLAY "Enter Hourly Rate (e.g., 15.75): ".
ACCEPT HOURLY-RATE.
PERFORM CALCULATE-GROSS-PAY.
PERFORM CALCULATE-TAX.
PERFORM CALCULATE-NET-PAY.
PERFORM DISPLAY-RESULTS.
STOP RUN.
CALCULATE-GROSS-PAY.
IF HOURS-WORKED > 40 THEN
COMPUTE OVERTIME-HOURS = HOURS-WORKED - 40
COMPUTE OVERTIME-PAY = OVERTIME-HOURS * HOURLY-RATE * 1.5
COMPUTE GROSS-PAY = (40 * HOURLY-RATE) + OVERTIME-PAY
ELSE
COMPUTE GROSS-PAY = HOURS-WORKED * HOURLY-RATE
END-IF.
CALCULATE-TAX.
COMPUTE TAX-AMOUNT = GROSS-PAY * TAX-RATE.
CALCULATE-NET-PAY.
COMPUTE NET-PAY = GROSS-PAY - TAX-AMOUNT.
DISPLAY-RESULTS.
DISPLAY "----------------------------------".
DISPLAY "Employee Name: " EMPLOYEE-NAME.
DISPLAY "Hours Worked: " HOURS-WORKED.
DISPLAY "Hourly Rate: " HOURLY-RATE.
DISPLAY "Gross Pay: " GROSS-PAY.
DISPLAY "Tax (10%): " TAX-AMOUNT.
DISPLAY "Net Pay: " NET-PAY.
DISPLAY "----------------------------------".
+25
View File
@@ -0,0 +1,25 @@
(logging) {
log {
output file /var/log/caddy.log
}
}
# a comment
localhost:8080, example.com {
root * /var/www/site
file_server
reverse_proxy /.well-known/matrix/* localhost:8008 {
header_up Host {upstream_hostport}
}
import logging
}
www.example.com {
redir https://example.com{uri} permanent
import logging
}
status.example.com {
reverse_proxy localhost:3002
import logging
}
+399
View File
@@ -0,0 +1,399 @@
UWSM(1) General Commands Manual UWSM(1)
NAME
UWSM - Universal Wayland Session Manager.
SYNOPSIS
uwsm [-h|-v] {subcommand} [options ...]
DESCRIPTION
Launches arbitrary wayland compositor via a set of systemd user units to provide graphical user
session with environment management, XDG autostart support, clean shutdown. Provides helpers
for launching applications as scopes or services.
SUBCOMMANDS
select Select default compositor Entry.
start Start compositor and graphical session.
finalize Send compositor-set variables and unit startup notification to systemd user manager.
stop Stop graphical session and compositor.
app Application unit launcher (with Desktop Entry support).
check Perform state checks (for scripting and info).
aux Technical functions for use inside units.
See corresponding SUBCOMMANDS subsections below for further info.
Help for each subcommand is accessible by running "uwsm {subcommand} -h".
CONFIGURATION
Files
In XDG config hierarchy:
uwsm/env
uwsm/env.d/*
uwsm/env-${compositor}
uwsm/env-${compositor}.d/* Environment (shell) to be sourced for the graphical session.
Sourced from directories of increasing priority, in each directory
common file is sourced first, then suffixed files in the order of
items listed in XDG_CURRENT_SESSION var (lowercased).
uwsm/default-id Stores Desktop Entry ID of default compositor.
Fallback is also extended into the system part of XDG data hierarchy, this can be used for dis
tro level defaults.
Environment vars
UWSM_UNIT_RUNG (run|home)
Which rung of systemd/user/ hierarchy to manage generated unit
and drop-in files in: $XDG_RUNTIME_DIR or $XDG_CONFIG_HOME.
UWSM_TWEAKS (boolean value)
Set to False to remove and not generate tweak drop-ins for
other software.
UWSM_FINALIZE_VARNAMES (whitespace-separated names of env vars)
Additional variables for "uwsm finalize".
UWSM_WAIT_VARNAMES (whitespace-separated names of env vars)
Variables to wait for in activation environment before proceed
ing to graphical session (in addition to WAYLAND_DISPLAY).
UWSM_WAIT_VARNAMES_TIMEOUT (int value)
Seconds to wait for variables to appear in activation environ
ment. Essentially, startup timeout (default: 10).
UWSM_WAIT_VARNAMES_SETTLETIME (float value)
Seconds to pause after all expected vars found in activation
environment (default: 0.2).
UWSM_APP_UNIT_TYPE (scope|service)
Default unit type for launching apps (default: scope).
UWSM_SILENT_START (int or boolean value)
True or 1 to inhibit stdout messages from "uwsm start". 2 to
also inhibit warnings.
DEBUG (int or boolean value)
True or positive number to dump debug info to stderr.
OPERATION OVERVIEW
Login Sequence Integration
uwsm can be launched by using conditional exec in shell profile to replace login shell (see
Shell Profile Integration section).
Alternatively "uwsm start ..." command can be put into wayland session's Desktop Entry to be
launched by a display manager (see Use Inside Desktop Entry section).
Compositor Selection
uwsm can run arbitrary compositor command line or a Desktop Entry by ID (specifying Action ID
is also supported).
Desktop Entry can also be selected via a whiptail menu (see select subcommand section).
Startup
See start subcommand section for command syntax.
UWSM uses a set of units bound to standard user session targets:
• wayland-session-pre@.target (bound to graphical-session-pre.target)
• wayland-wm-env@.service (environment preloader service)
• wayland-session@.target (bound to graphical-session.target)
• wayland-wm@.service (service for the selected compositor)
• wayland-session-xdg-autostart@.target (bound to xdg-desktop-autostart.target)
• wayland-session-envelope@.target (lives through entire lifecycle)
• wayland-session-shutdown.target (conflicts with targets above for shutdown)
• wayland-session-bindpid@.service (PID-tracking session killswitch)
• wayland-session-waitenv.service (delays graphical session until vars appear)
Compositor ID (Desktop Entry ID or executable name) becomes the specifier for all templated
units.
At the stage of graphical-session-pre.target, the environment saved from "uwsm start" context
is loaded (or POSIX shell profile is sourced), uwsm environment files are sourced. The delta is
exported to the systemd and D-Bus activation environments by the environment preloader service
and is marked for cleanup at shutdown stage. Preloader shell context for convenience has
IN_UWSM_ENV_PRELOADER var set to true.
At the stage of graphical-session.target (before it) the main compositor unit wayland-
wm@${ID}.service and wayland-session-waitenv.service are started.
Compositor should at least put WAYLAND_DISPLAY variable to systemd activation environment. This
will trigger uwsm's automatic finalization logic. Without WAYLAND_DISPLAY in activation envi
ronment startup will timeout in 10 seconds.
Manual finalization is possible by running "uwsm finalize" (see finalize subcommand section),
also in combination with tweaking UWSM_WAIT_VARNAMES and UWSM_WAIT_VARNAMES_SETTLETIME vars
(see Environment vars section).
Successful activation of compositor unit and existence of WAYLAND_DISPLAY in activation envi
ronment will allow graphical-session.target to be declared reached.
Finally, xdg-desktop-autostart.target is activated.
Inside session
It is highly recommended to configure the compositor or app launcher to launch apps as scopes
or services in special user session slices (app.slice, background.slice, session.slice). uwsm
provides custom nested slices for apps to live in and be terminated on session end:
• app-graphical.slice
• background-graphical.slice
• session-graphical.slice
A helper app subcommand is provided to handle all the systemd-run invocations for you (see app
subcommand section).
The compositor is launched in session.slice by default (as recommended by systemd.special(7)).
Shutdown
Can be initiated by either:
• running uwsm stop
• stopping wayland-wm@*.service or wayland-session-envelope@*.target
• starting wayland-session-shutdown.target
Systemd stops all user units in reverse, as it usually does. During deactivation of graphical-
session-pre.target, the environment preloader service cleans activation environments by unset
ting all variables that were marked for removal during startup and finalization stages.
Do not use compositor's native exit mechanism or kill its process directly.
SUBCOMMANDS
select
Selects default wayland session compositor Desktop Entry.
uwsm select
Invokes a whiptail menu to select default session among Desktop Entries in wayland-sessions XDG
data hierarchy. Writes to ${XDG_CONFIG_HOME}/uwsm/default-id. Nothing else is done. Returns 1
if selection is cancelled. Can be used for scripting launch condition in shell profile.
check
Performs tests, returns 0 on success, 1 on failure.
is-active:
uwsm check is-active [-h] [-v] [compositor]
-v show additional info
compositor check for specific compositor
Checks if unit of specific compositor or graphical-session*.target in general is in active or
activating state.
may-start:
uwsm check may-start [-h] [-g [S]] [-v|-q] [N ...]
N ... allowed VT numbers (default: 1)
-g S wait S seconds for graphical.target in queue (default: 60; 0 or less disables
check).
-i do not check for login shell
-r do not check for local session (allow remote session)
-v show all failed tests
-q be quiet
Checks whether it is OK to launch a wayland session via the following conditions:
• DBUS_SESSION_BUS_ADDRESS is set
• Running from login shell
• System is at graphical.target
• User graphical-session*.target units are not yet active
• Foreground VT is among allowed (default: 1)
• Login session's VT is matching
start
Generates units for given compositor command line or Desktop Entry and starts them.
uwsm start [-h] [-D name[:name...]] [-a|-e] [-N Name] [-C Comment] [-U {run|home}] [-t]
[-o] [-n] -- compositor [args ...]
-F Hardcode mode, always write command line to unit drop-ins and use full
paths.
-D name[:name...] Names to fill XDG_CURRENT_DESKTOP with (:-separated). Existing var con
tent is a starting point if no active session is running.
-a Append desktop names set by -D to other sources (default).
-e Use desktop names set by -D exclusively, discard other sources.
-N Name Fancy name for compositor (filled from Desktop Entry by default).
-C Comment Fancy description for compositor (filled from Desktop Entry by de
fault).
-U {run|home} Select rung for generated unit files: run: $XDG_RUNTIME_DIR/sys
temd/user (default), or home: $XDG_CONFIG_HOME/systemd/user. Permanent
destination will save some time by removing need for reloading systemd.
Managed files from other rung will be removed. Can be preset with
UWSM_UNIT_RUNG environment var.
-t Do not generate (and remove) tweak unit files. Can be preset with
UWSM_TWEAKS=false environment var.
-T Generate tweak unit files for other software. This is default behavior.
-g S Wait for S seconds for system graphical.target in queue and warn if
timed out or not in queue (default: 60, negative to disable).
-G S Wait for S seconds for system graphical.target in queue and abort if
timed out or not in queue (overrides -g, default: -1, (disabled)).
-o Only generate units, but do not start.
-n Dry run, do not write or start anything.
The first argument of the compositor command line acts as an ID and should be either one of:
• Executable name
• Desktop Entry ID (optionally with ":"-delimited action ID)
• Special value:
• select - invoke menu to select compositor.
• default - run previously selected compositor (or select if no selection was saved).
If given as path, hardcode mode will be used implicitly.
Always use "--" to disambiguate dashed arguments intended for compositor itself.
After units are (re)generated, wayland-session-bindpid@${PID}.service is started, to track the
PID of invoking uwsm, then uwsm process replaces itself with systemctl execution that starts
wayland-wm@${ID}.service and waits for it to finish.
In order to complete the startup sequence, the compositor has to put WAYLAND_DISPLAY into the
systemd activation environment. This can be done explicitly by making compositor run "uwsm fi
nalize" command (see the next subsection).
finalize
For running by a compositor on startup.
uwsm finalize [-h] [VAR_NAME ...]
Exports WAYLAND_DISPLAY, DISPLAY and any defined vars mentioned by names in arguments or in
UWSM_FINALIZE_VARNAMES variable (whitespace-separated). Then sends startup notification for the
unit to systemd user manager.
This is required if compositor itself does not put WAYLAND_DISPLAY to systemd activation envi
ronment, otherwise wayland-session@.service unit or a dedicated wayland-session-waitenv.service
unit will terminate due to startup timeout.
UWSM_FINALIZE_VARNAMES variable can be prefilled by plugins.
Direct assignment as VAR_NAME=value is also possible, but recommended only for creating flags
for UWSM_WAIT_VARNAMES mechanism.
stop
Stops compositor and optionally removes generated units.
uwsm stop [-h] [-r [compositor] [-U {run|home}] [-n]
-r [compositor] Also remove units (all or only compositor-specific).
-U {run|home} Select rung for generated unit files: run: $XDG_RUNTIME_DIR/systemd/user
(default), or home: $XDG_CONFIG_HOME/systemd/user. Permanent destination
will save some time by removing need for reloading systemd. Managed files
from other rung will be removed. Can be preset with UWSM_UNIT_RUNG envi
ronment var.
-n Dry run, do not stop or remove anything.
app
Application-to-unit launcher with Desktop Entry support.
uwsm app [-h] [-s {a,b,s,custom.slice}] [-t {scope,service}] [-a app_name] [-u unit_name]
[-d unit_description] [-S ] [-T] -- application [args ...]
-s {a,b,s,custom.slice} Slice selector (default: a):
a - app-graphical.slice
b - background-graphical.slice
s - session-graphical.slice
any slice by full name
-t {scope,service} Type of unit to launch (default: scope, can be preset by
UWSM_APP_UNIT_TYPE env var).
-a app_name Override app name (a substring in unit name).
-u unit_name Override the whole autogenerated unit name.
-d unit_description Unit Description.
-p Property=value Set additional unit property (option is repeatable).
-S {out,err,both} Silence stdout, stderr, or both.
-T Launch app in a terminal. Allows command to be empty to just
launch a terminal.
Application can be provided as a command with optional arguments, or a Desktop Entry ID, op
tionally suffixed with ":"-delimited Action ID. If Desktop Entry is being launched, arguments
should be compatible with it.
Always use "--" to disambiguate dashed arguments intended for application itself.
aux
For use in systemd user services. Can only be called by systemd user manager.
prepare-env Prepares environment (for use in ExecStart in wayland-wm-env@.service bound to
wayland-session-pre@.target).
cleanup-env Cleans up environment (for use ExecStop in in wayland-wm-env@.service bound to
wayland-session-pre@.target).
exec Executes a command with arguments or a desktop entry (for use in Exec in wayland-
wm@.service bound to wayland-session@.target).
app-daemon Daemon for faster app argument generation, used by uwsm-app client.
APP DAEMON
Provided as wayland-wm-app-daemon.service to be started on-demand.
Daemon receives app arguments from ${XDG_RUNTIME_DIR}/uwsm-app-daemon-in pipe. Resulting argu
ments are formatted as shell code and written to ${XDG_RUNTIME_DIR}/uwsm-app-daemon-out pipe.
Arguments are expected to be \0-delimited, leading \0 are stripped. One command is received per
write+close.
The first argument determines the behavior:
• app the rest is processed the same as in "uwsm app"
• ping just "pong" is returnedn
• stop daemon is stoppedn
Single commands are prepended with exec, iterated commands are assembled with trailing & each,
followed by wait.
The purpose of all this is to skip all the expensive Python startup and import routines that
slow things down every time "uwsm app" is called. Instead the daemon does it once and then lis
tens for requests, while a simple shell script may dump arguments to one pipe and run the code
received from another via eval, which is much faster.
The simplest script is:
#!/bin/sh
printf '0%s' app "$@" > "${XDG_RUNTIME_DIR}/uwsm-app-daemon-in"
IFS='' read -r cmd < "${XDG_RUNTIME_DIR}/uwsm-app-daemon-out"
eval "$cmd"
Provided uwsm-app client script is a bit smarter: it can start the daemon, applies timeouts,
and supports newlines in returned args.
SHELL PROFILE INTEGRATION
To launch uwsm automatically on login, add one of constructs below (or similar) to shell pro
file.
This asks to select a compositor (or refuse and continue with login shell) when logged in on VT
1:
if uwsm check may-start && uwsm select; then
exec systemd-cat -t uwsm_start uwsm start default
fi
This just starts a specific compositor depending on foreground VT:
if uwsm check may-start 1; then
exec systemd-cat -t uwsm_start uwsm start sway.desktop
elif uwsm check may-start 2; then
exec systemd-cat -t uwsm_start uwsm start labwc.desktop
fi
Using "uwsm check may-start" as a condition is essential, not only to prevent accidental
startup attempts where they are not expected, but also since startup may involve sourcing shell
profile, which might lead to nasty loops.
See check subcommand section for info on may-start checker.
exec allows uwsm to replace login shell in order to properly bind to user session and handle
session termination.
"systemd-cat -t uwsm_start" (optional) executes the command given to it (uwsm) with its stdout
and stderr connected to the systemd journal, tagged with identifier "uwsm_start". See systemd-
cat(1) for more options.
USE INSIDE DESKTOP ENTRY
To launch uwsm from a display/login manager, "uwsm start" can be used inside Desktop Entries.
Example /usr/local/share/wayland-sessions/my-compositor.desktop:
[Desktop Entry]
Name=My compositor (with UWSM)
Comment=My cool compositor
Exec=uwsm start -N "My compositor" -D mycompositor -C "My cool compositor" mywm
DesktopNames=mycompositor
Type=Application
Things to keep in mind:
• For consistency, command line arguments should mirror the keys of the entry
• Command in Exec= should start with "uwsm start"
• It should not point to itself (as a combination of Desktop Entry ID and Action ID)
• It should not point to a Desktop Entry ID and Action ID that also uses uwsm
Potentially such entries may be found and used by uwsm itself, i.e. in shell profile integra
tion situation, or when launched manually. Following the principles above ensures uwsm will
properly recognize itself and parse requested arguments inside the entry without any side ef
fects.
SEE ALSO
uwsm-plugins(3), systemd-run(1), systemd-cat(1), systemd.special(7)
2026-02-14 UWSM(1)
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/expect -f
# Expect script detected via expect shebang
set timeout 30
spawn ssh user@host
expect "password:"
send "secret\r"
expect eof
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env tclsh
# Tcl script detected via tclsh shebang
puts "Hello from tclsh"
set x 42
if {$x > 0} {
puts "positive"
}

Some files were not shown because too many files have changed in this diff Show More