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

Compare commits

...

215 Commits

Author SHA1 Message Date
dependabot[bot] b739f7d1e8 build(deps): bump softprops/action-gh-release from 2 to 3
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-01 02:12:24 +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
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
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
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
75 changed files with 3033 additions and 773 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
+5 -7
View File
@@ -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 , }
@@ -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"
@@ -444,7 +442,7 @@ jobs:
echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT
- name: Publish archives and packages
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: steps.is-release.outputs.IS_RELEASE
with:
files: |
+3
View File
@@ -1,3 +1,4 @@
.direnv/
/target/
**/*.rs.bk
@@ -12,3 +13,5 @@
/assets/completions/bat.zsh
/assets/manual/bat.1
/assets/metadata.yaml
+6 -3
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
@@ -278,3 +275,9 @@
[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
+52 -10
View File
@@ -1,22 +1,64 @@
# unreleased
- Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l)
## Features
- Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk)
## Bugfixes
- `--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)
## Other
- Add instructions for removing fish help abbreviations to README, see #3655 (@claw-explorer). Closes #3536
- Add .NET slnx extension, see #3682 (@ltrzesniewski)
## Features
- 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-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)
## 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)
## 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)
## Themes
+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
+61 -61
View File
@@ -223,9 +223,9 @@ checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
[[package]]
name = "bytesize"
version = "1.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc"
checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3"
[[package]]
name = "cc"
@@ -252,18 +252,18 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clap"
version = "4.5.46"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.46"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
dependencies = [
"anstream",
"anstyle",
@@ -274,9 +274,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.4"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "clircle"
@@ -440,9 +440,9 @@ dependencies = [
[[package]]
name = "deranged"
version = "0.3.11"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
dependencies = [
"powerfmt",
]
@@ -658,9 +658,9 @@ dependencies = [
[[package]]
name = "git2"
version = "0.20.2"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110"
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
dependencies = [
"bitflags",
"libc",
@@ -861,9 +861,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.12.1"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
@@ -909,15 +909,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.175"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "libgit2-sys"
version = "0.18.2+1.9.1"
version = "0.18.3+1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222"
checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487"
dependencies = [
"cc",
"libc",
@@ -951,9 +951,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
@@ -1032,9 +1032,9 @@ dependencies = [
[[package]]
name = "nix"
version = "0.30.1"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
dependencies = [
"bitflags",
"cfg-if",
@@ -1068,9 +1068,9 @@ dependencies = [
[[package]]
name = "num-conv"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
name = "num-traits"
@@ -1217,9 +1217,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.103"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
@@ -1235,9 +1235,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.40"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
@@ -1324,14 +1324,14 @@ dependencies = [
[[package]]
name = "rustix"
version = "1.1.2"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys 0.11.0",
"linux-raw-sys 0.12.1",
"windows-sys 0.61.2",
]
@@ -1406,18 +1406,18 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "1.0.3"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_with"
version = "3.15.1"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04"
checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9"
dependencies = [
"serde_core",
"serde_with_macros",
@@ -1425,9 +1425,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.15.1"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955"
checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0"
dependencies = [
"darling",
"proc-macro2",
@@ -1603,14 +1603,14 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.23.0"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix 1.1.2",
"rustix 1.1.4",
"windows-sys 0.61.2",
]
@@ -1716,30 +1716,30 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.37"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.19"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
@@ -1757,9 +1757,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.8"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
checksum = "994b95d9e7bae62b34bab0e2a4510b801fa466066a6a8b2b57361fa1eba068ee"
dependencies = [
"indexmap",
"serde_core",
@@ -1772,27 +1772,27 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.7.3"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.0.4"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.4"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
[[package]]
name = "typenum"
@@ -1808,15 +1808,15 @@ checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
[[package]]
name = "unicode-width"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unsafe-libyaml"
@@ -2186,9 +2186,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.14"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
[[package]]
name = "wit-bindgen-rt"
+13 -13
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"]
@@ -56,7 +56,7 @@ minus = { version = "5.6", 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"
@@ -69,11 +69,11 @@ etcetera = { version = "0.11.0", optional = true }
grep-cli = { version = "0.1.12", optional = true }
regex = { version = "1.12.2", 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.15", optional = true }
terminal-colorsaurus = "1.0"
unicode-segmentation = "1.12.0"
unicode-segmentation = "1.13.2"
itertools = "0.14.0"
[dependencies.git2]
@@ -87,7 +87,7 @@ default-features = false
features = ["parsing"]
[dependencies.clap]
version = "4.5.46"
version = "4.5.60"
optional = true
features = ["wrap_help", "cargo"]
@@ -100,30 +100,30 @@ expect-test = "1.5.0"
serial_test = { version = "2.0.0", default-features = false }
predicates = "3.1.3"
wait-timeout = "0.2.1"
tempfile = "3.23.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.13.0", features = ["serde"] }
itertools = "0.14.0"
once_cell = "1.20"
prettyplease = "0.2.37"
proc-macro2 = "1.0.103"
quote = "1.0.40"
proc-macro2 = "1.0.106"
quote = "1.0.45"
regex = "1.12.2"
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.8", features = ["preserve_order"] }
toml = { version = "1.1.1", features = ["preserve_order"] }
walkdir = "2.5"
[build-dependencies.clap]
version = "4.5.46"
version = "4.5.60"
optional = true
features = ["wrap_help", "cargo"]
+38 -25
View File
@@ -19,20 +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>
### Syntax highlighting
`bat` supports syntax highlighting for a large number of programming and markup
@@ -43,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)
@@ -198,7 +184,7 @@ bat main.cpp | xclip
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).
@@ -208,12 +194,6 @@ Note that the [Manpage syntax](assets/syntaxes/02_Extra/Manpage.sublime-syntax)
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`
@@ -246,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.
@@ -276,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
@@ -519,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.
@@ -581,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
+4 -3
View File
@@ -9,7 +9,7 @@ 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')
@@ -135,7 +135,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*)')
@@ -173,7 +173,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 +186,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.')
+4 -2
View File
@@ -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)
@@ -189,8 +190,8 @@ _bat() {
$split && return 0
if [[ $cur == -* ]]; then
# --unbuffered excluded intentionally (no-op)
COMPREPLY=($(compgen -W "
--unbuffered
--show-all
--nonprintable-notation
--binary
@@ -227,6 +228,7 @@ _bat() {
--lessopen
--completion
--diagnostic
--quiet-empty
--acknowledgements
--set-terminal-title
--help
+4 -2
View File
@@ -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
@@ -238,7 +240,7 @@ complete -c $bat -l theme-dark -x -a "(command $bat --list-themes | command cat)
complete -c $bat -l theme-light -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme for light backgrounds" -n __bat_no_excl_args
complete -c $bat -s 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
+4 -3
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]'
@@ -58,10 +58,11 @@ _{{PROJECT_EXECUTABLE}}_main() {
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,7 +90,7 @@ _{{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) }') )
languages=( ${(f)"$({{PROJECT_EXECUTABLE}} --list-languages | awk -F':|,' '{ for (i = 1; i <= NF; ++i) printf("%s:%s\n", $i, $1) }')"} )
_describe 'language' languages && 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:
-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 {
+20 -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'
@@ -203,8 +211,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 +223,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
];
};
}
);
};
}
+154 -20
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| {
@@ -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 {
@@ -661,6 +719,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 +789,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"
);
}
}
+37 -13
View File
@@ -384,6 +384,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
@@ -399,27 +403,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() && maybe_term_width.is_none() {
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()) {
@@ -461,6 +468,8 @@ impl App {
Some("auto") => StripAnsiMode::Auto,
_ => unreachable!("other values for --strip-ansi are not allowed"),
},
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")
@@ -581,7 +590,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.
@@ -618,10 +637,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)
}
+50 -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(
@@ -550,11 +563,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 +659,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 +710,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 +735,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));
}
+33 -7
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,
};
@@ -197,7 +196,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 +205,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 +258,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 +286,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 +329,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 +351,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")]
@@ -401,7 +427,7 @@ fn run() -> Result<bool> {
};
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());
+9
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,
@@ -107,6 +110,12 @@ pub struct Config<'a> {
// Whether or not to strip ANSI escape codes from the input
pub strip_ansi: 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;
+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\
+3 -3
View File
@@ -155,7 +155,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,
@@ -192,7 +192,7 @@ impl LessOpenPreprocessor {
.map(|s| s.replacen("%s", &path_str, 1).replacen("%s", "-", 1)),
}
},
)),
))?,
metadata: input.metadata,
description: input.description,
})
@@ -261,7 +261,7 @@ impl Drop for Preprocessed {
}
};
if lessclose_output.status.success() {
if !lessclose_output.status.success() {
bat_warning!("$LESSCLOSE exited with nonzero exit code",)
};
}
+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(()),
}
}
}
+66
View File
@@ -149,6 +149,35 @@ pub fn strip_ansi(line: &str) -> String {
buffer
}
/// 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:
@@ -261,3 +290,40 @@ fn test_strip_overstrike() {
// 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'));
}
+83 -9
View File
@@ -29,7 +29,9 @@ use crate::error::*;
use crate::input::OpenedInput;
use crate::line_range::{MaxBufferedLineNumber, RangeCheckResult};
use crate::output::OutputHandle;
use crate::preprocessor::{expand_tabs, replace_nonprintable, strip_ansi, strip_overstrike};
use crate::preprocessor::{
expand_tabs, replace_nonprintable, 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};
@@ -37,6 +39,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",
@@ -268,7 +280,12 @@ impl<'a> InteractivePrinter<'a> {
const PLAIN_TEXT_SYNTAX: &str = "Plain Text";
const MANPAGE_SYNTAX: &str = "Manpage";
const COMMAND_HELP_SYNTAX: &str = "Command Help";
match assets.get_syntax(config.language, input, &config.syntax_mapping) {
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
@@ -454,6 +471,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)?;
}
@@ -469,7 +491,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, '┬')?;
@@ -523,9 +545,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)
}
@@ -556,6 +580,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
@@ -771,11 +800,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 {
@@ -797,13 +836,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,
@@ -816,8 +879,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"]
@@ -0,0 +1,2 @@
[mappings]
"Git Ignore" = [".?*ignore"]
+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),
}
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
+581
View File
@@ -1047,6 +1047,57 @@ fn tabs_4_arg_overrides_env_noconfig() {
);
}
#[test]
fn terminal_width_env_var_is_respected() {
let tmp_dir = tempdir().expect("can create temporary directory");
let tmp_path = tmp_dir.path().join("long.txt");
std::fs::write(
&tmp_path,
"0123456789abcdef0123456789abcdef0123456789abcdef\n",
)
.expect("can write temporary file");
bat()
.env("BAT_WIDTH", "20")
.arg(&tmp_path)
.arg("--paging=never")
.arg("--color=never")
.arg("--style=numbers")
.arg("--decorations=always")
.arg("--wrap=character")
.assert()
.success()
.stdout(" 1 0123456789abcde\n f0123456789abcd\n ef0123456789abc\n def\n")
.stderr("");
}
#[test]
fn terminal_width_arg_overrides_env() {
let tmp_dir = tempdir().expect("can create temporary directory");
let tmp_path = tmp_dir.path().join("long.txt");
std::fs::write(
&tmp_path,
"0123456789abcdef0123456789abcdef0123456789abcdef\n",
)
.expect("can write temporary file");
bat()
.env("BAT_WIDTH", "20")
.arg(&tmp_path)
.arg("--paging=never")
.arg("--color=never")
.arg("--style=numbers")
.arg("--decorations=always")
.arg("--wrap=character")
.arg("--terminal-width=10")
.assert()
.success()
.stdout(
" 1 01234\n 56789\n abcde\n f0123\n 45678\n 9abcd\n ef012\n 34567\n 89abc\n def\n",
)
.stderr("");
}
#[test]
fn fail_non_existing() {
bat().arg("non-existing-file").assert().failure();
@@ -1425,6 +1476,21 @@ fn pager_failed_to_parse() {
.stderr(predicate::str::contains("Could not parse pager command"));
}
#[test]
#[serial]
fn pager_missing_warning() {
bat()
.env("BAT_PAGER", "nonexistent-pager-xyz-missing")
.arg("--paging=always")
.arg("test.txt")
.assert()
.success()
.stderr(predicate::str::contains("[bat warning]"))
.stderr(predicate::str::contains("not found"))
.stderr(predicate::str::contains("nonexistent-pager-xyz-missing"))
.stdout(predicate::str::contains("hello world\n"));
}
#[test]
#[serial]
fn env_var_bat_paging() {
@@ -1442,6 +1508,7 @@ fn env_var_bat_paging() {
#[test]
fn basic_set_terminal_title() {
bat()
.env("BAT_PAGER", "cat")
.arg("--paging=always")
.arg("--set-terminal-title")
.arg("test.txt")
@@ -1458,6 +1525,7 @@ fn diagnostic_sanity_check() {
.assert()
.success()
.stdout(predicate::str::contains("BAT_PAGER="))
.stdout(predicate::str::contains("BAT_WIDTH="))
.stderr("");
}
@@ -2077,6 +2145,24 @@ fn header_binary() {
.stderr("");
}
#[test]
fn header_zip_file_is_binary() {
let tmp_dir = tempdir().expect("can create temporary directory");
let tmp_path = tmp_dir.path().join("test.zip");
std::fs::write(&tmp_path, b"PK\x03\x04hello").expect("can write temporary file");
bat()
.arg(&tmp_path)
.arg("--decorations=always")
.arg("--style=header")
.arg("-r=0:0")
.arg("--file-name=test.zip")
.assert()
.success()
.stdout("File: test.zip <BINARY>\n")
.stderr("");
}
#[test]
fn header_full_binary() {
bat()
@@ -2454,6 +2540,121 @@ fn no_first_line_fallback_when_mapping_to_invalid_syntax() {
.stderr(predicate::str::contains("unknown syntax: 'InvalidSyntax'"));
}
#[test]
fn fallback_syntax_is_used_when_no_syntax_is_detected() {
let content = "# comment\nfoo=bar\n";
let fallback_output = bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--file-name=unknown.fallbacksyntax")
.arg("--fallback-syntax=bash")
.write_stdin(content)
.assert()
.success()
.get_output()
.stdout
.clone();
let explicit_output = bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--language=bash")
.arg("--file-name=unknown.fallbacksyntax")
.write_stdin(content)
.assert()
.success()
.get_output()
.stdout
.clone();
assert_eq!(
from_utf8(&fallback_output).expect("output is valid utf-8"),
from_utf8(&explicit_output).expect("output is valid utf-8")
);
}
#[test]
fn fallback_syntax_does_not_override_detected_syntax() {
let content = "fn main() { println!(\"hello\"); }\n";
let with_fallback = bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--file-name=test.rs")
.arg("--fallback-syntax=json")
.write_stdin(content)
.assert()
.success()
.get_output()
.stdout
.clone();
let without_fallback = bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--file-name=test.rs")
.write_stdin(content)
.assert()
.success()
.get_output()
.stdout
.clone();
assert_eq!(
from_utf8(&with_fallback).expect("output is valid utf-8"),
from_utf8(&without_fallback).expect("output is valid utf-8")
);
}
#[test]
fn fallback_syntax_does_not_override_explicit_language() {
let content = "{\"a\": 1}\n";
let with_fallback = bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--language=json")
.arg("--fallback-syntax=rust")
.arg("--file-name=unknown.fallbacksyntax")
.write_stdin(content)
.assert()
.success()
.get_output()
.stdout
.clone();
let without_fallback = bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--language=json")
.arg("--file-name=unknown.fallbacksyntax")
.write_stdin(content)
.assert()
.success()
.get_output()
.stdout
.clone();
assert_eq!(
from_utf8(&with_fallback).expect("output is valid utf-8"),
from_utf8(&without_fallback).expect("output is valid utf-8")
);
}
#[test]
fn invalid_fallback_syntax_returns_error() {
bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--file-name=unknown.fallbacksyntax")
.arg("--fallback-syntax=InvalidSyntax")
.write_stdin("foo\n")
.assert()
.failure()
.stderr(predicate::str::contains("unknown syntax: 'InvalidSyntax'"));
}
#[test]
fn show_all_mode() {
bat()
@@ -2550,6 +2751,25 @@ fn binary_as_text() {
.stderr("");
}
#[test]
fn binary_as_text_control_char_width() {
// Control characters are displayed as caret notation (e.g. ^@) by the
// terminal, occupying 2 columns each. With 20 NUL bytes (40 columns) +
// "END" (3 columns) = 43 columns, wrapping at terminal width 40 must
// produce 2 lines, not 1. See #3631.
bat()
.arg("--binary=as-text")
.arg("--wrap=character")
.arg("--terminal-width=40")
.arg("--decorations=always")
.arg("--style=plain")
.arg("--color=never")
.arg("regression_tests/issue_3631.txt")
.assert()
.success()
.stdout(predicate::function(|s: &str| s.lines().count() == 2));
}
#[test]
fn no_strip_overstrike_for_plain_text() {
// Overstrike is preserved for plain text files (no syntax highlighting)
@@ -2884,6 +3104,44 @@ fn no_wrapping_with_chop_long_lines() {
wrapping_test("--chop-long-lines", false);
}
#[test]
#[serial]
fn wrap_never_flag_respected_with_paging_always() {
mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| {
bat()
.arg("--pager=cat")
.arg("--paging=always")
.arg("--wrap=never")
.arg("--color=never")
.arg("--decorations=never")
.arg("--style=plain")
.write_stdin("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz\n")
.assert()
.success()
.stdout(predicate::str::contains("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz").normalize())
.stderr("");
});
}
#[test]
#[serial]
fn s_flag_respected_with_paging_always() {
mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| {
bat()
.arg("--pager=cat")
.arg("--paging=always")
.arg("-S")
.arg("--color=never")
.arg("--decorations=never")
.arg("--style=plain")
.write_stdin("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz\n")
.assert()
.success()
.stdout(predicate::str::contains("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz").normalize())
.stderr("");
});
}
#[test]
fn theme_arg_overrides_env() {
bat()
@@ -3654,3 +3912,326 @@ fn plain_with_sized_terminal_width() {
.stdout("hello \nworld\n")
.stderr("");
}
#[test]
fn quiet_empty_suppresses_output_on_empty_stdin() {
bat()
.arg("--quiet-empty")
.write_stdin("")
.assert()
.success()
.stdout("");
}
#[test]
fn quiet_empty_does_not_affect_non_empty_input() {
bat()
.arg("--quiet-empty")
.write_stdin("hello\n")
.assert()
.success()
.stdout("hello\n");
}
#[test]
fn quiet_empty_suppresses_output_on_empty_file() {
bat()
.arg("--quiet-empty")
.arg("empty.txt")
.assert()
.success()
.stdout("");
}
#[test]
fn cache_help_shows_help_message() {
// Test that `bat cache --help` works (fixes #3560)
// Run in cache_source directory which doesn't have a file named "cache"
bat_with_config()
.current_dir(Path::new(EXAMPLES_DIR).join("cache_source"))
.arg("cache")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains(
"Modify the syntax-definition and theme cache",
))
.stdout(predicate::str::contains("--build"))
.stdout(predicate::str::contains("--clear"));
}
#[test]
fn unbuffered_flag_is_accepted() {
bat()
.arg("--unbuffered")
.arg("test.txt")
.assert()
.success()
.stdout("hello world\n");
}
#[test]
fn unbuffered_mode_disables_line_numbers() {
// When --unbuffered is used, line numbers should be auto-disabled even if requested
bat()
.arg("--unbuffered")
.arg("--style=numbers")
.arg("--decorations=always")
.arg("--color=never")
.arg("test.txt")
.assert()
.success()
.stdout(predicate::str::starts_with(" 1").not());
}
#[test]
fn unbuffered_mode_plain_output() {
bat()
.arg("--unbuffered")
.arg("--color=never")
.arg("--decorations=never")
.arg("test.txt")
.assert()
.success()
.stdout("hello world\n");
}
#[test]
fn word_wrap_breaks_at_word_boundaries() {
bat()
.arg("word-wrap.txt")
.arg("--wrap=word")
.arg("--terminal-width=40")
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.assert()
.success()
.stdout(
"\
The quick brown fox jumps over the lazy
dog and then runs away
superlongwordthatdefinitelyexceedstheter
minalwidthandshouldfallbacktocharacterwr
apping
short words here
",
);
}
#[test]
fn word_wrap_with_line_numbers() {
bat()
.arg("word-wrap.txt")
.arg("--wrap=word")
.arg("--terminal-width=40")
.arg("--style=numbers")
.arg("--decorations=always")
.arg("--color=never")
.assert()
.success()
.stdout(
" 1 The quick brown fox jumps over the
lazy dog and then runs away
2 superlongwordthatdefinitelyexceedst
heterminalwidthandshouldfallbacktoc
haracterwrapping
3 short words here
",
);
}
#[test]
fn word_wrap_short_line_no_wrap() {
bat()
.arg("--wrap=word")
.arg("--terminal-width=80")
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("single-line.txt")
.assert()
.success()
.stdout("Single Line\n");
}
#[cfg(unix)]
#[cfg(feature = "git")]
fn setup_diff_test_repo() -> tempfile::TempDir {
use std::process::Command;
let dir = tempfile::tempdir().expect("can create temporary directory");
let repo = dir.path();
// Initialize a git repo and commit a file
Command::new("git")
.args(["init"])
.current_dir(repo)
.output()
.expect("git init");
Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(repo)
.output()
.expect("git config email");
Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(repo)
.output()
.expect("git config name");
std::fs::write(repo.join("test.txt"), "line 1\nline 2\nline 3\n").expect("can write test file");
Command::new("git")
.args(["add", "test.txt"])
.current_dir(repo)
.output()
.expect("git add");
Command::new("git")
.args(["commit", "-m", "initial"])
.current_dir(repo)
.output()
.expect("git commit");
// Modify the file so --diff has something to show
std::fs::write(
repo.join("test.txt"),
"line 1\nline 2 modified\nline 3\nline 4 added\n",
)
.expect("can write modified test file");
dir
}
#[cfg(unix)]
#[cfg(feature = "git")]
#[test]
fn diff_plain_preserves_change_markers() {
let repo = setup_diff_test_repo();
// With --diff --plain, output should contain the change marker column
// but not other decorations like line numbers or grid
let output = bat()
.current_dir(repo.path())
.arg("--diff")
.arg("--plain")
.arg("--color=never")
.arg("--decorations=always")
.arg("test.txt")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = std::str::from_utf8(&output).expect("valid utf-8");
// The output should contain the modified and added lines
assert!(
stdout.contains("line 2 modified"),
"diff plain output should contain modified line, got: {stdout}"
);
assert!(
stdout.contains("line 4 added"),
"diff plain output should contain added line, got: {stdout}"
);
// Should NOT contain line numbers (a decoration that --plain disables)
assert!(
!stdout.contains(" 1"),
"diff plain output should not contain line numbers, got: {stdout}"
);
}
#[cfg(unix)]
#[cfg(feature = "git")]
#[test]
fn diff_plain_does_not_show_grid_or_header() {
let repo = setup_diff_test_repo();
let output = bat()
.current_dir(repo.path())
.arg("--diff")
.arg("--plain")
.arg("--color=never")
.arg("--decorations=always")
.arg("--terminal-width=80")
.arg("test.txt")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = std::str::from_utf8(&output).expect("valid utf-8");
// Grid lines use box-drawing characters
assert!(
!stdout.contains('─'),
"diff plain output should not contain grid lines, got: {stdout}"
);
assert!(
!stdout.contains('│'),
"diff plain output should not contain grid separators, got: {stdout}"
);
// Header shows "File: <filename>"
assert!(
!stdout.contains("File:"),
"diff plain output should not contain file header, got: {stdout}"
);
}
#[cfg(unix)]
#[cfg(feature = "git")]
#[test]
fn plain_without_diff_still_works() {
let repo = setup_diff_test_repo();
// --plain without --diff should output file content with no decorations at all
bat()
.current_dir(repo.path())
.arg("--plain")
.arg("--color=never")
.arg("--decorations=always")
.arg("test.txt")
.assert()
.success()
.stdout("line 1\nline 2 modified\nline 3\nline 4 added\n");
}
#[test]
fn tcl_shebang_detection_tclsh() {
bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--decorations=always")
.arg("regression_tests/issue_3647_tclsh")
.assert()
.success();
}
#[test]
fn tcl_shebang_detection_wish() {
bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--decorations=always")
.arg("regression_tests/issue_3647_wish")
.assert()
.success();
}
#[test]
fn tcl_shebang_detection_expect() {
bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--decorations=always")
.arg("regression_tests/issue_3647_expect")
.assert()
.success();
}
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 "----------------------------------".
+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 "----------------------------------".
+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>