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

Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] c3c9814307 build(deps): bump assets/syntaxes/02_Extra/TodoTxt
Bumps [assets/syntaxes/02_Extra/TodoTxt](https://github.com/dertuxmalwieder/SublimeTodoTxt) from `071a004` to `cb49855`.
- [Commits](https://github.com/dertuxmalwieder/SublimeTodoTxt/compare/071a004217f981152c78dc7a530536374a753d98...cb498554fbf146d46faea72dd32005142a41545b)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 03:04:17 +00:00
48 changed files with 199 additions and 2555 deletions
-1
View File
@@ -1 +0,0 @@
use flake
+6 -4
View File
@@ -161,12 +161,12 @@ jobs:
fail-fast: false
matrix:
job:
- { target: aarch64-unknown-linux-musl , os: ubuntu-latest , dpkg_arch: musl-linux-arm64, use-cross: true }
- { target: aarch64-unknown-linux-musl , os: ubuntu-latest , dpkg_arch: 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: i386, use-cross: true }
- { target: i686-unknown-linux-gnu , os: ubuntu-latest , dpkg_arch: i686, 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,7 +195,9 @@ jobs:
- name: Install cross
if: matrix.job.use-cross
run: cargo install cross --git https://github.com/cross-rs/cross --rev 588b3c99db52b5a9c5906fab96cfadcf1bde7863
uses: taiki-e/install-action@v2
with:
tool: cross
- name: Overwrite build command env variable
if: matrix.job.use-cross
@@ -333,7 +335,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"
-3
View File
@@ -1,4 +1,3 @@
.direnv/
/target/
**/*.rs.bk
@@ -13,5 +12,3 @@
/assets/completions/bat.zsh
/assets/manual/bat.1
/assets/metadata.yaml
-3
View File
@@ -278,6 +278,3 @@
[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
+1 -31
View File
@@ -1,52 +1,22 @@
# unreleased
## Other
- Add instructions for removing fish help abbreviations to README, see #3655 (@claw-explorer). Closes #3536
- Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l)
## 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)
- 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)
## Bugfixes
- 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)
## 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)
## Syntaxes
- 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)
## Themes
-28
View File
@@ -68,8 +68,6 @@ 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.
@@ -93,29 +91,3 @@ 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
+35 -35
View File
@@ -223,9 +223,9 @@ checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
[[package]]
name = "bytesize"
version = "2.3.1"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3"
checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc"
[[package]]
name = "cc"
@@ -252,18 +252,18 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clap"
version = "4.5.60"
version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.60"
version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41"
dependencies = [
"anstream",
"anstyle",
@@ -274,9 +274,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "1.0.0"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "clircle"
@@ -440,9 +440,9 @@ dependencies = [
[[package]]
name = "deranged"
version = "0.5.5"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
@@ -658,9 +658,9 @@ dependencies = [
[[package]]
name = "git2"
version = "0.20.4"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110"
dependencies = [
"bitflags",
"libc",
@@ -861,9 +861,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.13.0"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
@@ -909,15 +909,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.182"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "libgit2-sys"
version = "0.18.3+1.9.2"
version = "0.18.2+1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487"
checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222"
dependencies = [
"cc",
"libc",
@@ -1032,9 +1032,9 @@ dependencies = [
[[package]]
name = "nix"
version = "0.31.2"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags",
"cfg-if",
@@ -1068,9 +1068,9 @@ dependencies = [
[[package]]
name = "num-conv"
version = "0.2.0"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
@@ -1217,9 +1217,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.106"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
@@ -1415,9 +1415,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.17.0"
version = "3.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9"
checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04"
dependencies = [
"serde_core",
"serde_with_macros",
@@ -1425,9 +1425,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.17.0"
version = "3.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0"
checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955"
dependencies = [
"darling",
"proc-macro2",
@@ -1716,30 +1716,30 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.47"
version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde_core",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.8"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.27"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [
"num-conv",
"time-core",
+8 -8
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.88"
rust-version = "1.87"
[features]
default = ["application", "git"]
@@ -69,7 +69,7 @@ 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 = "2.3.1" }
bytesize = { version = "1.3.0" }
encoding_rs = "0.8.35"
execute = { version = "0.2.15", optional = true }
terminal-colorsaurus = "1.0"
@@ -87,7 +87,7 @@ default-features = false
features = ["parsing"]
[dependencies.clap]
version = "4.5.60"
version = "4.5.46"
optional = true
features = ["wrap_help", "cargo"]
@@ -104,26 +104,26 @@ tempfile = "3.23.0"
serde = { version = "1.0", features = ["derive"] }
[target.'cfg(unix)'.dev-dependencies]
nix = { version = "0.31", default-features = false, features = ["term"] }
nix = { version = "0.30", default-features = false, features = ["term"] }
[build-dependencies]
anyhow = "1.0.97"
indexmap = { version = "2.13.0", features = ["serde"] }
indexmap = { version = "2.8.0", features = ["serde"] }
itertools = "0.14.0"
once_cell = "1.20"
prettyplease = "0.2.37"
proc-macro2 = "1.0.106"
proc-macro2 = "1.0.103"
quote = "1.0.40"
regex = "1.12.2"
serde = "1.0"
serde_derive = "1.0"
serde_with = { version = "3.17.0", default-features = false, features = ["macros"] }
serde_with = { version = "3.15.1", default-features = false, features = ["macros"] }
syn = { version = "2.0.104", features = ["full"] }
toml = { version = "0.9.8", features = ["preserve_order"] }
walkdir = "2.5"
[build-dependencies.clap]
version = "4.5.60"
version = "4.5.46"
optional = true
features = ["wrap_help", "cargo"]
+5 -14
View File
@@ -43,7 +43,7 @@ languages:
### Git integration
`bat` communicates with `git` to show modifications with respect to the index
(see left sidebar):
(see left side bar):
![Git integration example](https://i.imgur.com/2lSW4RE.png)
@@ -198,7 +198,7 @@ bat main.cpp | xclip
export MANPAGER="bat -plman"
man 2 select
```
(on some older Debian or Ubuntu releases, the executable is named `batcat` instead of `bat`)
(replace `bat` with `batcat` if you are on Debian or Ubuntu)
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).
@@ -246,14 +246,6 @@ 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.
@@ -284,8 +276,8 @@ If your Ubuntu/Debian installation is new enough you can simply run:
sudo apt install bat
```
**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:
**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:
``` bash
mkdir -p ~/.local/bin
ln -s /usr/bin/batcat ~/.local/bin/bat
@@ -589,8 +581,7 @@ syntax:
This works very similar to how we add new syntax definitions.
> [!NOTE]
> 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.
> Themes are stored in [`.tmTheme` files](https://www.sublimetext.com/docs/color_schemes_tmtheme.html).
First, create a folder with the new syntax highlighting themes:
```bash
+3 -4
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', 'word')
$ArrayWrap = @('always', 'never', 'character')
$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*, never, character, word).')
[CompletionResult]::new('--wrap' , 'wrap' , [CompletionResultType]::ParameterName, 'Specify the text-wrapping mode (*auto*, character).')
[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, 'Enable unbuffered input reading for streaming use cases')
[CompletionResult]::new('--unbuffered' , 'unbuffered' , [CompletionResultType]::ParameterName, 'unbuffered')
[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,7 +186,6 @@ 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.')
+2 -4
View File
@@ -100,7 +100,6 @@ _bat() {
--lessopen | \
--no-paging | \
--diagnostic | \
--quiet-empty | \
--acknowledgements | \
-h | --help | \
-V | --version | \
@@ -117,7 +116,7 @@ _bat() {
return 0
;;
--wrap)
COMPREPLY=($(compgen -W "auto never character word" -- "$cur"))
COMPREPLY=($(compgen -W "auto never character" -- "$cur"))
return 0
;;
--binary)
@@ -190,8 +189,8 @@ _bat() {
$split && return 0
if [[ $cur == -* ]]; then
# --unbuffered excluded intentionally (no-op)
COMPREPLY=($(compgen -W "
--unbuffered
--show-all
--nonprintable-notation
--binary
@@ -228,7 +227,6 @@ _bat() {
--lessopen
--completion
--diagnostic
--quiet-empty
--acknowledgements
--set-terminal-title
--help
+2 -4
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 quiet-empty \
-l diagnostic \
-l list-languages -l list-themes
end
@@ -118,7 +118,6 @@ 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.
@@ -160,7 +159,6 @@ 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
@@ -240,7 +238,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 "Enable unbuffered input reading for streaming use cases" -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 V -l version -f -d "Show version information" -n __fish_is_first_arg
+2 -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 word)'
--wrap='[specify the text-wrapping mode]:mode [auto]:(auto never character)'
'!(--wrap)'{-S,--chop-long-lines}
--terminal-width='[explicitly set the width of the terminal instead of determining it automatically]:width'
'(-n --number --diff --diff-context)'{-n,--number}'[show line numbers]'
@@ -58,11 +58,10 @@ _{{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'[enable unbuffered input reading for streaming use cases]'
'(-u --unbuffered)'--unbuffered'[this option exists for POSIX-compliance reasons]'
--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]"
+4 -12
View File
@@ -102,10 +102,8 @@ 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, 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.
Specify the text\-wrapping mode (*auto*, never, character). The '\-\-terminal\-width' option
can be used in addition to control the output width.
.HP
\fB\-S\fR, \fB\-\-chop\-long\-lines\fR
.IP
@@ -265,10 +263,8 @@ Display a list of supported languages for syntax highlighting.
.HP
\fB\-u\fR, \fB\-\-unbuffered\fR
.IP
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.
This option exists for POSIX\-compliance reasons ('u' is for 'unbuffered'). The output is
always unbuffered \- this option is simply ignored.
.HP
\fB\-\-no\-custom\-assets\fR
.IP
@@ -286,10 +282,6 @@ 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.
+1 -2
View File
@@ -9,7 +9,6 @@ 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:
@@ -70,7 +69,7 @@ contexts:
escape: '(?={{section_heading}})'
function-call:
- match: '(?<!\e\[)(?:\b|\s)(?:{{ansi_escape_sequence}})?([A-Za-z0-9_\-]+\.)?([A-Za-z0-9_\-]+)(?:{{ansi_escape_sequence}})?(\()([^)]*)(\))'
- match: '\b([A-Za-z0-9_\-]+\.)?([A-Za-z0-9_\-]+)(\()([^)]*)(\))'
captures:
1: entity.name.function.man
2: entity.name.function.man
+53 -118
View File
@@ -47,49 +47,12 @@ impl ToTokens for MappingTarget {
}
}
/// 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);
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr)]
/// A single matcher.
///
/// Codegen converts this into a `Lazy<Option<GlobMatcher>>`.
#[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.
struct Matcher(Vec<MatcherSegment>);
/// Parse a matcher.
///
/// Note that this implementation is rather strict: it will greedily interpret
/// every valid environment variable replacement as such, then immediately
@@ -103,95 +66,68 @@ struct Matcher {
///
/// Revision history:
/// - 2024-02-20: allow `{` and `}` (glob brace expansion)
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());
impl FromStr for Matcher {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use MatcherSegment as Seg;
static VAR_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\$\{([\w\d_]+)\}").unwrap());
let mut segments = vec![];
let mut text_start = 0;
for capture in VAR_REGEX.captures_iter(s) {
let match_0 = capture.get(0).unwrap();
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()));
}
// 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 })
}
// this var
segments.push(Seg::Env(capture.get(1).unwrap().as_str().into()));
}
// possible trailing text
segments.push(Seg::Text(s[text_start..].into()));
// cleanup empty text segments
let non_empty_segments = segments
.into_iter()
.filter(|seg| seg.text().map(|t| !t.is_empty()).unwrap_or(true))
.collect_vec();
// sanity check
if non_empty_segments
.windows(2)
.any(|segs| segs[0].is_text() && segs[1].is_text())
{
unreachable!("Parsed into consecutive text segments: {non_empty_segments:?}");
}
// guard empty case
if non_empty_segments.is_empty() {
bail!(r#"Parsed an empty matcher: "{s}""#);
}
// guard variable syntax leftover fragments
if non_empty_segments
.iter()
.filter_map(Seg::text)
.any(|t| t.contains('$'))
{
bail!(r#"Invalid matcher: "{s}""#);
}
Ok(Self(non_empty_segments))
}
}
impl ToTokens for Matcher {
fn to_tokens(&self, tokens: &mut TokenStream) {
let case = &self.case;
let t = match self.segments.as_slice() {
let t = match self.0.as_slice() {
[] => unreachable!("0-length matcher should never be created"),
[MatcherSegment::Text(text)] => {
quote! { Lazy::new(|| Some(build_matcher_fixed(#text, #case))) }
quote! { Lazy::new(|| Some(build_matcher_fixed(#text))) }
}
// parser logic ensures that this case can only happen when there are dynamic segments
segs @ [_, ..] => {
quote! { Lazy::new(|| build_matcher_dynamic(&[ #(#segs),* ], #case)) }
}
segs @ [_, ..] => quote! { Lazy::new(|| build_matcher_dynamic(&[ #(#segs),* ])) },
};
tokens.append_all(t);
}
@@ -239,7 +175,6 @@ 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 {
+4 -18
View File
@@ -37,13 +37,6 @@ 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
@@ -68,8 +61,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, word). The '--terminal-width'
option can be used in addition to control the output width.
Specify the text-wrapping mode (*auto*, never, character). 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'.
@@ -210,11 +203,8 @@ Options:
Display a list of supported languages for syntax highlighting.
-u, --unbuffered
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.
This option exists for POSIX-compliance reasons ('u' is for 'unbuffered'). The output is
always unbuffered - this option is simply ignored.
--completion <SHELL>
Show shell completion for a certain shell. [possible values: bash, fish, zsh, ps1]
@@ -222,10 +212,6 @@ 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.
+1 -7
View File
@@ -17,8 +17,6 @@ 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>
@@ -28,7 +26,7 @@ Options:
--tabs <T>
Set the tab width to T spaces.
--wrap <mode>
Specify the text-wrapping mode (*auto*, never, character, word).
Specify the text-wrapping mode (*auto*, never, character).
-S, --chop-long-lines
Truncate all lines longer than screen width. Alias for '--wrap=never'.
-n, --number
@@ -60,12 +58,8 @@ 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
Generated
-27
View File
@@ -1,27 +0,0 @@
{
"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
@@ -1,40 +0,0 @@
{
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
];
};
}
);
};
}
+15 -143
View File
@@ -210,7 +210,6 @@ impl HighlightingAssets {
pub(crate) fn get_syntax(
&self,
language: Option<&str>,
fallback_syntax: Option<&str>,
input: &mut OpenedInput,
mapping: &SyntaxMapping,
) -> Result<SyntaxReferenceInSet<'_>> {
@@ -223,50 +222,21 @@ impl HighlightingAssets {
}
let path = input.path();
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)
})
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,
)
} 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 {
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))
}
}
// 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)),
_ => path_syntax,
}
}
@@ -292,24 +262,6 @@ 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
@@ -323,9 +275,6 @@ 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| {
@@ -446,12 +395,11 @@ 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, fallback_syntax, input, mapping)
.get_syntax(language, input, mapping)
.map(|syntax_in_set| syntax_in_set.syntax.name.clone())
.unwrap_or_else(|_| "!no syntax!".to_owned())
}
@@ -471,7 +419,7 @@ mod tests {
let dummy_stdin: &[u8] = &[];
let mut opened_input = input.open(dummy_stdin, None).unwrap();
self.get_syntax_name(None, None, &mut opened_input, &self.syntax_mapping)
self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
}
fn syntax_for_file_with_content_os(&self, file_name: &OsStr, first_line: &str) -> String {
@@ -481,7 +429,7 @@ mod tests {
let dummy_stdin: &[u8] = &[];
let mut opened_input = input.open(dummy_stdin, None).unwrap();
self.get_syntax_name(None, None, &mut opened_input, &self.syntax_mapping)
self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
}
#[cfg(unix)]
@@ -501,7 +449,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, None, &mut opened_input, &self.syntax_mapping)
self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
}
fn syntax_is_same_for_inputkinds(&self, file_name: &str, content: &str) -> bool {
@@ -713,55 +661,6 @@ 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() {
@@ -783,35 +682,8 @@ contexts:
let mut opened_input = input.open(dummy_stdin, None).unwrap();
assert_eq!(
test.get_syntax_name(None, None, &mut opened_input, &test.syntax_mapping),
test.get_syntax_name(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"
);
}
}
+13 -63
View File
@@ -50,11 +50,6 @@ pub struct App {
/// (not from config file or environment variables).
/// This is used to honor the flag when piping output, similar to `cat -n`.
number_from_cli: bool,
/// True if --wrap=character was passed on the command line
/// (not from config file or environment variables).
/// When piping output (non-interactive), --wrap=character is ignored unless
/// it was explicitly provided on the command line.
wrap_character_from_cli: bool,
}
impl App {
@@ -95,18 +90,6 @@ impl App {
false
});
// Check if --wrap=character was passed on the command line
// (before merging with config file and environment variables).
// This is needed to honor --wrap=character when piping output, while
// ignoring it when it comes only from the config file or BAT_OPTS.
let cli_args_vec: Vec<_> = wild::args_os().collect();
let wrap_character_from_cli = cli_args_vec
.iter()
.any(|arg| arg.to_string_lossy() == "--wrap=character")
|| cli_args_vec.windows(2).any(|pair| {
pair[0].to_string_lossy() == "--wrap" && pair[1].to_string_lossy() == "character"
});
let matches = Self::matches(interactive_output)?;
if matches.get_flag("help") {
@@ -147,7 +130,6 @@ impl App {
matches,
interactive_output,
number_from_cli,
wrap_character_from_cli,
})
}
@@ -402,10 +384,6 @@ 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
@@ -421,38 +399,27 @@ impl App {
Some("no-printing") => BinaryBehavior::NoPrinting,
_ => unreachable!("other values for --binary are not allowed"),
},
wrapping_mode: {
if self.matches.get_flag("chop-long-lines") {
WrappingMode::NoWrapping(true)
} else {
wrapping_mode: if self.interactive_output || maybe_term_width.is_some() {
if !self.matches.get_flag("chop-long-lines") {
match self.matches.get_one::<String>("wrap").map(|s| s.as_str()) {
Some("character") => {
if !self.interactive_output && !self.wrap_character_from_cli {
// When piping output (non-interactive), ignore --wrap=character
// unless it was explicitly provided on the command line.
Some("character") => WrappingMode::Character,
Some("never") => WrappingMode::NoWrapping(true),
Some("auto") | None => {
if style_components.plain() && maybe_term_width.is_none() {
WrappingMode::NoWrapping(false)
} else {
WrappingMode::Character
}
}
Some("word") => WrappingMode::Word,
Some("never") => WrappingMode::NoWrapping(true),
Some("auto") | None => {
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 {
// 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()) {
@@ -494,8 +461,6 @@ 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")
@@ -616,17 +581,7 @@ impl App {
// Plain if `--plain` is specified at least once.
if self.matches.get_count("plain") > 0 {
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));
return Some(StyleComponents(HashSet::from([StyleComponent::Plain])));
}
// Default behavior.
@@ -663,11 +618,6 @@ 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)
}
+15 -44
View File
@@ -1,5 +1,7 @@
use bat::style::StyleComponentList;
use clap::{crate_name, crate_version, value_parser, Arg, ArgAction, ColorChoice, Command};
use clap::{
crate_name, crate_version, value_parser, Arg, ArgAction, ArgGroup, ColorChoice, Command,
};
use once_cell::sync::Lazy;
use std::env;
use std::path::{Path, PathBuf};
@@ -120,17 +122,6 @@ 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")
@@ -222,11 +213,11 @@ pub fn build_app(interactive_output: bool) -> Command {
.long("wrap")
.overrides_with("wrap")
.value_name("mode")
.value_parser(["auto", "never", "character", "word"])
.value_parser(["auto", "never", "character"])
.default_value("auto")
.hide_default_value(true)
.help("Specify the text-wrapping mode (*auto*, never, character, word).")
.long_help("Specify the text-wrapping mode (*auto*, never, character, word). \
.help("Specify the text-wrapping mode (*auto*, never, character).")
.long_help("Specify the text-wrapping mode (*auto*, never, character). \
The '--terminal-width' option can be used in addition to \
control the output width."),
)
@@ -559,14 +550,11 @@ pub fn build_app(interactive_output: bool) -> Command {
.short('u')
.long("unbuffered")
.action(ArgAction::SetTrue)
.help("Enable unbuffered input reading for streaming use cases.")
.hide_short_help(true)
.long_help(
"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.",
"This option exists for POSIX-compliance reasons ('u' is for \
'unbuffered'). The output is always unbuffered - this option \
is simply ignored.",
),
)
.arg(
@@ -655,18 +643,6 @@ 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")
@@ -706,20 +682,11 @@ 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 \
@@ -731,9 +698,13 @@ 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")
+4 -55
View File
@@ -2,7 +2,7 @@ use std::env;
use std::ffi::OsString;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use crate::directories::PROJECT_DIRS;
@@ -104,32 +104,18 @@ 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();
let system_config = system_config_file();
let user_config = config_file();
if let Ok(c) = fs::read_to_string(&system_config) {
if let Ok(c) = fs::read_to_string(system_config_file()) {
config.push_str(&c);
config.push('\n');
}
// 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);
}
if let Ok(c) = fs::read_to_string(config_file()) {
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))
}
@@ -228,40 +214,3 @@ 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));
}
-9
View File
@@ -38,9 +38,6 @@ 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,
@@ -110,12 +107,6 @@ 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,7 +158,6 @@ 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())
@@ -328,9 +327,6 @@ impl Controller<'_> {
}
printer.print_line(false, writer, line_nr, &line, max_buffered_line_number)?;
if self.config.unbuffered {
writer.flush()?;
}
}
RangeCheckResult::AfterLastRange => {
break;
-105
View File
@@ -253,7 +253,6 @@ 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> {
@@ -277,7 +276,6 @@ impl<'a> InputReader<'a> {
inner: Box::new(reader),
first_line,
content_type,
unbuffered: false,
}
}
@@ -294,29 +292,9 @@ 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 read_utf16_line<R: BufRead>(
@@ -403,89 +381,6 @@ 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\
+5 -39
View File
@@ -23,18 +23,6 @@ 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 || {
@@ -117,10 +105,6 @@ 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());
}
};
@@ -147,13 +131,8 @@ 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'
// The BusyBox version of less does not support -K
if less_version != Some(LessVersion::BusyBox) {
p.arg("-K"); // Short version of '--quit-on-intr'
}
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.
@@ -163,7 +142,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 less_version {
match retrieve_less_version(&pager.bin) {
None => {
p.arg("--no-init");
}
@@ -190,13 +169,7 @@ impl OutputType {
Ok(p.stdin(Stdio::piped())
.spawn()
.map(OutputType::Pager)
.unwrap_or_else(|_| {
crate::bat_warning!(
"Pager '{}' not found, outputting to stdout instead",
&pager.bin
);
OutputType::stdout()
}))
.unwrap_or_else(|_| OutputType::stdout()))
}
pub(crate) fn stdout() -> Self {
@@ -237,8 +210,8 @@ impl Drop for OutputType {
let _ = command.wait();
}
OutputType::BuiltinPager(ref mut pager) => {
if let Some(handle) = pager.handle.take() {
let _ = handle.join();
if pager.handle.is_some() {
let _ = pager.handle.take().unwrap().join().unwrap();
}
}
OutputType::Stdout(_) => (),
@@ -258,11 +231,4 @@ 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(()),
}
}
}
+5 -75
View File
@@ -37,16 +37,6 @@ 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",
@@ -278,12 +268,7 @@ 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,
config.fallback_syntax,
input,
&config.syntax_mapping,
) {
match assets.get_syntax(config.language, input, &config.syntax_mapping) {
Ok(syntax_in_set) => (
syntax_in_set.syntax.name == PLAIN_TEXT_SYNTAX,
syntax_in_set.syntax.name == MANPAGE_SYNTAX
@@ -469,11 +454,6 @@ 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)?;
}
@@ -576,11 +556,6 @@ 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
@@ -796,21 +771,11 @@ 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 = char_width(c);
let cw = c.width().unwrap_or(0);
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 {
@@ -832,37 +797,13 @@ 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!(
"{}{}",
self.ansi_style,
&line_buf[..emit_end]
),
&format!("{}{line_buf}", self.ansi_style),
self.config.true_color,
self.config.colored_output,
self.config.use_italic_text,
@@ -875,19 +816,8 @@ impl Printer for InteractivePrinter<'_> {
cursor = 0;
max_width = cursor_max;
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.clear();
current_width = cw;
}
line_buf.push(c);
+3 -54
View File
@@ -17,16 +17,9 @@ use ignored_suffixes::IgnoredSuffixes;
mod builtin;
pub mod ignored_suffixes;
/// 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> {
fn make_glob_matcher(from: &str) -> Result<GlobMatcher> {
let matcher = GlobBuilder::new(from)
.case_insensitive(matches!(case, Case::Insensitive))
.case_insensitive(true)
.literal_separator(true)
.build()?
.compile_matcher();
@@ -104,14 +97,7 @@ impl<'a> SyntaxMapping<'a> {
}
pub fn insert(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> {
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)?;
let matcher = make_glob_matcher(from)?;
self.custom_mappings.push((matcher, to));
Ok(())
}
@@ -275,41 +261,4 @@ 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, Case, MappingTarget};
use crate::syntax_mapping::{make_glob_matcher, MappingTarget};
// Static syntax mappings generated from /src/syntax_mapping/builtins/ by the
// build script (/build/syntax_mapping.rs).
@@ -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, case: Case) -> GlobMatcher {
make_glob_matcher(from, case).expect("A builtin fixed glob matcher failed to compile")
fn build_matcher_fixed(from: &str) -> GlobMatcher {
make_glob_matcher(from).expect("A builtin fixed glob matcher failed to compile")
}
/// Join a list of matcher segments to create a glob string, replacing all
@@ -64,7 +64,7 @@ fn build_matcher_fixed(from: &str, case: Case) -> GlobMatcher {
/// to compile.
///
/// Used internally by `Lazy<Option<GlobMatcher>>`'s lazy evaluation closure.
fn build_matcher_dynamic(segs: &[MatcherSegment], case: Case) -> Option<GlobMatcher> {
fn build_matcher_dynamic(segs: &[MatcherSegment]) -> Option<GlobMatcher> {
// join segments
let mut buf = String::new();
for seg in segs {
@@ -77,7 +77,7 @@ fn build_matcher_dynamic(segs: &[MatcherSegment], case: Case) -> Option<GlobMatc
}
}
// compile glob matcher
let matcher = make_glob_matcher(&buf, case).ok()?;
let matcher = make_glob_matcher(&buf).ok()?;
Some(matcher)
}
+5 -12
View File
@@ -20,10 +20,12 @@ 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 "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.
The value of each key should be an array of strings, with each item being a glob
matcher. We will call each of these items a "rule".
For example, if `foo-application` uses both TOML and YAML configuration files,
we could write something like this:
@@ -96,15 +98,6 @@ 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
@@ -1,2 +0,0 @@
[mappings]
"Python" = [{ glob = "BUILD", case_sensitive = true }]
@@ -1,3 +0,0 @@
[mappings]
"INI" = [".boto", "**/gcloud/configurations/config_*"]
"Git Ignore" = [".gcloudignore"]
@@ -1,2 +0,0 @@
[mappings]
"Git Ignore" = [".?*ignore"]
-1
View File
@@ -1,7 +1,6 @@
#[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.
-3
View File
@@ -1,3 +0,0 @@
The quick brown fox jumps over the lazy dog and then runs away
superlongwordthatdefinitelyexceedstheterminalwidthandshouldfallbacktocharacterwrapping
short words here
-525
View File
@@ -1425,21 +1425,6 @@ 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() {
@@ -1457,7 +1442,6 @@ 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")
@@ -2470,121 +2454,6 @@ 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()
@@ -2681,25 +2550,6 @@ 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)
@@ -3034,91 +2884,6 @@ fn no_wrapping_with_chop_long_lines() {
wrapping_test("--chop-long-lines", false);
}
#[test]
fn line_wrapping_when_set_to_character() {
wrapping_test("--wrap=character", true);
}
#[test]
fn no_line_wrapping_when_character_wrap_from_config() {
// --wrap=character in a config file should be ignored when output is not interactive
// (e.g. when piping), since wrapping is only useful with a pager.
let tmp_dir = tempdir().expect("can create temporary directory");
let tmp_config_path = tmp_dir.path().join("wrap-character.conf");
std::fs::write(&tmp_config_path, "--wrap=character").expect("can write config file");
let expected = "abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz\n";
bat_with_config()
.env("BAT_CONFIG_PATH", tmp_config_path.to_str().unwrap())
.arg("--style=rule")
.arg("--color=never")
.arg("--decorations=always")
.arg("--terminal-width=80")
.arg("long-single-line.txt")
.assert()
.success()
.stdout(expected)
.stderr("");
}
#[test]
fn no_line_wrapping_when_character_wrap_from_bat_opts() {
// --wrap=character in BAT_OPTS should be ignored when output is not interactive,
// since it was not explicitly provided on the command line.
let expected = "abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz\n";
bat_with_config()
.env("BAT_OPTS", "--wrap=character")
.arg("--style=rule")
.arg("--color=never")
.arg("--decorations=always")
.arg("--terminal-width=80")
.arg("long-single-line.txt")
.assert()
.success()
.stdout(expected)
.stderr("");
}
#[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()
@@ -3889,293 +3654,3 @@ 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");
}
-61
View File
@@ -1,61 +0,0 @@
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
@@ -1,61 +0,0 @@
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
@@ -1,399 +0,0 @@
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)
-61
View File
@@ -1,61 +0,0 @@
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
@@ -1,61 +0,0 @@
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
@@ -1,399 +0,0 @@
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)