From fca5502f21451fc40c1d1e7b26e76c8f549ad83f Mon Sep 17 00:00:00 2001 From: ANCHAL Date: Tue, 16 Dec 2025 22:25:27 +0530 Subject: [PATCH 01/47] Fix bat crash with BusyBox less on Windows - Retrieve less version earlier in src/output.rs. - Skip -K argument if less is detected as BusyBox version. - Reuses the version check for the existing --no-init logic. - Fixes #3518. --- CHANGELOG.md | 1 + src/output.rs | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddefdcf5..720bb375 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk) ## Bugfixes +- Fix crash with BusyBox `less` on Windows, see #3518 (@Anchal-T) - `--help` now correctly honors `--pager=builtin`. See #3516 (@keith-hall) - `--help` now correctly honors custom themes. See #3524 (@keith-hall) diff --git a/src/output.rs b/src/output.rs index 0926f2bf..6c728c4f 100644 --- a/src/output.rs +++ b/src/output.rs @@ -131,8 +131,13 @@ impl OutputType { p.arg("-S"); // Short version of --chop-long-lines for compatibility } + let less_version = retrieve_less_version(&pager.bin); + // Ensures that 'less' quits together with 'bat' - p.arg("-K"); // Short version of '--quit-on-intr' + // The BusyBox version of less does not support -K + if less_version != Some(LessVersion::BusyBox) { + p.arg("-K"); // Short version of '--quit-on-intr' + } // Passing '--no-init' fixes a bug with '--quit-if-one-screen' in older // versions of 'less'. Unfortunately, it also breaks mouse-wheel support. @@ -142,7 +147,7 @@ impl OutputType { // For newer versions (530 or 558 on Windows), we omit '--no-init' as it // is not needed anymore. if single_screen_action == SingleScreenAction::Quit { - match retrieve_less_version(&pager.bin) { + match less_version { None => { p.arg("--no-init"); } From 8c7fccc8a5119c8677dd8d4dd00af72153111080 Mon Sep 17 00:00:00 2001 From: Anchal <155175388+Anchal-T@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:33:44 +0530 Subject: [PATCH 02/47] Update changelog for feature #3527 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 720bb375..3165707b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Features -- Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk) +- Improve native man pages and command help syntax highlighting by stripping overstriking, see #3527 (@akirk) ## Bugfixes - Fix crash with BusyBox `less` on Windows, see #3518 (@Anchal-T) From 782ce7124792799d00db5cb417d2cd3e3feb910d Mon Sep 17 00:00:00 2001 From: Anchal <155175388+Anchal-T@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:40:35 +0530 Subject: [PATCH 03/47] Update issue references in CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3165707b..c01dd3ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,10 @@ ## Features -- Improve native man pages and command help syntax highlighting by stripping overstriking, see #3527 (@akirk) +- Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk) ## Bugfixes -- Fix crash with BusyBox `less` on Windows, see #3518 (@Anchal-T) +- Fix crash with BusyBox `less` on Windows, see #3527 (@Anchal-T) - `--help` now correctly honors `--pager=builtin`. See #3516 (@keith-hall) - `--help` now correctly honors custom themes. See #3524 (@keith-hall) From eff57943c9d8a4fbc2c2726c191f5bda94408e8b Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Sun, 1 Feb 2026 14:01:19 +0100 Subject: [PATCH 04/47] Add initial flake.nix; just for develop, for now (fixes#3577) --- .envrc | 1 + .gitignore | 3 +++ flake.lock | 27 +++++++++++++++++++++++++++ flake.nix | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..3550a30f --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index fa381206..cd2047c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.direnv/ /target/ **/*.rs.bk @@ -12,3 +13,5 @@ /assets/completions/bat.zsh /assets/manual/bat.1 /assets/metadata.yaml + + diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..b8b992e7 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1769789167, + "narHash": "sha256-kKB3bqYJU5nzYeIROI82Ef9VtTbu4uA3YydSk/Bioa8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "62c8382960464ceb98ea593cb8321a2cf8f9e3e5", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..262b2901 --- /dev/null +++ b/flake.nix @@ -0,0 +1,40 @@ +{ + description = "bat"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = + { self, ... }@inputs: + let + supportedSystems = [ + "x86_64-linux" # 64-bit Intel/AMD Linux + "aarch64-linux" # 64-bit ARM Linux + "aarch64-darwin" # 64-bit ARM macOS + ]; + + forEachSupportedSystem = + f: + inputs.nixpkgs.lib.genAttrs supportedSystems ( + system: + f { + inherit system; + pkgs = import inputs.nixpkgs { + inherit system; + config.allowUnfree = true; + }; + } + ); + in + { + devShells = forEachSupportedSystem ( + { pkgs, system }: + { + default = pkgs.mkShellNoCC { + packages = with pkgs; [ + cargo + ]; + }; + } + ); + }; +} From d19c80a7c10f33d175113cbf0c4435aeb71e5b47 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Sun, 1 Feb 2026 14:09:18 +0100 Subject: [PATCH 05/47] Remove allowUnfree = true from flake.nix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- flake.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/flake.nix b/flake.nix index 262b2901..9afacc68 100644 --- a/flake.nix +++ b/flake.nix @@ -20,7 +20,6 @@ inherit system; pkgs = import inputs.nixpkgs { inherit system; - config.allowUnfree = true; }; } ); From 36b37e1aa56a82f16e174724206b035a7fafdf59 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Sun, 1 Feb 2026 14:11:05 +0100 Subject: [PATCH 06/47] Add x86_64-darwin (Intel macOS) to flake.nix --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index 9afacc68..35c8cec1 100644 --- a/flake.nix +++ b/flake.nix @@ -10,6 +10,7 @@ "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 = From 2c682ff6a62b00ab88c454a3226f15df23526d35 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Sun, 1 Feb 2026 14:13:03 +0100 Subject: [PATCH 07/47] docs: Document flake.nix in CHANGELOG.md (see #3577) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9934329f..214b1754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## Features +- Added an initial `flake.nix` for a ready made development environment; see #3577 (@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) From 3f12b1c1ab247b18ec3778f34eca4fd636bf4944 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Sun, 1 Feb 2026 14:20:40 +0100 Subject: [PATCH 08/47] docs: Use PR not Issue ID in CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 214b1754..31e50d4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ## Features -- Added an initial `flake.nix` for a ready made development environment; see #3577 (@vorburger) +- 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) From a4a7692134d99997d0bc8b4f31e1a150d0978e79 Mon Sep 17 00:00:00 2001 From: dddffgg Date: Fri, 6 Feb 2026 21:32:02 +0800 Subject: [PATCH 09/47] chore(deps): update time crate to 0.3.47 (RUSTSEC-2026-0009) --- Cargo.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 384b771b..6684e40b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -440,9 +440,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -1068,9 +1068,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-traits" @@ -1716,30 +1716,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", From a0f326aa22c04e0579d2193312618de4d32b7a6b Mon Sep 17 00:00:00 2001 From: dddffgg Date: Sat, 7 Feb 2026 19:23:47 +0800 Subject: [PATCH 10/47] chore: bump MSRV to 1.88 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c4bd2e2f..4a2025cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ exclude = ["assets/syntaxes/*", "assets/themes/*"] build = "build/main.rs" edition = '2021' # You are free to bump MSRV as soon as a reason for bumping emerges. -rust-version = "1.87" +rust-version = "1.88" [features] default = ["application", "git"] From 2b517199a28eb276c1cea7ef9d2cf68892054a10 Mon Sep 17 00:00:00 2001 From: dddffgg Date: Sat, 7 Feb 2026 19:23:57 +0800 Subject: [PATCH 11/47] docs: add changelog entry for MSRV bump and time crate update --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f96dbf1f..bac8f637 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ ## Other +- Bump MSRV to 1.88, update `time` crate to 0.3.47 to fix RUSTSEC-2026-0009, see #3581 (@NORMAL-EX) + ## Syntaxes - Change the URL of Zig submodule from GitHub to Codeberg, see #3519 (@sorairolake) From cb8b6375747241e45ac129278b0fc8f8e0979699 Mon Sep 17 00:00:00 2001 From: dddffgg Date: Mon, 9 Feb 2026 11:06:40 +0800 Subject: [PATCH 12/47] fix(cache): allow --help flag for cache subcommand (#3580) * fix(cache): allow --help flag for cache subcommand * docs: add changelog entry for cache --help fix --- CHANGELOG.md | 1 + src/bin/bat/clap_app.rs | 19 +++++++++++-------- tests/integration_tests.rs | 17 +++++++++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bac8f637..0dd98c0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ## Bugfixes - 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) diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index 10bf7a8b..8ee3604e 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -1,7 +1,5 @@ use bat::style::StyleComponentList; -use clap::{ - crate_name, crate_version, value_parser, Arg, ArgAction, ArgGroup, ColorChoice, Command, -}; +use clap::{crate_name, crate_version, value_parser, Arg, ArgAction, ColorChoice, Command}; use once_cell::sync::Lazy; use std::env; use std::path::{Path, PathBuf}; @@ -694,11 +692,20 @@ pub fn build_app(interactive_output: bool) -> Command { Command::new("cache") .hide(true) .about("Modify the syntax-definition and theme cache") + .arg_required_else_help(true) + .arg( + Arg::new("help") + .short('h') + .long("help") + .action(ArgAction::Help) + .help("Print help"), + ) .arg( Arg::new("build") .long("build") .short('b') .action(ArgAction::SetTrue) + .conflicts_with("clear") .help("Initialize (or update) the syntax/theme cache.") .long_help( "Initialize (or update) the syntax/theme cache by loading from \ @@ -710,13 +717,9 @@ pub fn build_app(interactive_output: bool) -> Command { .long("clear") .short('c') .action(ArgAction::SetTrue) + .conflicts_with("build") .help("Remove the cached syntax definitions and themes."), ) - .group( - ArgGroup::new("cache-actions") - .args(["build", "clear"]) - .required(true), - ) .arg( Arg::new("source") .long("source") diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 7b4e2df4..7d0f69cd 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -3684,3 +3684,20 @@ fn quiet_empty_suppresses_output_on_empty_file() { .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")); +} From 167dda63a878e2f8c09541f59b8ccb8c6ebde91e Mon Sep 17 00:00:00 2001 From: mainnebula <51970406+mainnebula@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:13:20 -0500 Subject: [PATCH 13/47] feat: implement --unbuffered mode for streaming input (#3555) Repurpose the existing --unbuffered/-u flag (previously a POSIX no-op) to enable unbuffered input reading using fill_buf()/consume() instead of read_until(b'\n'). This allows partial lines to display immediately when piping streaming input like `tail -f` into bat. - Add unbuffered field to Config and InputReader - Add read_line_unbuffered() using BufRead::fill_buf()/consume() - Add flush() to OutputHandle, called after each line in unbuffered mode - Auto-disable line numbers in unbuffered mode to avoid partial line confusion - Update help text, man page, and shell completions - Add unit tests and integration tests --- assets/completions/_bat.ps1.in | 2 +- assets/completions/bat.bash.in | 2 +- assets/completions/bat.fish.in | 2 +- assets/completions/bat.zsh.in | 2 +- assets/manual/bat.1.in | 6 +- doc/long-help.txt | 7 ++- doc/short-help.txt | 2 + src/bin/bat/app.rs | 6 ++ src/bin/bat/clap_app.rs | 11 ++-- src/config.rs | 3 + src/controller.rs | 4 ++ src/input.rs | 105 +++++++++++++++++++++++++++++++++ src/output.rs | 7 +++ tests/integration_tests.rs | 36 +++++++++++ 14 files changed, 183 insertions(+), 12 deletions(-) diff --git a/assets/completions/_bat.ps1.in b/assets/completions/_bat.ps1.in index db10302a..80eb6654 100644 --- a/assets/completions/_bat.ps1.in +++ b/assets/completions/_bat.ps1.in @@ -173,7 +173,7 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script # [CompletionResult]::new('-L' , 'L' , [CompletionResultType]::ParameterName, 'Display all supported languages.') [CompletionResult]::new('--list-languages' , 'list-languages' , [CompletionResultType]::ParameterName, 'Display all supported languages.') # [CompletionResult]::new('-u' , 'u' , [CompletionResultType]::ParameterName, 'u') - [CompletionResult]::new('--unbuffered' , 'unbuffered' , [CompletionResultType]::ParameterName, 'unbuffered') + [CompletionResult]::new('--unbuffered' , 'unbuffered' , [CompletionResultType]::ParameterName, 'Enable unbuffered input reading for streaming use cases') [CompletionResult]::new('--completion' , 'completion' , [CompletionResultType]::ParameterName, 'Show shell completion for a certain shell. [possible values: bash, fish, zsh, ps1]') [CompletionResult]::new('--no-config' , 'no-config' , [CompletionResultType]::ParameterName, 'Do not use the configuration file') [CompletionResult]::new('--no-custom-assets' , 'no-custom-assets' , [CompletionResultType]::ParameterName, 'Do not load custom assets') diff --git a/assets/completions/bat.bash.in b/assets/completions/bat.bash.in index 7593df8f..2cc360ab 100644 --- a/assets/completions/bat.bash.in +++ b/assets/completions/bat.bash.in @@ -190,8 +190,8 @@ _bat() { $split && return 0 if [[ $cur == -* ]]; then - # --unbuffered excluded intentionally (no-op) COMPREPLY=($(compgen -W " + --unbuffered --show-all --nonprintable-notation --binary diff --git a/assets/completions/bat.fish.in b/assets/completions/bat.fish.in index 21c07adf..57f3cd7d 100644 --- a/assets/completions/bat.fish.in +++ b/assets/completions/bat.fish.in @@ -239,7 +239,7 @@ complete -c $bat -l theme-dark -x -a "(command $bat --list-themes | command cat) complete -c $bat -l theme-light -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme for light backgrounds" -n __bat_no_excl_args -complete -c $bat -s u -l unbuffered -d "This option exists for POSIX-compliance reasons" -n __bat_no_excl_args +complete -c $bat -s u -l unbuffered -d "Enable unbuffered input reading for streaming use cases" -n __bat_no_excl_args complete -c $bat -s V -l version -f -d "Show version information" -n __fish_is_first_arg diff --git a/assets/completions/bat.zsh.in b/assets/completions/bat.zsh.in index da9a66e6..536cd8df 100644 --- a/assets/completions/bat.zsh.in +++ b/assets/completions/bat.zsh.in @@ -58,7 +58,7 @@ _{{PROJECT_EXECUTABLE}}_main() { default auto full plain changes header header-filename header-filesize grid rule numbers snip' \*{-r+,--line-range=}'[only print the specified line range]:start\:end' '(* -)'{-L,--list-languages}'[display all supported languages]' - '(-u --unbuffered)'--unbuffered'[this option exists for POSIX-compliance reasons]' + '(-u --unbuffered)'--unbuffered'[enable unbuffered input reading for streaming use cases]' --completion='[show shell completion for a certain shell]:shell:(bash fish zsh ps1)' --set-terminal-title'[sets terminal title to filenames when using a pager]' --diagnostic'[show diagnostic information for bug reports]' diff --git a/assets/manual/bat.1.in b/assets/manual/bat.1.in index ff4cb76f..e35b10f9 100644 --- a/assets/manual/bat.1.in +++ b/assets/manual/bat.1.in @@ -263,8 +263,10 @@ Display a list of supported languages for syntax highlighting. .HP \fB\-u\fR, \fB\-\-unbuffered\fR .IP -This option exists for POSIX\-compliance reasons ('u' is for 'unbuffered'). The output is -always unbuffered \- this option is simply ignored. +Enable unbuffered input reading. When this flag is set, bat will display data as soon as it +is available, without waiting for a complete line. This is useful for streaming use cases like +\&'tail \-f logfile | bat \-u \-\-paging=never'. Note that line numbers are automatically disabled +in unbuffered mode, and syntax highlighting may be imperfect on partial lines. .HP \fB\-\-no\-custom\-assets\fR .IP diff --git a/doc/long-help.txt b/doc/long-help.txt index 994a3810..d00c84e9 100644 --- a/doc/long-help.txt +++ b/doc/long-help.txt @@ -203,8 +203,11 @@ Options: Display a list of supported languages for syntax highlighting. -u, --unbuffered - This option exists for POSIX-compliance reasons ('u' is for 'unbuffered'). The output is - always unbuffered - this option is simply ignored. + Enable unbuffered input reading. When this flag is set, bat will display data as soon as + it is available, without waiting for a complete line. This is useful for streaming use + cases like 'tail -f logfile | bat -u --paging=never'. Note that line numbers are + automatically disabled in unbuffered mode, and syntax highlighting may be imperfect on + partial lines. --completion Show shell completion for a certain shell. [possible values: bash, fish, zsh, ps1] diff --git a/doc/short-help.txt b/doc/short-help.txt index 41a0fdee..04ca57a6 100644 --- a/doc/short-help.txt +++ b/doc/short-help.txt @@ -58,6 +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 Show shell completion for a certain shell. [possible values: bash, fish, zsh, ps1] -E, --quiet-empty diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index f5f59ec7..521674f2 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -462,6 +462,7 @@ impl App { _ => 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") @@ -619,6 +620,11 @@ 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) } diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index 8ee3604e..d286904a 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -548,11 +548,14 @@ pub fn build_app(interactive_output: bool) -> Command { .short('u') .long("unbuffered") .action(ArgAction::SetTrue) - .hide_short_help(true) + .help("Enable unbuffered input reading for streaming use cases.") .long_help( - "This option exists for POSIX-compliance reasons ('u' is for \ - 'unbuffered'). The output is always unbuffered - this option \ - is simply ignored.", + "Enable unbuffered input reading. When this flag is set, bat will \ + display data as soon as it is available, without waiting for a \ + complete line. This is useful for streaming use cases like \ + 'tail -f logfile | bat -u --paging=never'. Note that line numbers \ + are automatically disabled in unbuffered mode, and syntax \ + highlighting may be imperfect on partial lines.", ), ) .arg( diff --git a/src/config.rs b/src/config.rs index 7209c2fd..8ea0e275 100644 --- a/src/config.rs +++ b/src/config.rs @@ -110,6 +110,9 @@ pub struct Config<'a> { /// 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"))] diff --git a/src/controller.rs b/src/controller.rs index d50ffe54..8f64e8c5 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -158,6 +158,7 @@ impl Controller<'_> { #[cfg(not(feature = "lessopen"))] input.open(stdin, stdout_identifier)? }; + opened_input.reader.unbuffered = self.config.unbuffered; #[cfg(feature = "git")] let line_changes = if self.config.visible_lines.diff_mode() || (!self.config.loop_through && self.config.style_components.changes()) @@ -327,6 +328,9 @@ impl Controller<'_> { } printer.print_line(false, writer, line_nr, &line, max_buffered_line_number)?; + if self.config.unbuffered { + writer.flush()?; + } } RangeCheckResult::AfterLastRange => { break; diff --git a/src/input.rs b/src/input.rs index f807d125..29846abe 100644 --- a/src/input.rs +++ b/src/input.rs @@ -253,6 +253,7 @@ pub(crate) struct InputReader<'a> { inner: Box, pub(crate) first_line: Vec, pub(crate) content_type: Option, + pub(crate) unbuffered: bool, } impl<'a> InputReader<'a> { @@ -276,6 +277,7 @@ impl<'a> InputReader<'a> { inner: Box::new(reader), first_line, content_type, + unbuffered: false, } } @@ -292,9 +294,29 @@ 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) -> io::Result { + 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( @@ -381,6 +403,89 @@ fn utf16le() { assert!(buffer.is_empty()); } +#[test] +fn unbuffered_returns_partial_data() { + use std::io::Cursor; + + let content = b"first line\npartial"; + let mut reader = InputReader::new(Cursor::new(&content[..])); + reader.unbuffered = true; + + // First call returns first_line (buffered during new()) + let mut buffer = vec![]; + let res = reader.read_line(&mut buffer); + assert!(res.is_ok()); + assert!(res.unwrap()); + assert_eq!(b"first line\n", &buffer[..]); + + // Subsequent calls use unbuffered reading + buffer.clear(); + let res = reader.read_line(&mut buffer); + assert!(res.is_ok()); + assert!(res.unwrap()); + assert_eq!(b"partial", &buffer[..]); + + // EOF + buffer.clear(); + let res = reader.read_line(&mut buffer); + assert!(res.is_ok()); + assert!(!res.unwrap()); + assert!(buffer.is_empty()); +} + +#[test] +fn unbuffered_returns_complete_lines() { + use std::io::Cursor; + + let content = b"line1\nline2\n"; + let mut reader = InputReader::new(Cursor::new(&content[..])); + reader.unbuffered = true; + + // First call returns first_line + let mut buffer = vec![]; + let res = reader.read_line(&mut buffer); + assert!(res.is_ok()); + assert!(res.unwrap()); + assert_eq!(b"line1\n", &buffer[..]); + + // Second call returns line2 (complete line with newline) + buffer.clear(); + let res = reader.read_line(&mut buffer); + assert!(res.is_ok()); + assert!(res.unwrap()); + assert_eq!(b"line2\n", &buffer[..]); + + // EOF + buffer.clear(); + let res = reader.read_line(&mut buffer); + assert!(res.is_ok()); + assert!(!res.unwrap()); + assert!(buffer.is_empty()); +} + +#[test] +fn unbuffered_eof_handling() { + use std::io::Cursor; + + let content = b"only line\n"; + let mut reader = InputReader::new(Cursor::new(&content[..])); + reader.unbuffered = true; + + // First call returns first_line + let mut buffer = vec![]; + let res = reader.read_line(&mut buffer); + assert!(res.is_ok()); + assert!(res.unwrap()); + assert_eq!(b"only line\n", &buffer[..]); + + // EOF - empty buffer returns false + buffer.clear(); + let res = reader.read_line(&mut buffer); + assert!(res.is_ok()); + assert!(!res.unwrap()); + assert!(buffer.is_empty()); +} + #[test] fn utf16le_issue3367() { let content = b"\xFF\xFE\x0A\x4E\x00\x4E\x0A\x4F\x00\x52\x0A\x00\ diff --git a/src/output.rs b/src/output.rs index 6c728c4f..e5c8a654 100644 --- a/src/output.rs +++ b/src/output.rs @@ -236,4 +236,11 @@ impl OutputHandle<'_> { Self::FmtWrite(handle) => handle.write_fmt(args).map_err(Into::into), } } + + pub fn flush(&mut self) -> Result<()> { + match self { + Self::IoWrite(handle) => handle.flush().map_err(Into::into), + Self::FmtWrite(_) => Ok(()), + } + } } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 7d0f69cd..da8b21eb 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -3701,3 +3701,39 @@ fn cache_help_shows_help_message() { .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"); +} From 0c883d6f28859922dddbd2064a40a8d621490754 Mon Sep 17 00:00:00 2001 From: mainnebula <51970406+mainnebula@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:45:20 -0500 Subject: [PATCH 14/47] docs: add changelog entry for --unbuffered mode (#3583) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dd98c0f..a628e329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## Features +- 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) From ab393a02816e7e76b763abd53a55cdf8a418c563 Mon Sep 17 00:00:00 2001 From: Alex Dukhan Date: Thu, 12 Feb 2026 21:06:03 +0000 Subject: [PATCH 15/47] Add COBOL syntax highlighting --- .gitmodules | 3 + assets/syntaxes/02_Extra/COBOL | 1 + tests/syntax-tests/highlighted/COBOL/test.cbl | 61 +++++++++++++++++++ tests/syntax-tests/source/COBOL/test.cbl | 61 +++++++++++++++++++ 4 files changed, 126 insertions(+) create mode 160000 assets/syntaxes/02_Extra/COBOL create mode 100644 tests/syntax-tests/highlighted/COBOL/test.cbl create mode 100644 tests/syntax-tests/source/COBOL/test.cbl diff --git a/.gitmodules b/.gitmodules index ee84fe05..b64280c6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -278,3 +278,6 @@ [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 diff --git a/assets/syntaxes/02_Extra/COBOL b/assets/syntaxes/02_Extra/COBOL new file mode 160000 index 00000000..90f88bf6 --- /dev/null +++ b/assets/syntaxes/02_Extra/COBOL @@ -0,0 +1 @@ +Subproject commit 90f88bf65f339be3b42d6c490bca0a4a85cefad0 diff --git a/tests/syntax-tests/highlighted/COBOL/test.cbl b/tests/syntax-tests/highlighted/COBOL/test.cbl new file mode 100644 index 00000000..11e19fe9 --- /dev/null +++ b/tests/syntax-tests/highlighted/COBOL/test.cbl @@ -0,0 +1,61 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. PAYROLL-CALC. + +DATA DIVISION. +WORKING-STORAGE SECTION. + 01 EMPLOYEE-DETAILS. + 05 EMPLOYEE-NAME PIC X(30). + 05 HOURS-WORKED PIC 99V9. + 05 HOURLY-RATE PIC 99V99. + 01 PAY-CALCULATIONS. + 05 GROSS-PAY PIC 9(5)V99. + 05 OVERTIME-HOURS PIC 99V9. + 05 OVERTIME-PAY PIC 9(5)V99. + 05 TAX-RATE PIC V99 VALUE 0.10. + 05 TAX-AMOUNT PIC 9(5)V99. + 05 NET-PAY PIC 9(5)V99. + +PROCEDURE DIVISION. +MAIN-LOGIC. + DISPLAY "--- COBOL Payroll Calculator ---". + + DISPLAY "Enter Employee Name: ". + ACCEPT EMPLOYEE-NAME. + + DISPLAY "Enter Hours Worked (e.g., 40.5): ". + ACCEPT HOURS-WORKED. + + DISPLAY "Enter Hourly Rate (e.g., 15.75): ". + ACCEPT HOURLY-RATE. + + PERFORM CALCULATE-GROSS-PAY. + PERFORM CALCULATE-TAX. + PERFORM CALCULATE-NET-PAY. + PERFORM DISPLAY-RESULTS. + + STOP RUN. + +CALCULATE-GROSS-PAY. + IF HOURS-WORKED > 40 THEN + COMPUTE OVERTIME-HOURS = HOURS-WORKED - 40 + COMPUTE OVERTIME-PAY = OVERTIME-HOURS * HOURLY-RATE * 1.5 + COMPUTE GROSS-PAY = (40 * HOURLY-RATE) + OVERTIME-PAY + ELSE + COMPUTE GROSS-PAY = HOURS-WORKED * HOURLY-RATE + END-IF. + +CALCULATE-TAX. + COMPUTE TAX-AMOUNT = GROSS-PAY * TAX-RATE. + +CALCULATE-NET-PAY. + COMPUTE NET-PAY = GROSS-PAY - TAX-AMOUNT. + +DISPLAY-RESULTS. + DISPLAY "----------------------------------". + DISPLAY "Employee Name: " EMPLOYEE-NAME. + DISPLAY "Hours Worked: " HOURS-WORKED. + DISPLAY "Hourly Rate: " HOURLY-RATE. + DISPLAY "Gross Pay: " GROSS-PAY. + DISPLAY "Tax (10%): " TAX-AMOUNT. + DISPLAY "Net Pay: " NET-PAY. + DISPLAY "----------------------------------". \ No newline at end of file diff --git a/tests/syntax-tests/source/COBOL/test.cbl b/tests/syntax-tests/source/COBOL/test.cbl new file mode 100644 index 00000000..ab552167 --- /dev/null +++ b/tests/syntax-tests/source/COBOL/test.cbl @@ -0,0 +1,61 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. PAYROLL-CALC. + +DATA DIVISION. +WORKING-STORAGE SECTION. + 01 EMPLOYEE-DETAILS. + 05 EMPLOYEE-NAME PIC X(30). + 05 HOURS-WORKED PIC 99V9. + 05 HOURLY-RATE PIC 99V99. + 01 PAY-CALCULATIONS. + 05 GROSS-PAY PIC 9(5)V99. + 05 OVERTIME-HOURS PIC 99V9. + 05 OVERTIME-PAY PIC 9(5)V99. + 05 TAX-RATE PIC V99 VALUE 0.10. + 05 TAX-AMOUNT PIC 9(5)V99. + 05 NET-PAY PIC 9(5)V99. + +PROCEDURE DIVISION. +MAIN-LOGIC. + DISPLAY "--- COBOL Payroll Calculator ---". + + DISPLAY "Enter Employee Name: ". + ACCEPT EMPLOYEE-NAME. + + DISPLAY "Enter Hours Worked (e.g., 40.5): ". + ACCEPT HOURS-WORKED. + + DISPLAY "Enter Hourly Rate (e.g., 15.75): ". + ACCEPT HOURLY-RATE. + + PERFORM CALCULATE-GROSS-PAY. + PERFORM CALCULATE-TAX. + PERFORM CALCULATE-NET-PAY. + PERFORM DISPLAY-RESULTS. + + STOP RUN. + +CALCULATE-GROSS-PAY. + IF HOURS-WORKED > 40 THEN + COMPUTE OVERTIME-HOURS = HOURS-WORKED - 40 + COMPUTE OVERTIME-PAY = OVERTIME-HOURS * HOURLY-RATE * 1.5 + COMPUTE GROSS-PAY = (40 * HOURLY-RATE) + OVERTIME-PAY + ELSE + COMPUTE GROSS-PAY = HOURS-WORKED * HOURLY-RATE + END-IF. + +CALCULATE-TAX. + COMPUTE TAX-AMOUNT = GROSS-PAY * TAX-RATE. + +CALCULATE-NET-PAY. + COMPUTE NET-PAY = GROSS-PAY - TAX-AMOUNT. + +DISPLAY-RESULTS. + DISPLAY "----------------------------------". + DISPLAY "Employee Name: " EMPLOYEE-NAME. + DISPLAY "Hours Worked: " HOURS-WORKED. + DISPLAY "Hourly Rate: " HOURLY-RATE. + DISPLAY "Gross Pay: " GROSS-PAY. + DISPLAY "Tax (10%): " TAX-AMOUNT. + DISPLAY "Net Pay: " NET-PAY. + DISPLAY "----------------------------------". \ No newline at end of file From 9f8ac934abbdbb3a71270356aa51f18e635df79f Mon Sep 17 00:00:00 2001 From: Alex Dukhan Date: Thu, 12 Feb 2026 21:43:34 +0000 Subject: [PATCH 16/47] Updated CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a628e329..0ef08a67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - 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 #3479 (@adukhan99) ## Themes From 2b47f3c5eb314cdccc66319a8204304f75d22d5a Mon Sep 17 00:00:00 2001 From: Alex Dukhan Date: Thu, 12 Feb 2026 22:10:08 +0000 Subject: [PATCH 17/47] Updated CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ef08a67..181e1bc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ - 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 #3479 (@adukhan99) +- Add syntax highlighting support for COBOL, see #3584 (@adukhan99) ## Themes From 3ebfbb7ac50a0b2ccebb4fcef9ba37e4d52c1068 Mon Sep 17 00:00:00 2001 From: Guy Barzilai Date: Tue, 17 Feb 2026 21:44:46 +0200 Subject: [PATCH 18/47] Fixed manpage syntax to remove ANSI artifacts --- CHANGELOG.md | 1 + assets/syntaxes/02_Extra/Manpage.sublime-syntax | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a628e329..d8d93805 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - 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) +- Fixed manpage syntax so that ANSI escape codes don't get incorrectly highlighted and thus broken, see #3586 (@BlueElectivire) ## Themes diff --git a/assets/syntaxes/02_Extra/Manpage.sublime-syntax b/assets/syntaxes/02_Extra/Manpage.sublime-syntax index 0779ce04..80d1ff98 100644 --- a/assets/syntaxes/02_Extra/Manpage.sublime-syntax +++ b/assets/syntaxes/02_Extra/Manpage.sublime-syntax @@ -69,7 +69,7 @@ contexts: escape: '(?={{section_heading}})' function-call: - - match: '\b([A-Za-z0-9_\-]+\.)?([A-Za-z0-9_\-]+)(\()([^)]*)(\))' + - match: '(?:\s+)(?:\e\[[\?=]?(?:\d+;?)+[A-Za-z])?([A-Za-z0-9_\-^\e]+\.)?([A-Za-z0-9_\-]+)(?:\e\[[\?=]?(?:\d+;?)+[A-Za-z])?(\()([^)]*)(\))' captures: 1: entity.name.function.man 2: entity.name.function.man From 5bd08845a38029bf49c72d14f2fa3b9a6d650760 Mon Sep 17 00:00:00 2001 From: Guy Barzilai Date: Wed, 18 Feb 2026 23:59:38 +0200 Subject: [PATCH 19/47] Fixed manpage regex regression and add syntax test --- .../syntaxes/02_Extra/Manpage.sublime-syntax | 3 +- .../highlighted/Manpage/uwsm-0.26.3.man | 399 ++++++++++++++++++ .../source/Manpage/uwsm-0.26.3.man | 399 ++++++++++++++++++ 3 files changed, 800 insertions(+), 1 deletion(-) create mode 100644 tests/syntax-tests/highlighted/Manpage/uwsm-0.26.3.man create mode 100644 tests/syntax-tests/source/Manpage/uwsm-0.26.3.man diff --git a/assets/syntaxes/02_Extra/Manpage.sublime-syntax b/assets/syntaxes/02_Extra/Manpage.sublime-syntax index 80d1ff98..cebc28ac 100644 --- a/assets/syntaxes/02_Extra/Manpage.sublime-syntax +++ b/assets/syntaxes/02_Extra/Manpage.sublime-syntax @@ -9,6 +9,7 @@ scope: source.man variables: section_heading: '^(?!#)\S.*$' command_line_option: '(--?[A-Za-z0-9][_A-Za-z0-9-]*)' + ansi_escape_sequence: '\e\[[\?=]?(?:\d+;?)*[A-Za-z]' contexts: prototype: @@ -69,7 +70,7 @@ contexts: escape: '(?={{section_heading}})' function-call: - - match: '(?:\s+)(?:\e\[[\?=]?(?:\d+;?)+[A-Za-z])?([A-Za-z0-9_\-^\e]+\.)?([A-Za-z0-9_\-]+)(?:\e\[[\?=]?(?:\d+;?)+[A-Za-z])?(\()([^)]*)(\))' + - match: '(? "${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) diff --git a/tests/syntax-tests/source/Manpage/uwsm-0.26.3.man b/tests/syntax-tests/source/Manpage/uwsm-0.26.3.man new file mode 100644 index 00000000..9e63ec42 --- /dev/null +++ b/tests/syntax-tests/source/Manpage/uwsm-0.26.3.man @@ -0,0 +1,399 @@ +UWSM(1) General Commands Manual UWSM(1) + +NAME + UWSM - Universal Wayland Session Manager. + +SYNOPSIS + uwsm [-h|-v] {subcommand} [options ...] + +DESCRIPTION + Launches arbitrary wayland compositor via a set of systemd user units to provide graphical user + session with environment management, XDG autostart support, clean shutdown. Provides helpers + for launching applications as scopes or services. + +SUBCOMMANDS + select Select default compositor Entry. + start Start compositor and graphical session. + finalize Send compositor-set variables and unit startup notification to systemd user manager. + stop Stop graphical session and compositor. + app Application unit launcher (with Desktop Entry support). + check Perform state checks (for scripting and info). + aux Technical functions for use inside units. + + See corresponding SUBCOMMANDS subsections below for further info. + + Help for each subcommand is accessible by running "uwsm {subcommand} -h". + +CONFIGURATION + Files + In XDG config hierarchy: + uwsm/env + uwsm/env.d/* + uwsm/env-${compositor} + uwsm/env-${compositor}.d/* Environment (shell) to be sourced for the graphical session. + Sourced from directories of increasing priority, in each directory + common file is sourced first, then suffixed files in the order of + items listed in XDG_CURRENT_SESSION var (lowercased). + uwsm/default-id Stores Desktop Entry ID of default compositor. + + Fallback is also extended into the system part of XDG data hierarchy, this can be used for dis‐ + tro level defaults. + + Environment vars + UWSM_UNIT_RUNG (run|home) + Which rung of systemd/user/ hierarchy to manage generated unit + and drop-in files in: $XDG_RUNTIME_DIR or $XDG_CONFIG_HOME. + UWSM_TWEAKS (boolean value) + Set to False to remove and not generate tweak drop-ins for + other software. + UWSM_FINALIZE_VARNAMES (whitespace-separated names of env vars) + Additional variables for "uwsm finalize". + UWSM_WAIT_VARNAMES (whitespace-separated names of env vars) + Variables to wait for in activation environment before proceed‐ + ing to graphical session (in addition to WAYLAND_DISPLAY). + UWSM_WAIT_VARNAMES_TIMEOUT (int value) + Seconds to wait for variables to appear in activation environ‐ + ment. Essentially, startup timeout (default: 10). + UWSM_WAIT_VARNAMES_SETTLETIME (float value) + Seconds to pause after all expected vars found in activation + environment (default: 0.2). + UWSM_APP_UNIT_TYPE (scope|service) + Default unit type for launching apps (default: scope). + UWSM_SILENT_START (int or boolean value) + True or 1 to inhibit stdout messages from "uwsm start". 2 to + also inhibit warnings. + DEBUG (int or boolean value) + True or positive number to dump debug info to stderr. + +OPERATION OVERVIEW + Login Sequence Integration + uwsm can be launched by using conditional exec in shell profile to replace login shell (see + Shell Profile Integration section). + + Alternatively "uwsm start ..." command can be put into wayland session's Desktop Entry to be + launched by a display manager (see Use Inside Desktop Entry section). + + Compositor Selection + uwsm can run arbitrary compositor command line or a Desktop Entry by ID (specifying Action ID + is also supported). + + Desktop Entry can also be selected via a whiptail menu (see select subcommand section). + + Startup + See start subcommand section for command syntax. + + UWSM uses a set of units bound to standard user session targets: + + • wayland-session-pre@.target (bound to graphical-session-pre.target) + • wayland-wm-env@.service (environment preloader service) + • wayland-session@.target (bound to graphical-session.target) + • wayland-wm@.service (service for the selected compositor) + • wayland-session-xdg-autostart@.target (bound to xdg-desktop-autostart.target) + • wayland-session-envelope@.target (lives through entire lifecycle) + • wayland-session-shutdown.target (conflicts with targets above for shutdown) + • wayland-session-bindpid@.service (PID-tracking session killswitch) + • wayland-session-waitenv.service (delays graphical session until vars appear) + + Compositor ID (Desktop Entry ID or executable name) becomes the specifier for all templated + units. + + At the stage of graphical-session-pre.target, the environment saved from "uwsm start" context + is loaded (or POSIX shell profile is sourced), uwsm environment files are sourced. The delta is + exported to the systemd and D-Bus activation environments by the environment preloader service + and is marked for cleanup at shutdown stage. Preloader shell context for convenience has + IN_UWSM_ENV_PRELOADER var set to true. + + At the stage of graphical-session.target (before it) the main compositor unit wayland- + wm@${ID}.service and wayland-session-waitenv.service are started. + + Compositor should at least put WAYLAND_DISPLAY variable to systemd activation environment. This + will trigger uwsm's automatic finalization logic. Without WAYLAND_DISPLAY in activation envi‐ + ronment startup will timeout in 10 seconds. + + Manual finalization is possible by running "uwsm finalize" (see finalize subcommand section), + also in combination with tweaking UWSM_WAIT_VARNAMES and UWSM_WAIT_VARNAMES_SETTLETIME vars + (see Environment vars section). + + Successful activation of compositor unit and existence of WAYLAND_DISPLAY in activation envi‐ + ronment will allow graphical-session.target to be declared reached. + + Finally, xdg-desktop-autostart.target is activated. + + Inside session + It is highly recommended to configure the compositor or app launcher to launch apps as scopes + or services in special user session slices (app.slice, background.slice, session.slice). uwsm + provides custom nested slices for apps to live in and be terminated on session end: + • app-graphical.slice + • background-graphical.slice + • session-graphical.slice + + A helper app subcommand is provided to handle all the systemd-run invocations for you (see app + subcommand section). + + The compositor is launched in session.slice by default (as recommended by systemd.special(7)). + + Shutdown + Can be initiated by either: + • running uwsm stop + • stopping wayland-wm@*.service or wayland-session-envelope@*.target + • starting wayland-session-shutdown.target + + Systemd stops all user units in reverse, as it usually does. During deactivation of graphical- + session-pre.target, the environment preloader service cleans activation environments by unset‐ + ting all variables that were marked for removal during startup and finalization stages. + + Do not use compositor's native exit mechanism or kill its process directly. + +SUBCOMMANDS + select + Selects default wayland session compositor Desktop Entry. + + uwsm select + + Invokes a whiptail menu to select default session among Desktop Entries in wayland-sessions XDG + data hierarchy. Writes to ${XDG_CONFIG_HOME}/uwsm/default-id. Nothing else is done. Returns 1 + if selection is cancelled. Can be used for scripting launch condition in shell profile. + + check + Performs tests, returns 0 on success, 1 on failure. + + is-active: + + uwsm check is-active [-h] [-v] [compositor] + + -v show additional info + compositor check for specific compositor + + Checks if unit of specific compositor or graphical-session*.target in general is in active or + activating state. + + may-start: + + uwsm check may-start [-h] [-g [S]] [-v|-q] [N ...] + + N ... allowed VT numbers (default: 1) + -g S wait S seconds for graphical.target in queue (default: 60; 0 or less disables + check). + -i do not check for login shell + -r do not check for local session (allow remote session) + -v show all failed tests + -q be quiet + + Checks whether it is OK to launch a wayland session via the following conditions: + • DBUS_SESSION_BUS_ADDRESS is set + • Running from login shell + • System is at graphical.target + • User graphical-session*.target units are not yet active + • Foreground VT is among allowed (default: 1) + • Login session's VT is matching + + start + Generates units for given compositor command line or Desktop Entry and starts them. + + uwsm start [-h] [-D name[:name...]] [-a|-e] [-N Name] [-C Comment] [-U {run|home}] [-t] + [-o] [-n] -- compositor [args ...] + + -F Hardcode mode, always write command line to unit drop-ins and use full + paths. + -D name[:name...] Names to fill XDG_CURRENT_DESKTOP with (:-separated). Existing var con‐ + tent is a starting point if no active session is running. + -a Append desktop names set by -D to other sources (default). + -e Use desktop names set by -D exclusively, discard other sources. + -N Name Fancy name for compositor (filled from Desktop Entry by default). + -C Comment Fancy description for compositor (filled from Desktop Entry by de‐ + fault). + -U {run|home} Select rung for generated unit files: run: $XDG_RUNTIME_DIR/sys‐ + temd/user (default), or home: $XDG_CONFIG_HOME/systemd/user. Permanent + destination will save some time by removing need for reloading systemd. + Managed files from other rung will be removed. Can be preset with + UWSM_UNIT_RUNG environment var. + -t Do not generate (and remove) tweak unit files. Can be preset with + UWSM_TWEAKS=false environment var. + -T Generate tweak unit files for other software. This is default behavior. + -g S Wait for S seconds for system graphical.target in queue and warn if + timed out or not in queue (default: 60, negative to disable). + -G S Wait for S seconds for system graphical.target in queue and abort if + timed out or not in queue (overrides -g, default: -1, (disabled)). + -o Only generate units, but do not start. + -n Dry run, do not write or start anything. + + The first argument of the compositor command line acts as an ID and should be either one of: + • Executable name + • Desktop Entry ID (optionally with ":"-delimited action ID) + • Special value: + • select - invoke menu to select compositor. + • default - run previously selected compositor (or select if no selection was saved). + + If given as path, hardcode mode will be used implicitly. + + Always use "--" to disambiguate dashed arguments intended for compositor itself. + + After units are (re)generated, wayland-session-bindpid@${PID}.service is started, to track the + PID of invoking uwsm, then uwsm process replaces itself with systemctl execution that starts + wayland-wm@${ID}.service and waits for it to finish. + + In order to complete the startup sequence, the compositor has to put WAYLAND_DISPLAY into the + systemd activation environment. This can be done explicitly by making compositor run "uwsm fi‐ + nalize" command (see the next subsection). + + finalize + For running by a compositor on startup. + + uwsm finalize [-h] [VAR_NAME ...] + + Exports WAYLAND_DISPLAY, DISPLAY and any defined vars mentioned by names in arguments or in + UWSM_FINALIZE_VARNAMES variable (whitespace-separated). Then sends startup notification for the + unit to systemd user manager. + + This is required if compositor itself does not put WAYLAND_DISPLAY to systemd activation envi‐ + ronment, otherwise wayland-session@.service unit or a dedicated wayland-session-waitenv.service + unit will terminate due to startup timeout. + + UWSM_FINALIZE_VARNAMES variable can be prefilled by plugins. + + Direct assignment as VAR_NAME=value is also possible, but recommended only for creating flags + for UWSM_WAIT_VARNAMES mechanism. + + stop + Stops compositor and optionally removes generated units. + + uwsm stop [-h] [-r [compositor] [-U {run|home}] [-n] + + -r [compositor] Also remove units (all or only compositor-specific). + -U {run|home} Select rung for generated unit files: run: $XDG_RUNTIME_DIR/systemd/user + (default), or home: $XDG_CONFIG_HOME/systemd/user. Permanent destination + will save some time by removing need for reloading systemd. Managed files + from other rung will be removed. Can be preset with UWSM_UNIT_RUNG envi‐ + ronment var. + -n Dry run, do not stop or remove anything. + + app + Application-to-unit launcher with Desktop Entry support. + + uwsm app [-h] [-s {a,b,s,custom.slice}] [-t {scope,service}] [-a app_name] [-u unit_name] + [-d unit_description] [-S ] [-T] -- application [args ...] + + -s {a,b,s,custom.slice} Slice selector (default: a): + a - app-graphical.slice + b - background-graphical.slice + s - session-graphical.slice + any slice by full name + -t {scope,service} Type of unit to launch (default: scope, can be preset by + UWSM_APP_UNIT_TYPE env var). + -a app_name Override app name (a substring in unit name). + -u unit_name Override the whole autogenerated unit name. + -d unit_description Unit Description. + -p Property=value Set additional unit property (option is repeatable). + -S {out,err,both} Silence stdout, stderr, or both. + -T Launch app in a terminal. Allows command to be empty to just + launch a terminal. + + Application can be provided as a command with optional arguments, or a Desktop Entry ID, op‐ + tionally suffixed with ":"-delimited Action ID. If Desktop Entry is being launched, arguments + should be compatible with it. + + Always use "--" to disambiguate dashed arguments intended for application itself. + + aux + For use in systemd user services. Can only be called by systemd user manager. + + prepare-env Prepares environment (for use in ExecStart in wayland-wm-env@.service bound to + wayland-session-pre@.target). + cleanup-env Cleans up environment (for use ExecStop in in wayland-wm-env@.service bound to + wayland-session-pre@.target). + exec Executes a command with arguments or a desktop entry (for use in Exec in wayland- + wm@.service bound to wayland-session@.target). + app-daemon Daemon for faster app argument generation, used by uwsm-app client. + +APP DAEMON + Provided as wayland-wm-app-daemon.service to be started on-demand. + + Daemon receives app arguments from ${XDG_RUNTIME_DIR}/uwsm-app-daemon-in pipe. Resulting argu‐ + ments are formatted as shell code and written to ${XDG_RUNTIME_DIR}/uwsm-app-daemon-out pipe. + + Arguments are expected to be \0-delimited, leading \0 are stripped. One command is received per + write+close. + + The first argument determines the behavior: + + • app the rest is processed the same as in "uwsm app" + • ping just "pong" is returnedn + • stop daemon is stoppedn + + Single commands are prepended with exec, iterated commands are assembled with trailing & each, + followed by wait. + + The purpose of all this is to skip all the expensive Python startup and import routines that + slow things down every time "uwsm app" is called. Instead the daemon does it once and then lis‐ + tens for requests, while a simple shell script may dump arguments to one pipe and run the code + received from another via eval, which is much faster. + + The simplest script is: + + #!/bin/sh + printf '0%s' app "$@" > "${XDG_RUNTIME_DIR}/uwsm-app-daemon-in" + IFS='' read -r cmd < "${XDG_RUNTIME_DIR}/uwsm-app-daemon-out" + eval "$cmd" + + Provided uwsm-app client script is a bit smarter: it can start the daemon, applies timeouts, + and supports newlines in returned args. + +SHELL PROFILE INTEGRATION + To launch uwsm automatically on login, add one of constructs below (or similar) to shell pro‐ + file. + + This asks to select a compositor (or refuse and continue with login shell) when logged in on VT + 1: + + if uwsm check may-start && uwsm select; then + exec systemd-cat -t uwsm_start uwsm start default + fi + + This just starts a specific compositor depending on foreground VT: + + if uwsm check may-start 1; then + exec systemd-cat -t uwsm_start uwsm start sway.desktop + elif uwsm check may-start 2; then + exec systemd-cat -t uwsm_start uwsm start labwc.desktop + fi + + Using "uwsm check may-start" as a condition is essential, not only to prevent accidental + startup attempts where they are not expected, but also since startup may involve sourcing shell + profile, which might lead to nasty loops. + + See check subcommand section for info on may-start checker. + + exec allows uwsm to replace login shell in order to properly bind to user session and handle + session termination. + + "systemd-cat -t uwsm_start" (optional) executes the command given to it (uwsm) with its stdout + and stderr connected to the systemd journal, tagged with identifier "uwsm_start". See systemd- + cat(1) for more options. + +USE INSIDE DESKTOP ENTRY + To launch uwsm from a display/login manager, "uwsm start" can be used inside Desktop Entries. + Example /usr/local/share/wayland-sessions/my-compositor.desktop: + + [Desktop Entry] + Name=My compositor (with UWSM) + Comment=My cool compositor + Exec=uwsm start -N "My compositor" -D mycompositor -C "My cool compositor" mywm + DesktopNames=mycompositor + Type=Application + + Things to keep in mind: + + • For consistency, command line arguments should mirror the keys of the entry + • Command in Exec= should start with "uwsm start" + • It should not point to itself (as a combination of Desktop Entry ID and Action ID) + • It should not point to a Desktop Entry ID and Action ID that also uses ‘uwsm‘ + + Potentially such entries may be found and used by uwsm itself, i.e. in shell profile integra‐ + tion situation, or when launched manually. Following the principles above ensures uwsm will + properly recognize itself and parse requested arguments inside the entry without any side ef‐ + fects. + +SEE ALSO + uwsm-plugins(3), systemd-run(1), systemd-cat(1), systemd.special(7) + + 2026-02-14 UWSM(1) From a71d16fa1b9f8b2b0790e728e3934dc34f76593d Mon Sep 17 00:00:00 2001 From: Guy Barzilai Date: Thu, 19 Feb 2026 18:11:36 +0200 Subject: [PATCH 20/47] Fixed manpage syntax-test format --- .../highlighted/Manpage/uwsm-0.26.3.man | 752 +++++++++--------- 1 file changed, 376 insertions(+), 376 deletions(-) diff --git a/tests/syntax-tests/highlighted/Manpage/uwsm-0.26.3.man b/tests/syntax-tests/highlighted/Manpage/uwsm-0.26.3.man index 8395d816..319020e8 100644 --- a/tests/syntax-tests/highlighted/Manpage/uwsm-0.26.3.man +++ b/tests/syntax-tests/highlighted/Manpage/uwsm-0.26.3.man @@ -1,399 +1,399 @@ -UWSM(1) General Commands Manual UWSM(1) - -NAME - UWSM - Universal Wayland Session Manager. - -SYNOPSIS - uwsm [-h|-v] {subcommand} [options ...] - -DESCRIPTION - Launches arbitrary wayland compositor via a set of systemd user units to provide graphical user - session with environment management, XDG autostart support, clean shutdown. Provides helpers - for launching applications as scopes or services. - -SUBCOMMANDS - select Select default compositor Entry. - start Start compositor and graphical session. - finalize Send compositor-set variables and unit startup notification to systemd user manager. - stop Stop graphical session and compositor. - app Application unit launcher (with Desktop Entry support). - check Perform state checks (for scripting and info). - aux Technical functions for use inside units. - - See corresponding SUBCOMMANDS subsections below for further info. - - Help for each subcommand is accessible by running "uwsm {subcommand} -h". - -CONFIGURATION - Files - In XDG config hierarchy: - uwsm/env - uwsm/env.d/* - uwsm/env-${compositor} - uwsm/env-${compositor}.d/* Environment (shell) to be sourced for the graphical session. - Sourced from directories of increasing priority, in each directory - common file is sourced first, then suffixed files in the order of - items listed in XDG_CURRENT_SESSION var (lowercased). - uwsm/default-id Stores Desktop Entry ID of default compositor. - - Fallback is also extended into the system part of XDG data hierarchy, this can be used for dis‐ - tro level defaults. - - Environment vars - UWSM_UNIT_RUNG (run|home) - Which rung of systemd/user/ hierarchy to manage generated unit - and drop-in files in: $XDG_RUNTIME_DIR or $XDG_CONFIG_HOME. - UWSM_TWEAKS (boolean value) - Set to False to remove and not generate tweak drop-ins for - other software. - UWSM_FINALIZE_VARNAMES (whitespace-separated names of env vars) - Additional variables for "uwsm finalize". - UWSM_WAIT_VARNAMES (whitespace-separated names of env vars) - Variables to wait for in activation environment before proceed‐ - ing to graphical session (in addition to WAYLAND_DISPLAY). - UWSM_WAIT_VARNAMES_TIMEOUT (int value) - Seconds to wait for variables to appear in activation environ‐ - ment. Essentially, startup timeout (default: 10). - UWSM_WAIT_VARNAMES_SETTLETIME (float value) - Seconds to pause after all expected vars found in activation - environment (default: 0.2). - UWSM_APP_UNIT_TYPE (scope|service) - Default unit type for launching apps (default: scope). - UWSM_SILENT_START (int or boolean value) - True or 1 to inhibit stdout messages from "uwsm start". 2 to - also inhibit warnings. - DEBUG (int or boolean value) - True or positive number to dump debug info to stderr. - -OPERATION OVERVIEW - Login Sequence Integration - uwsm can be launched by using conditional exec in shell profile to replace login shell (see - Shell Profile Integration section). - - Alternatively "uwsm start ..." command can be put into wayland session's Desktop Entry to be - launched by a display manager (see Use Inside Desktop Entry section). - - Compositor Selection - uwsm can run arbitrary compositor command line or a Desktop Entry by ID (specifying Action ID - is also supported). - - Desktop Entry can also be selected via a whiptail menu (see select subcommand section). - - Startup - See start subcommand section for command syntax. - - UWSM uses a set of units bound to standard user session targets: - - • wayland-session-pre@.target (bound to graphical-session-pre.target) - • wayland-wm-env@.service (environment preloader service) - • wayland-session@.target (bound to graphical-session.target) - • wayland-wm@.service (service for the selected compositor) - • wayland-session-xdg-autostart@.target (bound to xdg-desktop-autostart.target) - • wayland-session-envelope@.target (lives through entire lifecycle) - • wayland-session-shutdown.target (conflicts with targets above for shutdown) - • wayland-session-bindpid@.service (PID-tracking session killswitch) - • wayland-session-waitenv.service (delays graphical session until vars appear) - - Compositor ID (Desktop Entry ID or executable name) becomes the specifier for all templated - units. - - At the stage of graphical-session-pre.target, the environment saved from "uwsm start" context - is loaded (or POSIX shell profile is sourced), uwsm environment files are sourced. The delta is - exported to the systemd and D-Bus activation environments by the environment preloader service - and is marked for cleanup at shutdown stage. Preloader shell context for convenience has - IN_UWSM_ENV_PRELOADER var set to true. - - At the stage of graphical-session.target (before it) the main compositor unit wayland- - wm@${ID}.service and wayland-session-waitenv.service are started. - - Compositor should at least put WAYLAND_DISPLAY variable to systemd activation environment. This - will trigger uwsm's automatic finalization logic. Without WAYLAND_DISPLAY in activation envi‐ - ronment startup will timeout in 10 seconds. - - Manual finalization is possible by running "uwsm finalize" (see finalize subcommand section), - also in combination with tweaking UWSM_WAIT_VARNAMES and UWSM_WAIT_VARNAMES_SETTLETIME vars - (see Environment vars section). - - Successful activation of compositor unit and existence of WAYLAND_DISPLAY in activation envi‐ - ronment will allow graphical-session.target to be declared reached. - - Finally, xdg-desktop-autostart.target is activated. - - Inside session - It is highly recommended to configure the compositor or app launcher to launch apps as scopes - or services in special user session slices (app.slice, background.slice, session.slice). uwsm - provides custom nested slices for apps to live in and be terminated on session end: - • app-graphical.slice - • background-graphical.slice - • session-graphical.slice - - A helper app subcommand is provided to handle all the systemd-run invocations for you (see app - subcommand section). - - The compositor is launched in session.slice by default (as recommended by systemd.special(7)). - - Shutdown - Can be initiated by either: - • running uwsm stop - • stopping wayland-wm@*.service or wayland-session-envelope@*.target - • starting wayland-session-shutdown.target - - Systemd stops all user units in reverse, as it usually does. During deactivation of graphical- - session-pre.target, the environment preloader service cleans activation environments by unset‐ - ting all variables that were marked for removal during startup and finalization stages. - - Do not use compositor's native exit mechanism or kill its process directly. - -SUBCOMMANDS - select - Selects default wayland session compositor Desktop Entry. - - uwsm select - - Invokes a whiptail menu to select default session among Desktop Entries in wayland-sessions XDG - data hierarchy. Writes to ${XDG_CONFIG_HOME}/uwsm/default-id. Nothing else is done. Returns 1 - if selection is cancelled. Can be used for scripting launch condition in shell profile. - - check - Performs tests, returns 0 on success, 1 on failure. - - is-active: - - uwsm check is-active [-h] [-v] [compositor] - - -v show additional info - compositor check for specific compositor - - Checks if unit of specific compositor or graphical-session*.target in general is in active or - activating state. - - may-start: - - uwsm check may-start [-h] [-g [S]] [-v|-q] [N ...] - - N ... allowed VT numbers (default: 1) - -g S wait S seconds for graphical.target in queue (default: 60; 0 or less disables - check). - -i do not check for login shell - -r do not check for local session (allow remote session) - -v show all failed tests - -q be quiet - - Checks whether it is OK to launch a wayland session via the following conditions: - • DBUS_SESSION_BUS_ADDRESS is set - • Running from login shell - • System is at graphical.target - • User graphical-session*.target units are not yet active - • Foreground VT is among allowed (default: 1) - • Login session's VT is matching - - start - Generates units for given compositor command line or Desktop Entry and starts them. - - uwsm start [-h] [-D name[:name...]] [-a|-e] [-N Name] [-C Comment] [-U {run|home}] [-t] - [-o] [-n] -- compositor [args ...] - - -F Hardcode mode, always write command line to unit drop-ins and use full - paths. - -D name[:name...] Names to fill XDG_CURRENT_DESKTOP with (:-separated). Existing var con‐ - tent is a starting point if no active session is running. - -a Append desktop names set by -D to other sources (default). - -e Use desktop names set by -D exclusively, discard other sources. - -N Name Fancy name for compositor (filled from Desktop Entry by default). - -C Comment Fancy description for compositor (filled from Desktop Entry by de‐ - fault). - -U {run|home} Select rung for generated unit files: run: $XDG_RUNTIME_DIR/sys‐ - temd/user (default), or home: $XDG_CONFIG_HOME/systemd/user. Permanent - destination will save some time by removing need for reloading systemd. - Managed files from other rung will be removed. Can be preset with - UWSM_UNIT_RUNG environment var. - -t Do not generate (and remove) tweak unit files. Can be preset with - UWSM_TWEAKS=false environment var. - -T Generate tweak unit files for other software. This is default behavior. - -g S Wait for S seconds for system graphical.target in queue and warn if - timed out or not in queue (default: 60, negative to disable). - -G S Wait for S seconds for system graphical.target in queue and abort if - timed out or not in queue (overrides -g, default: -1, (disabled)). - -o Only generate units, but do not start. - -n Dry run, do not write or start anything. - - The first argument of the compositor command line acts as an ID and should be either one of: - • Executable name - • Desktop Entry ID (optionally with ":"-delimited action ID) - • Special value: - • select - invoke menu to select compositor. - • default - run previously selected compositor (or select if no selection was saved). - - If given as path, hardcode mode will be used implicitly. - - Always use "--" to disambiguate dashed arguments intended for compositor itself. - - After units are (re)generated, wayland-session-bindpid@${PID}.service is started, to track the - PID of invoking uwsm, then uwsm process replaces itself with systemctl execution that starts - wayland-wm@${ID}.service and waits for it to finish. - - In order to complete the startup sequence, the compositor has to put WAYLAND_DISPLAY into the - systemd activation environment. This can be done explicitly by making compositor run "uwsm fi‐ - nalize" command (see the next subsection). - - finalize - For running by a compositor on startup. - - uwsm finalize [-h] [VAR_NAME ...] - - Exports WAYLAND_DISPLAY, DISPLAY and any defined vars mentioned by names in arguments or in - UWSM_FINALIZE_VARNAMES variable (whitespace-separated). Then sends startup notification for the - unit to systemd user manager. - - This is required if compositor itself does not put WAYLAND_DISPLAY to systemd activation envi‐ - ronment, otherwise wayland-session@.service unit or a dedicated wayland-session-waitenv.service - unit will terminate due to startup timeout. - - UWSM_FINALIZE_VARNAMES variable can be prefilled by plugins. - - Direct assignment as VAR_NAME=value is also possible, but recommended only for creating flags - for UWSM_WAIT_VARNAMES mechanism. - - stop - Stops compositor and optionally removes generated units. - - uwsm stop [-h] [-r [compositor] [-U {run|home}] [-n] - - -r [compositor] Also remove units (all or only compositor-specific). - -U {run|home} Select rung for generated unit files: run: $XDG_RUNTIME_DIR/systemd/user - (default), or home: $XDG_CONFIG_HOME/systemd/user. Permanent destination - will save some time by removing need for reloading systemd. Managed files - from other rung will be removed. Can be preset with UWSM_UNIT_RUNG envi‐ - ronment var. - -n Dry run, do not stop or remove anything. - - app - Application-to-unit launcher with Desktop Entry support. - - uwsm app [-h] [-s {a,b,s,custom.slice}] [-t {scope,service}] [-a app_name] [-u unit_name] - [-d unit_description] [-S ] [-T] -- application [args ...] - - -s {a,b,s,custom.slice} Slice selector (default: a): - a - app-graphical.slice - b - background-graphical.slice - s - session-graphical.slice - any slice by full name - -t {scope,service} Type of unit to launch (default: scope, can be preset by - UWSM_APP_UNIT_TYPE env var). - -a app_name Override app name (a substring in unit name). - -u unit_name Override the whole autogenerated unit name. - -d unit_description Unit Description. - -p Property=value Set additional unit property (option is repeatable). - -S {out,err,both} Silence stdout, stderr, or both. - -T Launch app in a terminal. Allows command to be empty to just - launch a terminal. - - Application can be provided as a command with optional arguments, or a Desktop Entry ID, op‐ - tionally suffixed with ":"-delimited Action ID. If Desktop Entry is being launched, arguments - should be compatible with it. - - Always use "--" to disambiguate dashed arguments intended for application itself. - - aux - For use in systemd user services. Can only be called by systemd user manager. - - prepare-env Prepares environment (for use in ExecStart in wayland-wm-env@.service bound to - wayland-session-pre@.target). - cleanup-env Cleans up environment (for use ExecStop in in wayland-wm-env@.service bound to - wayland-session-pre@.target). - exec Executes a command with arguments or a desktop entry (for use in Exec in wayland- - wm@.service bound to wayland-session@.target). - app-daemon Daemon for faster app argument generation, used by uwsm-app client. - -APP DAEMON - Provided as wayland-wm-app-daemon.service to be started on-demand. +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. + 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. + Arguments are expected to be \0-delimited, leading \0 are stripped. One command is received per + write+close. - The first argument determines the behavior: + 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 + • 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. + 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 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: + 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" + #!/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. + 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. +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: + 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 + 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: + 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 + 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. + 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. + 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. + "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 +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: + 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‘ + • 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. + 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) +SEE ALSO + uwsm-plugins(3), systemd-run(1), systemd-cat(1), systemd.special(7) - 2026-02-14 UWSM(1) + 2026-02-14 UWSM(1) From d04b960c0551b343272e8a1ea53d380932ded923 Mon Sep 17 00:00:00 2001 From: Ian Maloney Date: Thu, 19 Feb 2026 20:55:18 +0000 Subject: [PATCH 21/47] warn when pager is missing instead of silently falling back when a configured pager (via BAT_PAGER, PAGER, or --pager) is not found, bat now shows a warning message before falling back to stdout. this helps users understand why their pager isn't running and makes it obvious when there's a typo or PATH issue. fixes issue #2904 --- CHANGELOG.md | 1 + src/output.rs | 12 +++++++++++- tests/integration_tests.rs | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8d93805..3e8fad14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk) ## Bugfixes +- Report error when pager is missing instead of silently falling back, see #3588 (@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) diff --git a/src/output.rs b/src/output.rs index e5c8a654..64ef2f15 100644 --- a/src/output.rs +++ b/src/output.rs @@ -105,6 +105,10 @@ impl OutputType { let resolved_path = match grep_cli::resolve_binary(&pager.bin) { Ok(path) => path, Err(_) => { + crate::bat_warning!( + "Pager '{}' not found, outputting to stdout instead", + pager.bin.to_string_lossy() + ); return Ok(OutputType::stdout()); } }; @@ -174,7 +178,13 @@ impl OutputType { Ok(p.stdin(Stdio::piped()) .spawn() .map(OutputType::Pager) - .unwrap_or_else(|_| OutputType::stdout())) + .unwrap_or_else(|_| { + crate::bat_warning!( + "Pager '{}' not found, outputting to stdout instead", + &pager.bin + ); + OutputType::stdout() + })) } pub(crate) fn stdout() -> Self { diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index da8b21eb..b6de86ee 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1425,6 +1425,21 @@ fn pager_failed_to_parse() { .stderr(predicate::str::contains("Could not parse pager command")); } +#[test] +#[serial] +fn pager_missing_warning() { + bat() + .env("BAT_PAGER", "nonexistent-pager-xyz-missing") + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stderr(predicate::str::contains("[bat warning]")) + .stderr(predicate::str::contains("not found")) + .stderr(predicate::str::contains("nonexistent-pager-xyz-missing")) + .stdout(predicate::str::contains("hello world\n")); +} + #[test] #[serial] fn env_var_bat_paging() { From 335eff51f3d6f819f7e634c1f562f2132051f9e7 Mon Sep 17 00:00:00 2001 From: Ian Maloney Date: Thu, 19 Feb 2026 21:44:47 +0000 Subject: [PATCH 22/47] fix type error, pager.bin is already a string --- src/output.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/output.rs b/src/output.rs index 64ef2f15..fbae5483 100644 --- a/src/output.rs +++ b/src/output.rs @@ -107,7 +107,7 @@ impl OutputType { Err(_) => { crate::bat_warning!( "Pager '{}' not found, outputting to stdout instead", - pager.bin.to_string_lossy() + pager.bin ); return Ok(OutputType::stdout()); } From 00e38cd05631bf4cf29775d79090b8e90d87c8ca Mon Sep 17 00:00:00 2001 From: Ian Maloney Date: Thu, 19 Feb 2026 22:01:58 +0000 Subject: [PATCH 23/47] only warn for explicitly configured pagers, not defaults --- src/output.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/output.rs b/src/output.rs index fbae5483..a5767bf5 100644 --- a/src/output.rs +++ b/src/output.rs @@ -105,10 +105,12 @@ 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 - ); + if pager.source != PagerSource::Default { + crate::bat_warning!( + "Pager '{}' not found, outputting to stdout instead", + pager.bin + ); + } return Ok(OutputType::stdout()); } }; From a240aa4afd4978179f044fc62eb53f7b2562fd9a Mon Sep 17 00:00:00 2001 From: Ian Maloney Date: Thu, 19 Feb 2026 22:21:17 +0000 Subject: [PATCH 24/47] never warn for missing 'less' pager (common default) --- src/output.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/output.rs b/src/output.rs index a5767bf5..7c1e6b17 100644 --- a/src/output.rs +++ b/src/output.rs @@ -105,7 +105,7 @@ impl OutputType { let resolved_path = match grep_cli::resolve_binary(&pager.bin) { Ok(path) => path, Err(_) => { - if pager.source != PagerSource::Default { + if pager.source != PagerSource::Default && pager.bin != "less" { crate::bat_warning!( "Pager '{}' not found, outputting to stdout instead", pager.bin From 2c1a8caadde0e888306470bbba2689514c24ff00 Mon Sep 17 00:00:00 2001 From: Ian Maloney Date: Thu, 19 Feb 2026 22:49:02 +0000 Subject: [PATCH 25/47] simplify: just never warn for less (universal default) --- src/output.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/output.rs b/src/output.rs index 7c1e6b17..c79de01c 100644 --- a/src/output.rs +++ b/src/output.rs @@ -105,7 +105,7 @@ impl OutputType { let resolved_path = match grep_cli::resolve_binary(&pager.bin) { Ok(path) => path, Err(_) => { - if pager.source != PagerSource::Default && pager.bin != "less" { + if pager.bin != "less" { crate::bat_warning!( "Pager '{}' not found, outputting to stdout instead", pager.bin From 319811df01f2b9bf036ebef677130dd87f9966d1 Mon Sep 17 00:00:00 2001 From: Ian Maloney Date: Fri, 20 Feb 2026 00:09:02 +0000 Subject: [PATCH 26/47] fix test: use builtin pager instead of less, revert to warn for all missing pagers --- src/output.rs | 10 ++++------ tests/integration_tests.rs | 1 + 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/output.rs b/src/output.rs index c79de01c..fbae5483 100644 --- a/src/output.rs +++ b/src/output.rs @@ -105,12 +105,10 @@ impl OutputType { let resolved_path = match grep_cli::resolve_binary(&pager.bin) { Ok(path) => path, Err(_) => { - if pager.bin != "less" { - crate::bat_warning!( - "Pager '{}' not found, outputting to stdout instead", - pager.bin - ); - } + crate::bat_warning!( + "Pager '{}' not found, outputting to stdout instead", + pager.bin + ); return Ok(OutputType::stdout()); } }; diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index b6de86ee..4c295709 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1458,6 +1458,7 @@ fn env_var_bat_paging() { fn basic_set_terminal_title() { bat() .arg("--paging=always") + .arg("--pager=builtin") .arg("--set-terminal-title") .arg("test.txt") .assert() From bc84854d4b84e0bac23c2017ba74a12ae2aaab10 Mon Sep 17 00:00:00 2001 From: Ian Maloney Date: Fri, 20 Feb 2026 00:19:47 +0000 Subject: [PATCH 27/47] use cat as test pager instead of builtin (builtin is interactive, doesnt output to stdout) --- tests/integration_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 4c295709..b2377a80 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1457,8 +1457,8 @@ fn env_var_bat_paging() { #[test] fn basic_set_terminal_title() { bat() + .env("BAT_PAGER", "cat") .arg("--paging=always") - .arg("--pager=builtin") .arg("--set-terminal-title") .arg("test.txt") .assert() From 790bed3a2d513f7f225de7677a3b9fce4fba4e29 Mon Sep 17 00:00:00 2001 From: Ian Maloney Date: Fri, 20 Feb 2026 05:52:56 +0000 Subject: [PATCH 28/47] fix: respect --wrap=never flag when paging is enabled When output is piped to a pager, the wrapping_mode logic was not checking the explicit --wrap=never flag and would always default to NoWrapping(false). This meant the -S flag was not passed to less, causing lines to wrap despite the user's explicit request. The fix prioritizes explicit CLI flags (--wrap=never, --chop-long-lines) over the interactive_output-based logic, ensuring they are always respected. Fixes #3587 --- src/bin/bat/app.rs | 26 ++++++++++++--------- tests/integration_tests.rs | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 521674f2..fed45ab6 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -399,27 +399,31 @@ impl App { Some("no-printing") => BinaryBehavior::NoPrinting, _ => unreachable!("other values for --binary are not allowed"), }, - wrapping_mode: if self.interactive_output || maybe_term_width.is_some() { - if !self.matches.get_flag("chop-long-lines") { + wrapping_mode: { + // Check for explicit flags first (--chop-long-lines or --wrap) + if self.matches.get_flag("chop-long-lines") { + WrappingMode::NoWrapping(true) + } else { match self.matches.get_one::("wrap").map(|s| s.as_str()) { Some("character") => WrappingMode::Character, Some("never") => WrappingMode::NoWrapping(true), Some("auto") | None => { - if style_components.plain() && maybe_term_width.is_none() { - WrappingMode::NoWrapping(false) + // For "auto" or default mode, respect interactive_output setting + if self.interactive_output || maybe_term_width.is_some() { + if style_components.plain() && maybe_term_width.is_none() { + WrappingMode::NoWrapping(false) + } else { + WrappingMode::Character + } } else { - WrappingMode::Character + // We don't have the tty width when piping to another program. + // There's no point in wrapping when this is the case. + WrappingMode::NoWrapping(false) } } _ => unreachable!("other values for --wrap are not allowed"), } - } else { - WrappingMode::NoWrapping(true) } - } else { - // We don't have the tty width when piping to another program. - // There's no point in wrapping when this is the case. - WrappingMode::NoWrapping(false) }, colored_output: self.matches.get_flag("force-colorization") || match self.matches.get_one::("color").map(|s| s.as_str()) { diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index da8b21eb..4235080f 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -2884,6 +2884,52 @@ fn no_wrapping_with_chop_long_lines() { wrapping_test("--chop-long-lines", false); } +// Regression test for issue #3587: --wrap=never should be respected when paging is enabled +// The fix ensures that bat respects the --wrap=never flag even when output is piped to a pager +// by passing the -S flag to less to disable wrapping in the pager +#[test] +#[serial] +fn wrap_never_flag_respected_with_paging_always() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + // Use cat as pager to pass through the output (avoiding the echo pager issue) + .arg("--pager=cat") + .arg("--paging=always") + .arg("--wrap=never") + .arg("--color=never") + .arg("--decorations=never") + .arg("--style=plain") + .write_stdin("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz\n") + .assert() + .success() + // With --wrap=never and cat pager, the line should NOT wrap + .stdout(predicate::str::contains("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz").normalize()) + .stderr(""); + }); +} + +// Regression test for issue #3587: -S flag should be respected when paging is enabled +#[test] +#[serial] +fn s_flag_respected_with_paging_always() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + // Use cat as pager to pass through the output + .arg("--pager=cat") + .arg("--paging=always") + .arg("-S") + .arg("--color=never") + .arg("--decorations=never") + .arg("--style=plain") + .write_stdin("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz\n") + .assert() + .success() + // With -S flag and cat pager, the line should NOT wrap + .stdout(predicate::str::contains("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz").normalize()) + .stderr(""); + }); +} + #[test] fn theme_arg_overrides_env() { bat() From 22ee03ff000942ed1b5be14c7f853a3c4fcb40d2 Mon Sep 17 00:00:00 2001 From: Ian Maloney Date: Fri, 20 Feb 2026 05:57:04 +0000 Subject: [PATCH 29/47] add changelog entry for wrap-never bug fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8d93805..0e9a6c4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk) ## Bugfixes +- Fix `--wrap=never` and `-S` flags being ignored when piping to pager, see #3590 (@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) From 16d041492cc2de71cb95d3bbd48b6903eca6ca31 Mon Sep 17 00:00:00 2001 From: Ian Maloney Date: Fri, 20 Feb 2026 06:07:23 +0000 Subject: [PATCH 30/47] fix pr number in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e9a6c4d..8dfae67d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ - Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk) ## Bugfixes -- Fix `--wrap=never` and `-S` flags being ignored when piping to pager, see #3590 (@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) From 62b0388236ad36a05315d1c6fb78f132d165e0c8 Mon Sep 17 00:00:00 2001 From: Ian Maloney Date: Fri, 20 Feb 2026 06:12:14 +0000 Subject: [PATCH 31/47] remove code comments --- src/bin/bat/app.rs | 2 -- tests/integration_tests.rs | 8 -------- 2 files changed, 10 deletions(-) diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index fed45ab6..54578087 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -400,7 +400,6 @@ impl App { _ => unreachable!("other values for --binary are not allowed"), }, wrapping_mode: { - // Check for explicit flags first (--chop-long-lines or --wrap) if self.matches.get_flag("chop-long-lines") { WrappingMode::NoWrapping(true) } else { @@ -408,7 +407,6 @@ impl App { Some("character") => WrappingMode::Character, Some("never") => WrappingMode::NoWrapping(true), Some("auto") | None => { - // For "auto" or default mode, respect interactive_output setting if self.interactive_output || maybe_term_width.is_some() { if style_components.plain() && maybe_term_width.is_none() { WrappingMode::NoWrapping(false) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 4235080f..6e99990b 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -2884,15 +2884,11 @@ fn no_wrapping_with_chop_long_lines() { wrapping_test("--chop-long-lines", false); } -// Regression test for issue #3587: --wrap=never should be respected when paging is enabled -// The fix ensures that bat respects the --wrap=never flag even when output is piped to a pager -// by passing the -S flag to less to disable wrapping in the pager #[test] #[serial] fn wrap_never_flag_respected_with_paging_always() { mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { bat() - // Use cat as pager to pass through the output (avoiding the echo pager issue) .arg("--pager=cat") .arg("--paging=always") .arg("--wrap=never") @@ -2902,19 +2898,16 @@ fn wrap_never_flag_respected_with_paging_always() { .write_stdin("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz\n") .assert() .success() - // With --wrap=never and cat pager, the line should NOT wrap .stdout(predicate::str::contains("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz").normalize()) .stderr(""); }); } -// Regression test for issue #3587: -S flag should be respected when paging is enabled #[test] #[serial] fn s_flag_respected_with_paging_always() { mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { bat() - // Use cat as pager to pass through the output .arg("--pager=cat") .arg("--paging=always") .arg("-S") @@ -2924,7 +2917,6 @@ fn s_flag_respected_with_paging_always() { .write_stdin("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz\n") .assert() .success() - // With -S flag and cat pager, the line should NOT wrap .stdout(predicate::str::contains("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz").normalize()) .stderr(""); }); From e36bb8cc66089b73109a65d8702d07b971d0cf8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 03:04:00 +0000 Subject: [PATCH 32/47] build(deps): bump serde_with from 3.16.1 to 3.17.0 Bumps [serde_with](https://github.com/jonasbb/serde_with) from 3.16.1 to 3.17.0. - [Release notes](https://github.com/jonasbb/serde_with/releases) - [Commits](https://github.com/jonasbb/serde_with/compare/v3.16.1...v3.17.0) --- updated-dependencies: - dependency-name: serde_with dependency-version: 3.17.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6684e40b..10a5c204 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1415,9 +1415,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" dependencies = [ "serde_core", "serde_with_macros", @@ -1425,9 +1425,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" dependencies = [ "darling", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 4a2025cb..33a64584 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,7 +117,7 @@ quote = "1.0.40" regex = "1.12.2" serde = "1.0" serde_derive = "1.0" -serde_with = { version = "3.16.1", default-features = false, features = ["macros"] } +serde_with = { version = "3.17.0", default-features = false, features = ["macros"] } syn = { version = "2.0.104", features = ["full"] } toml = { version = "0.9.8", features = ["preserve_order"] } walkdir = "2.5" From 90f8e00e0c2b16a9ef1ed965001804c0d934da78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 03:35:51 +0000 Subject: [PATCH 33/47] build(deps): bump nix from 0.30.1 to 0.31.2 Bumps [nix](https://github.com/nix-rust/nix) from 0.30.1 to 0.31.2. - [Changelog](https://github.com/nix-rust/nix/blob/master/CHANGELOG.md) - [Commits](https://github.com/nix-rust/nix/compare/v0.30.1...v0.31.2) --- updated-dependencies: - dependency-name: nix dependency-version: 0.31.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10a5c204..6f6c7878 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -909,9 +909,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libgit2-sys" @@ -1032,9 +1032,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.30.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ "bitflags", "cfg-if", diff --git a/Cargo.toml b/Cargo.toml index 33a64584..b5a95c42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,7 @@ tempfile = "3.23.0" serde = { version = "1.0", features = ["derive"] } [target.'cfg(unix)'.dev-dependencies] -nix = { version = "0.30", default-features = false, features = ["term"] } +nix = { version = "0.31", default-features = false, features = ["term"] } [build-dependencies] anyhow = "1.0.97" From 397252d2ccada6b40aed166052c40a0385f412b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 04:00:55 +0000 Subject: [PATCH 34/47] build(deps): bump git2 from 0.20.3 to 0.20.4 Bumps [git2](https://github.com/rust-lang/git2-rs) from 0.20.3 to 0.20.4. - [Changelog](https://github.com/rust-lang/git2-rs/blob/git2-0.20.4/CHANGELOG.md) - [Commits](https://github.com/rust-lang/git2-rs/compare/git2-0.20.3...git2-0.20.4) --- updated-dependencies: - dependency-name: git2 dependency-version: 0.20.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f6c7878..a7f8b8a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -658,9 +658,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.20.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c" +checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ "bitflags", "libc", From fa354958886e1c416e4b67f020d964e451379da1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 04:18:28 +0000 Subject: [PATCH 35/47] build(deps): bump clap from 4.5.56 to 4.5.60 Bumps [clap](https://github.com/clap-rs/clap) from 4.5.56 to 4.5.60. - [Release notes](https://github.com/clap-rs/clap/releases) - [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md) - [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.56...clap_complete-v4.5.60) --- updated-dependencies: - dependency-name: clap dependency-version: 4.5.60 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 12 ++++++------ Cargo.toml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a7f8b8a4..788eaffa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -252,18 +252,18 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.56" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.56" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -274,9 +274,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clircle" diff --git a/Cargo.toml b/Cargo.toml index b5a95c42..66c53e68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,7 +87,7 @@ default-features = false features = ["parsing"] [dependencies.clap] -version = "4.5.56" +version = "4.5.60" optional = true features = ["wrap_help", "cargo"] @@ -123,7 +123,7 @@ toml = { version = "0.9.8", features = ["preserve_order"] } walkdir = "2.5" [build-dependencies.clap] -version = "4.5.56" +version = "4.5.60" optional = true features = ["wrap_help", "cargo"] From 17a70d9b8cc956b23a488fa71483e7d10bb19203 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 04:36:13 +0000 Subject: [PATCH 36/47] build(deps): bump bytesize from 1.3.0 to 2.3.1 Bumps [bytesize](https://github.com/bytesize-rs/bytesize) from 1.3.0 to 2.3.1. - [Release notes](https://github.com/bytesize-rs/bytesize/releases) - [Changelog](https://github.com/bytesize-rs/bytesize/blob/master/CHANGELOG.md) - [Commits](https://github.com/bytesize-rs/bytesize/compare/v1.3.0...bytesize-v2.3.1) --- updated-dependencies: - dependency-name: bytesize dependency-version: 2.3.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 788eaffa..2e777a2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -223,9 +223,9 @@ checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "bytesize" -version = "1.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" [[package]] name = "cc" diff --git a/Cargo.toml b/Cargo.toml index 66c53e68..6c783336 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = "1.3.0" } +bytesize = { version = "2.3.1" } encoding_rs = "0.8.35" execute = { version = "0.2.15", optional = true } terminal-colorsaurus = "1.0" From fd67095cff7f96b4a82ee157e09e3b3792cee680 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 2 Mar 2026 04:12:50 +0900 Subject: [PATCH 37/47] Fix panic in BuiltinPager drop when pager thread panics The Drop impl for OutputType::BuiltinPager calls handle.take().unwrap().join().unwrap() which panics if the pager thread itself panicked. In Drop, this causes a double panic (abort). Replace with if-let and discard the join result, matching the pattern used for OutputType::Pager which also uses let _ =. Reported in #3449 where BAT_PAGER=builtin with --help causes the pager thread to fail. Fixes #3449 --- src/output.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/output.rs b/src/output.rs index fbae5483..0205f48e 100644 --- a/src/output.rs +++ b/src/output.rs @@ -225,8 +225,8 @@ impl Drop for OutputType { let _ = command.wait(); } OutputType::BuiltinPager(ref mut pager) => { - if pager.handle.is_some() { - let _ = pager.handle.take().unwrap().join().unwrap(); + if let Some(handle) = pager.handle.take() { + let _ = handle.join(); } } OutputType::Stdout(_) => (), From cc5f782d28a8e6156b8ebd3346b0a7f7c49256e2 Mon Sep 17 00:00:00 2001 From: Varun Chawla <34209028+veeceey@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:18:49 -0800 Subject: [PATCH 38/47] Add word wrapping mode (#3597) * feat: add word wrapping mode for --wrap flag * Run `cargo fmt` and add CHANGELOG entry * Add word wrap tests, update manpage and shell completions - Add integration tests for word wrapping: basic word boundary breaking, fallback to character wrapping for long words, line numbers, and short lines that fit without wrapping - Update manpage to document the new 'word' wrapping mode - Update bash, fish, zsh, and PowerShell completions with 'word' option - Avoid unnecessary clone of `line_buf` when word wrap is disabled * make clippy and cargo fmt happy --------- Co-authored-by: Keith Hall --- CHANGELOG.md | 1 + assets/completions/_bat.ps1.in | 4 +-- assets/completions/bat.bash.in | 2 +- assets/completions/bat.fish.in | 1 + assets/completions/bat.zsh.in | 2 +- assets/manual/bat.1.in | 6 ++-- doc/long-help.txt | 4 +-- doc/short-help.txt | 2 +- src/bin/bat/app.rs | 1 + src/bin/bat/clap_app.rs | 6 ++-- src/printer.rs | 53 ++++++++++++++++++++++++++++-- src/wrapping.rs | 1 + tests/examples/word-wrap.txt | 3 ++ tests/integration_tests.rs | 59 ++++++++++++++++++++++++++++++++++ 14 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 tests/examples/word-wrap.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 278bfc74..04bb52ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## Features +- 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) diff --git a/assets/completions/_bat.ps1.in b/assets/completions/_bat.ps1.in index 80eb6654..97f76932 100644 --- a/assets/completions/_bat.ps1.in +++ b/assets/completions/_bat.ps1.in @@ -9,7 +9,7 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script $ArrayCompletion = @('bash', 'fish', 'zsh', 'ps1') $ArrayWhen = @('auto', 'never', 'always') $ArrayYesNo = @('never', 'always') - $ArrayWrap = @('always', 'never', 'character') + $ArrayWrap = @('always', 'never', 'character', 'word') $ArrayBinary = @('no-printing', 'as-text') $ArrayPrint = @('unicode', 'caret') @@ -135,7 +135,7 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script [CompletionResult]::new('--file-name' , 'file-name' , [CompletionResultType]::ParameterName, 'Specify the name to display for a file.') [CompletionResult]::new('--diff-context' , 'diff-context' , [CompletionResultType]::ParameterName, 'diff-context') [CompletionResult]::new('--tabs' , 'tabs' , [CompletionResultType]::ParameterName, 'Set the tab width to T spaces.') - [CompletionResult]::new('--wrap' , 'wrap' , [CompletionResultType]::ParameterName, 'Specify the text-wrapping mode (*auto*, character).') + [CompletionResult]::new('--wrap' , 'wrap' , [CompletionResultType]::ParameterName, 'Specify the text-wrapping mode (*auto*, never, character, word).') [CompletionResult]::new('--terminal-width' , 'terminal-width' , [CompletionResultType]::ParameterName, 'Explicitly set the width of the terminal instead of determining it automatically. If prefixed with ''+'' or ''-'', the value will be treated as an offset to the actual terminal width. See also: ''--wrap''.') [CompletionResult]::new('--color' , 'color' , [CompletionResultType]::ParameterName, 'When to use colors (*auto*, never, always).') [CompletionResult]::new('--italic-text' , 'italic-text' , [CompletionResultType]::ParameterName, 'Use italics in output (always, *never*)') diff --git a/assets/completions/bat.bash.in b/assets/completions/bat.bash.in index 2cc360ab..6e45bd19 100644 --- a/assets/completions/bat.bash.in +++ b/assets/completions/bat.bash.in @@ -117,7 +117,7 @@ _bat() { return 0 ;; --wrap) - COMPREPLY=($(compgen -W "auto never character" -- "$cur")) + COMPREPLY=($(compgen -W "auto never character word" -- "$cur")) return 0 ;; --binary) diff --git a/assets/completions/bat.fish.in b/assets/completions/bat.fish.in index 57f3cd7d..2100338c 100644 --- a/assets/completions/bat.fish.in +++ b/assets/completions/bat.fish.in @@ -118,6 +118,7 @@ set -l wrap_opts ' auto\tdefault never\t character\t + word\t ' # While --tabs theoretically takes any number, most people should be OK with these. diff --git a/assets/completions/bat.zsh.in b/assets/completions/bat.zsh.in index 536cd8df..3c3f81d1 100644 --- a/assets/completions/bat.zsh.in +++ b/assets/completions/bat.zsh.in @@ -34,7 +34,7 @@ _{{PROJECT_EXECUTABLE}}_main() { '(-d --diff)'--diff'[only show lines that have been added/removed/modified]' --diff-context='[specify lines of context around added/removed/modified lines when using `--diff`]:lines' --tabs='[set the tab width]:tab width [4]' - --wrap='[specify the text-wrapping mode]:mode [auto]:(auto never character)' + --wrap='[specify the text-wrapping mode]:mode [auto]:(auto never character word)' '!(--wrap)'{-S,--chop-long-lines} --terminal-width='[explicitly set the width of the terminal instead of determining it automatically]:width' '(-n --number --diff --diff-context)'{-n,--number}'[show line numbers]' diff --git a/assets/manual/bat.1.in b/assets/manual/bat.1.in index e35b10f9..288a3d30 100644 --- a/assets/manual/bat.1.in +++ b/assets/manual/bat.1.in @@ -102,8 +102,10 @@ Set the tab width to T spaces. Use a width of 0 to pass tabs through directly .HP \fB\-\-wrap\fR .IP -Specify the text\-wrapping mode (*auto*, never, character). The '\-\-terminal\-width' option -can be used in addition to control the output width. +Specify the text\-wrapping mode (*auto*, never, character, word). The '\-\-terminal\-width' option +can be used in addition to control the output width. In \fBword\fR mode, lines are broken at +whitespace boundaries. If a single word exceeds the terminal width, it falls back to +character wrapping. .HP \fB\-S\fR, \fB\-\-chop\-long\-lines\fR .IP diff --git a/doc/long-help.txt b/doc/long-help.txt index d00c84e9..2c98ff25 100644 --- a/doc/long-help.txt +++ b/doc/long-help.txt @@ -61,8 +61,8 @@ Options: Set the tab width to T spaces. Use a width of 0 to pass tabs through directly --wrap - Specify the text-wrapping mode (*auto*, never, character). The '--terminal-width' option - can be used in addition to control the output width. + Specify the text-wrapping mode (*auto*, never, character, word). The '--terminal-width' + option can be used in addition to control the output width. -S, --chop-long-lines Truncate all lines longer than screen width. Alias for '--wrap=never'. diff --git a/doc/short-help.txt b/doc/short-help.txt index 04ca57a6..b0c45314 100644 --- a/doc/short-help.txt +++ b/doc/short-help.txt @@ -26,7 +26,7 @@ Options: --tabs Set the tab width to T spaces. --wrap - Specify the text-wrapping mode (*auto*, never, character). + Specify the text-wrapping mode (*auto*, never, character, word). -S, --chop-long-lines Truncate all lines longer than screen width. Alias for '--wrap=never'. -n, --number diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 54578087..73ad60fe 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -405,6 +405,7 @@ impl App { } else { match self.matches.get_one::("wrap").map(|s| s.as_str()) { Some("character") => WrappingMode::Character, + Some("word") => WrappingMode::Word, Some("never") => WrappingMode::NoWrapping(true), Some("auto") | None => { if self.interactive_output || maybe_term_width.is_some() { diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index d286904a..5e2b927c 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -211,11 +211,11 @@ pub fn build_app(interactive_output: bool) -> Command { .long("wrap") .overrides_with("wrap") .value_name("mode") - .value_parser(["auto", "never", "character"]) + .value_parser(["auto", "never", "character", "word"]) .default_value("auto") .hide_default_value(true) - .help("Specify the text-wrapping mode (*auto*, never, character).") - .long_help("Specify the text-wrapping mode (*auto*, never, character). \ + .help("Specify the text-wrapping mode (*auto*, never, character, word).") + .long_help("Specify the text-wrapping mode (*auto*, never, character, word). \ The '--terminal-width' option can be used in addition to \ control the output width."), ) diff --git a/src/printer.rs b/src/printer.rs index 3c3facf5..119258bd 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -781,11 +781,21 @@ impl Printer for InteractivePrinter<'_> { // Displayed width of line_buf let mut current_width = 0; + let word_wrap = matches!(self.config.wrapping_mode, WrappingMode::Word); + + // For word wrapping, track last whitespace position. + let mut last_ws_idx: Option = None; + for c in text.chars() { // calculate the displayed width for next character 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 { @@ -807,13 +817,37 @@ impl Printer for InteractivePrinter<'_> { } } + // Determine the break point and remainder + // for word wrapping. + let (emit_end, rest_start) = if word_wrap { + if let Some(ws_idx) = last_ws_idx { + // Skip the whitespace character itself + // and carry the rest to the next line. + let rs = ws_idx + + line_buf[ws_idx..] + .chars() + .next() + .map(|ch| ch.len_utf8()) + .unwrap_or(0); + (ws_idx, Some(rs)) + } else { + (line_buf.len(), None) + } + } else { + (line_buf.len(), None) + }; + // It wraps. write!( handle, "{}{}\n{}", as_terminal_escaped( style, - &format!("{}{line_buf}", self.ansi_style), + &format!( + "{}{}", + self.ansi_style, + &line_buf[..emit_end] + ), self.config.true_color, self.config.colored_output, self.config.use_italic_text, @@ -826,8 +860,21 @@ impl Printer for InteractivePrinter<'_> { cursor = 0; max_width = cursor_max; - line_buf.clear(); - current_width = cw; + if let Some(rs) = rest_start { + // Word wrap: carry remainder to next line. + let remainder = line_buf[rs..].to_string(); + let rem_width: usize = remainder + .chars() + .map(|ch| ch.width().unwrap_or(0)) + .sum(); + line_buf.clear(); + line_buf.push_str(&remainder); + current_width = rem_width + cw; + } else { + line_buf.clear(); + current_width = cw; + } + last_ws_idx = None; } line_buf.push(c); diff --git a/src/wrapping.rs b/src/wrapping.rs index 57201750..3a5fa457 100644 --- a/src/wrapping.rs +++ b/src/wrapping.rs @@ -1,6 +1,7 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WrappingMode { Character, + Word, // The bool specifies whether wrapping has been explicitly disabled by the user via --wrap=never NoWrapping(bool), } diff --git a/tests/examples/word-wrap.txt b/tests/examples/word-wrap.txt new file mode 100644 index 00000000..1c5141d0 --- /dev/null +++ b/tests/examples/word-wrap.txt @@ -0,0 +1,3 @@ +The quick brown fox jumps over the lazy dog and then runs away +superlongwordthatdefinitelyexceedstheterminalwidthandshouldfallbacktocharacterwrapping +short words here diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 4972aa46..ee727eb0 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -3791,3 +3791,62 @@ fn unbuffered_mode_plain_output() { .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"); +} From a1a10c777f8389d2a6997556bf96f0673de35d40 Mon Sep 17 00:00:00 2001 From: foxfromworld Date: Thu, 5 Mar 2026 13:55:05 +0800 Subject: [PATCH 39/47] =?UTF-8?q?Docs:=20clarify=20PATH=20requirement=20an?= =?UTF-8?q?d=20explain=20highlighted=20outputs=20(Fixes=E2=80=A6=20(#3610)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Docs: clarify PATH requirement and explain highlighted outputs (Fixes #2376) * Update CHANGELOG for PR #3610 * Apply suggestion from @keith-hall Applied reviewer’s suggestion to improve PATH export: safer with quotes and more convenient with $(pwd). Co-authored-by: Keith Hall * Docs: clarify PATH setup in CONTRIBUTING.md, remove that section from README.md * Docs: add cross reference to syntax tests step 5 in doc/assets.md --------- Co-authored-by: Keith Hall --- CHANGELOG.md | 1 + CONTRIBUTING.md | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04bb52ce..37f435d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # unreleased - Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l) +- Fixed syntax tests path, see #3610 (@foxfromworld) ## Features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 591bce7b..3852a755 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,3 +91,29 @@ To learn how to write regression tests for theme and syntax changes, read the [Syntax tests](https://github.com/sharkdp/bat/blob/master/doc/assets.md#syntax-tests) section in `assets.md`. + +### Ensuring bat is available for Syntax tests + +The syntax test script (`tests/syntax-tests/update.sh`) calls `bat` from your PATH and regenerates the highlighted output files under +`tests/syntax-tests/highlighted/`. These files are used to verify that syntax highlighting works as expected. + +- If you only built the binaries with: + ```bash + cargo build --bins + ``` + + you need to add the debug build to your PATH from the bat project root before running the tests. + See also step 5 in [Syntax +tests](https://github.com/sharkdp/bat/blob/master/doc/assets.md#syntax-tests) for related instructions. + ```bash + export PATH="$PATH:$(pwd)/target/debug" + ``` + Otherwise, you will see: + ```bash + Error: Could not execute 'bat'. Please make sure that the executable is available on the PATH. + ``` +- If you installed bat with: + ```bash + cargo install --path . --locked + ``` + then bat will be available in ~/.cargo/bin (usually already in PATH), and the tests will run without issues. \ No newline at end of file From ab80bd9717448d988445841fc9634e7d7c2f8cf6 Mon Sep 17 00:00:00 2001 From: Matei6942 Date: Fri, 6 Mar 2026 23:44:20 +0530 Subject: [PATCH 40/47] feat(syntax): add support for hidden_file_extensions (#3613) * feat(syntax): add support for hidden_file_extensions --- CHANGELOG.md | 1 + src/assets.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f435d3..e1f26ba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## Features +- 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) diff --git a/src/assets.rs b/src/assets.rs index 82c160c9..80ae3e57 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -262,6 +262,24 @@ impl HighlightingAssets { .map(|syntax| SyntaxReferenceInSet { syntax, syntax_set })) } + fn find_syntax_by_hidden_file_name( + &self, + file_name: &OsStr, + ) -> Result>> { + 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>> { let syntax_set = self.get_syntax_set()?; Ok(syntax_set @@ -275,6 +293,9 @@ impl HighlightingAssets { ignored_suffixes: &IgnoredSuffixes, ) -> Result>> { 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| { @@ -661,6 +682,55 @@ mod tests { ); } + #[cfg(feature = "build-assets")] + #[test] + fn syntax_detection_hidden_file_extensions() { + let source_dir = TempDir::new().expect("creation of temporary source directory"); + let cache_dir = TempDir::new().expect("creation of temporary cache directory"); + let syntax_dir = source_dir.path().join("syntaxes"); + + std::fs::create_dir_all(&syntax_dir).expect("creation of syntax directory succeeds"); + std::fs::write( + syntax_dir.join("HiddenFileExtension.sublime-syntax"), + r#"%YAML 1.2 +--- +name: Hidden File Extension +hidden_file_extensions: + - testrc +scope: source.hiddenfileextension + +contexts: + main: + - match: . + scope: source.hiddenfileextension +"#, + ) + .expect("custom syntax can be written"); + + build( + source_dir.path(), + false, + false, + cache_dir.path(), + env!("CARGO_PKG_VERSION"), + ) + .expect("custom assets can be built"); + + let test = SyntaxDetectionTest { + assets: HighlightingAssets::from_cache(cache_dir.path()) + .expect("custom syntax cache can be loaded"), + syntax_mapping: SyntaxMapping::new(), + temp_dir: TempDir::new().expect("creation of temporary directory"), + }; + + assert_eq!(test.syntax_for_file(".testrc"), "Hidden File Extension"); + assert_eq!( + test.syntax_for_stdin_with_content(".testrc", b""), + "Hidden File Extension" + ); + assert!(test.syntax_is_same_for_inputkinds(".testrc", "")); + } + #[cfg(unix)] #[test] fn syntax_detection_for_symlinked_file() { From 844bfded506e99c06237472bd83a8af5af433538 Mon Sep 17 00:00:00 2001 From: Rizky Mirzaviandy Priambodo <142987522+Xavrir@users.noreply.github.com> Date: Sun, 8 Mar 2026 10:18:29 +0700 Subject: [PATCH 41/47] Add --fallback-syntax for undetected files (#3617) * feat(cli): add fallback syntax option Expose a new fallback syntax CLI option so users can opt into syntax highlighting only when auto-detection fails. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus * feat(syntax): apply fallback only after detection fails Use the fallback syntax only when path and first-line detection fail, preserving existing behavior for detected files and explicit language selection. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus * test(cli): cover fallback syntax behavior Add integration coverage for fallback syntax usage, precedence with --language, and no-op behavior when syntax is already detected; update help snapshots for the new option. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus * docs(changelog): document fallback syntax option Record the new fallback syntax feature in the unreleased changelog section. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --------- Co-authored-by: Sisyphus --- CHANGELOG.md | 1 + doc/long-help.txt | 7 +++ doc/short-help.txt | 2 + src/assets.rs | 25 +++++--- src/bin/bat/app.rs | 4 ++ src/bin/bat/clap_app.rs | 11 ++++ src/config.rs | 3 + src/printer.rs | 7 ++- tests/integration_tests.rs | 115 +++++++++++++++++++++++++++++++++++++ 9 files changed, 166 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1f26ba4..293e55dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - 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) ## Bugfixes - Report error when pager is missing instead of silently falling back, see #3588 (@IMaloney) diff --git a/doc/long-help.txt b/doc/long-help.txt index 2c98ff25..82878acd 100644 --- a/doc/long-help.txt +++ b/doc/long-help.txt @@ -37,6 +37,13 @@ Options: name (like 'C++' or 'LaTeX') or possible file extension (like 'cpp', 'hpp' or 'md'). Use '--list-languages' to show all supported language names and file extensions. + --fallback-syntax + 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 Highlight the specified line ranges with a different background color For example: '--highlight-line 40' highlights line 40 diff --git a/doc/short-help.txt b/doc/short-help.txt index b0c45314..e08bb604 100644 --- a/doc/short-help.txt +++ b/doc/short-help.txt @@ -17,6 +17,8 @@ Options: Show plain style (alias for '--style=plain'). -l, --language Set the language for syntax highlighting. + --fallback-syntax + Set a fallback language for undetected syntaxes. [aliases: --fallback-language] -H, --highlight-line Highlight lines N through M. --file-name diff --git a/src/assets.rs b/src/assets.rs index 80ae3e57..1315537f 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -210,6 +210,7 @@ impl HighlightingAssets { pub(crate) fn get_syntax( &self, language: Option<&str>, + fallback_syntax: Option<&str>, input: &mut OpenedInput, mapping: &SyntaxMapping, ) -> Result> { @@ -234,9 +235,16 @@ impl HighlightingAssets { match path_syntax { // If a path wasn't provided, or if path based syntax detection // above failed, we fall back to first-line syntax detection. - Err(Error::UndetectedSyntax(path)) => self - .get_first_line_syntax(&mut input.reader)? - .ok_or(Error::UndetectedSyntax(path)), + Err(Error::UndetectedSyntax(path)) => { + if let Some(syntax_in_set) = self.get_first_line_syntax(&mut input.reader)? { + Ok(syntax_in_set) + } else if let Some(language) = fallback_syntax { + self.find_syntax_by_token(language)? + .ok_or_else(|| Error::UnknownSyntax(language.to_owned())) + } else { + Err(Error::UndetectedSyntax(path)) + } + } _ => path_syntax, } } @@ -416,11 +424,12 @@ mod tests { fn get_syntax_name( &self, language: Option<&str>, + fallback_syntax: Option<&str>, input: &mut OpenedInput, mapping: &SyntaxMapping, ) -> String { self.assets - .get_syntax(language, input, mapping) + .get_syntax(language, fallback_syntax, input, mapping) .map(|syntax_in_set| syntax_in_set.syntax.name.clone()) .unwrap_or_else(|_| "!no syntax!".to_owned()) } @@ -440,7 +449,7 @@ mod tests { let dummy_stdin: &[u8] = &[]; let mut opened_input = input.open(dummy_stdin, None).unwrap(); - self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping) + self.get_syntax_name(None, None, &mut opened_input, &self.syntax_mapping) } fn syntax_for_file_with_content_os(&self, file_name: &OsStr, first_line: &str) -> String { @@ -450,7 +459,7 @@ mod tests { let dummy_stdin: &[u8] = &[]; let mut opened_input = input.open(dummy_stdin, None).unwrap(); - self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping) + self.get_syntax_name(None, None, &mut opened_input, &self.syntax_mapping) } #[cfg(unix)] @@ -470,7 +479,7 @@ mod tests { let input = Input::stdin().with_name(Some(file_name)); let mut opened_input = input.open(content, None).unwrap(); - self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping) + self.get_syntax_name(None, None, &mut opened_input, &self.syntax_mapping) } fn syntax_is_same_for_inputkinds(&self, file_name: &str, content: &str) -> bool { @@ -752,7 +761,7 @@ contexts: let mut opened_input = input.open(dummy_stdin, None).unwrap(); assert_eq!( - test.get_syntax_name(None, &mut opened_input, &test.syntax_mapping), + test.get_syntax_name(None, None, &mut opened_input, &test.syntax_mapping), "SSH Config" ); } diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 73ad60fe..dddb5559 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -384,6 +384,10 @@ impl App { None } }), + fallback_syntax: self + .matches + .get_one::("fallback-syntax") + .map(|s| s.as_str()), show_nonprintable: self.matches.get_flag("show-all"), nonprintable_notation: match self .matches diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index 5e2b927c..3636f081 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -120,6 +120,17 @@ pub fn build_app(interactive_output: bool) -> Command { language names and file extensions.", ), ) + .arg( + Arg::new("fallback-syntax") + .long("fallback-syntax") + .visible_alias("fallback-language") + .help("Set a fallback language for undetected syntaxes.") + .long_help( + "Set a fallback language for syntax highlighting when auto-detection fails. \ + Unlike '--language', this is only used when no syntax could be detected from \ + filename, custom syntax mappings, or first-line detection.", + ), + ) .arg( Arg::new("highlight-line") .long("highlight-line") diff --git a/src/config.rs b/src/config.rs index 8ea0e275..97720fb5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -38,6 +38,9 @@ pub struct Config<'a> { /// The explicitly configured language, if any pub language: Option<&'a str>, + /// The fallback syntax used when auto-detection fails + pub fallback_syntax: Option<&'a str>, + /// Whether or not to show/replace non-printable characters like space, tab and newline. pub show_nonprintable: bool, diff --git a/src/printer.rs b/src/printer.rs index 119258bd..6a57fb62 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -268,7 +268,12 @@ impl<'a> InteractivePrinter<'a> { const PLAIN_TEXT_SYNTAX: &str = "Plain Text"; const MANPAGE_SYNTAX: &str = "Manpage"; const COMMAND_HELP_SYNTAX: &str = "Command Help"; - match assets.get_syntax(config.language, input, &config.syntax_mapping) { + match assets.get_syntax( + config.language, + config.fallback_syntax, + input, + &config.syntax_mapping, + ) { Ok(syntax_in_set) => ( syntax_in_set.syntax.name == PLAIN_TEXT_SYNTAX, syntax_in_set.syntax.name == MANPAGE_SYNTAX diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index ee727eb0..cfbad253 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -2470,6 +2470,121 @@ fn no_first_line_fallback_when_mapping_to_invalid_syntax() { .stderr(predicate::str::contains("unknown syntax: 'InvalidSyntax'")); } +#[test] +fn fallback_syntax_is_used_when_no_syntax_is_detected() { + let content = "# comment\nfoo=bar\n"; + + let fallback_output = bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--file-name=unknown.fallbacksyntax") + .arg("--fallback-syntax=bash") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + let explicit_output = bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--language=bash") + .arg("--file-name=unknown.fallbacksyntax") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + assert_eq!( + from_utf8(&fallback_output).expect("output is valid utf-8"), + from_utf8(&explicit_output).expect("output is valid utf-8") + ); +} + +#[test] +fn fallback_syntax_does_not_override_detected_syntax() { + let content = "fn main() { println!(\"hello\"); }\n"; + + let with_fallback = bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--file-name=test.rs") + .arg("--fallback-syntax=json") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + let without_fallback = bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--file-name=test.rs") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + assert_eq!( + from_utf8(&with_fallback).expect("output is valid utf-8"), + from_utf8(&without_fallback).expect("output is valid utf-8") + ); +} + +#[test] +fn fallback_syntax_does_not_override_explicit_language() { + let content = "{\"a\": 1}\n"; + + let with_fallback = bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--language=json") + .arg("--fallback-syntax=rust") + .arg("--file-name=unknown.fallbacksyntax") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + let without_fallback = bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--language=json") + .arg("--file-name=unknown.fallbacksyntax") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + assert_eq!( + from_utf8(&with_fallback).expect("output is valid utf-8"), + from_utf8(&without_fallback).expect("output is valid utf-8") + ); +} + +#[test] +fn invalid_fallback_syntax_returns_error() { + bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--file-name=unknown.fallbacksyntax") + .arg("--fallback-syntax=InvalidSyntax") + .write_stdin("foo\n") + .assert() + .failure() + .stderr(predicate::str::contains("unknown syntax: 'InvalidSyntax'")); +} + #[test] fn show_all_mode() { bat() From 9cce9e04d2a5754d8cfae9d1595bece8999b3ce3 Mon Sep 17 00:00:00 2001 From: Rizky Mirzaviandy Priambodo <142987522+Xavrir@users.noreply.github.com> Date: Sun, 8 Mar 2026 11:57:23 +0700 Subject: [PATCH 42/47] Fix BAT_CONFIG_DIR pointing at system config dir causing duplicate flag errors When BAT_CONFIG_DIR is set to the system config directory (e.g. /etc/bat), both system_config_file() and config_file() resolve to /etc/bat/config. The config file was read twice, causing clap errors for non-repeatable flags like --italic-text. Skip the user config read when it resolves to the same file as the system config, using canonicalization to also handle symlinks. Closes #3589 --- CHANGELOG.md | 1 + src/bin/bat/config.rs | 59 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 293e55dc..21b98d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Add `--fallback-syntax`/`--fallback-language` to apply syntax highlighting only when auto-detection fails, see #1341 (@Xavrir) ## Bugfixes +- Fix `BAT_CONFIG_DIR` pointing at system config directory causing duplicate flag errors. Closes #3589, see #3620 (@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) diff --git a/src/bin/bat/config.rs b/src/bin/bat/config.rs index 7972d808..77d691c3 100644 --- a/src/bin/bat/config.rs +++ b/src/bin/bat/config.rs @@ -2,7 +2,7 @@ use std::env; use std::ffi::OsString; use std::fs; use std::io::{self, Write}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use crate::directories::PROJECT_DIRS; @@ -104,18 +104,32 @@ pub fn generate_config_file() -> bat::error::Result<()> { pub fn get_args_from_config_file() -> Result, shell_words::ParseError> { let mut config = String::new(); - if let Ok(c) = fs::read_to_string(system_config_file()) { + let system_config = system_config_file(); + let user_config = config_file(); + + if let Ok(c) = fs::read_to_string(&system_config) { config.push_str(&c); config.push('\n'); } - if let Ok(c) = fs::read_to_string(config_file()) { - config.push_str(&c); + // Skip the user config if it resolves to the same file as the system config, + // which can happen when BAT_CONFIG_DIR is set to e.g. "/etc/bat". See #3589. + if !same_file(&system_config, &user_config) { + if let Ok(c) = fs::read_to_string(&user_config) { + config.push_str(&c); + } } get_args_from_str(&config) } +fn same_file(a: &Path, b: &Path) -> bool { + match (fs::canonicalize(a), fs::canonicalize(b)) { + (Ok(a), Ok(b)) => a == b, + _ => a == b, + } +} + pub fn get_args_from_env_opts_var() -> Option, shell_words::ParseError>> { env::var("BAT_OPTS").ok().map(|s| get_args_from_str(&s)) } @@ -214,3 +228,40 @@ fn comments() { get_args_from_str(config).unwrap() ); } + +#[test] +fn same_file_identical_paths() { + let dir = tempfile::tempdir().unwrap(); + let file = dir.path().join("config"); + fs::write(&file, "").unwrap(); + assert!(same_file(&file, &file)); +} + +#[test] +fn same_file_different_paths() { + let dir = tempfile::tempdir().unwrap(); + let a = dir.path().join("a"); + let b = dir.path().join("b"); + fs::write(&a, "").unwrap(); + fs::write(&b, "").unwrap(); + assert!(!same_file(&a, &b)); +} + +#[test] +fn same_file_nonexistent() { + let dir = tempfile::tempdir().unwrap(); + let a = dir.path().join("a"); + let b = dir.path().join("b"); + assert!(!same_file(&a, &b)); +} + +#[cfg(unix)] +#[test] +fn same_file_via_symlink() { + let dir = tempfile::tempdir().unwrap(); + let original = dir.path().join("config"); + let link = dir.path().join("link"); + fs::write(&original, "").unwrap(); + std::os::unix::fs::symlink(&original, &link).unwrap(); + assert!(same_file(&original, &link)); +} From f49641a6c320a8fafa72f200d39432d5158fa04b Mon Sep 17 00:00:00 2001 From: Rizky Mirzaviandy Priambodo <142987522+Xavrir@users.noreply.github.com> Date: Sun, 8 Mar 2026 12:07:44 +0700 Subject: [PATCH 43/47] Fix syntax highlighting for symlinked files by resolving target path When a symlink name has no recognizable extension (e.g. Aliases/0install), syntax detection fails because the symlink path doesn't match any syntax. The target file may have a recognizable extension (e.g. Formula/zero-install.rb) but was never consulted. When path-based syntax detection fails with UndetectedSyntax, canonicalize the path to resolve symlinks and retry detection with the target path. This preserves existing behavior where the symlink path itself matches (e.g. .ssh/config), since the original path is tried first. Closes #1001 --- CHANGELOG.md | 1 + src/assets.rs | 63 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21b98d46..5d30114d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ## Bugfixes - 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) diff --git a/src/assets.rs b/src/assets.rs index 1315537f..29247bd7 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -223,18 +223,40 @@ impl HighlightingAssets { } let path = input.path(); - let path_syntax = if let Some(path) = path { - self.get_syntax_for_path( - PathAbs::new(path).map_or_else(|_| path.to_owned(), |p| p.as_path().to_path_buf()), - mapping, - ) + let absolute_path = path.and_then(|p| { + PathAbs::new(p) + .ok() + .map(|abs| abs.as_path().to_path_buf()) + .or_else(|| Some(p.to_owned())) + }); + + let path_syntax = if let Some(ref path) = absolute_path { + self.get_syntax_for_path(path, mapping).or_else(|e| { + // If syntax detection failed on the given path, retry with the + // canonicalized path (which resolves symlinks). This handles + // cases like `Aliases/0install -> ../Formula/zero-install.rb` + // where the symlink name has no extension but the target does. + // See #1001. + if matches!(e, Error::UndetectedSyntax(_)) { + if let Ok(resolved) = fs::canonicalize(path) { + if resolved != *path { + return match self.get_syntax_for_path(&resolved, mapping) { + Ok(syntax) => Ok(syntax), + Err(Error::UndetectedSyntax(_)) => Err(e), + Err(err) => Err(err), + }; + } + } + } + Err(e) + }) } else { Err(Error::UndetectedSyntax("[unknown]".into())) }; + // If a path wasn't provided, or if path based syntax detection + // above failed, we fall back to first-line syntax detection. match path_syntax { - // If a path wasn't provided, or if path based syntax detection - // above failed, we fall back to first-line syntax detection. Err(Error::UndetectedSyntax(path)) => { if let Some(syntax_in_set) = self.get_first_line_syntax(&mut input.reader)? { Ok(syntax_in_set) @@ -765,4 +787,31 @@ contexts: "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" + ); + } } From 91965d28a6cebd6ed7aebc27f7c5b3d0b73d9261 Mon Sep 17 00:00:00 2001 From: Alex Dukhan Date: Wed, 11 Mar 2026 14:36:20 +0000 Subject: [PATCH 44/47] Add COBOL sans CPY ext --- assets/syntaxes/02_Extra/COBOL | 2 +- .../highlighted/COBOL/payroll.cbl | 61 +++++++++++++++++++ tests/syntax-tests/source/COBOL/payroll.cbl | 61 +++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 tests/syntax-tests/highlighted/COBOL/payroll.cbl create mode 100644 tests/syntax-tests/source/COBOL/payroll.cbl diff --git a/assets/syntaxes/02_Extra/COBOL b/assets/syntaxes/02_Extra/COBOL index 90f88bf6..00a12937 160000 --- a/assets/syntaxes/02_Extra/COBOL +++ b/assets/syntaxes/02_Extra/COBOL @@ -1 +1 @@ -Subproject commit 90f88bf65f339be3b42d6c490bca0a4a85cefad0 +Subproject commit 00a12937cfff328f24db55ce7955682542cc872d diff --git a/tests/syntax-tests/highlighted/COBOL/payroll.cbl b/tests/syntax-tests/highlighted/COBOL/payroll.cbl new file mode 100644 index 00000000..11e19fe9 --- /dev/null +++ b/tests/syntax-tests/highlighted/COBOL/payroll.cbl @@ -0,0 +1,61 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. PAYROLL-CALC. + +DATA DIVISION. +WORKING-STORAGE SECTION. + 01 EMPLOYEE-DETAILS. + 05 EMPLOYEE-NAME PIC X(30). + 05 HOURS-WORKED PIC 99V9. + 05 HOURLY-RATE PIC 99V99. + 01 PAY-CALCULATIONS. + 05 GROSS-PAY PIC 9(5)V99. + 05 OVERTIME-HOURS PIC 99V9. + 05 OVERTIME-PAY PIC 9(5)V99. + 05 TAX-RATE PIC V99 VALUE 0.10. + 05 TAX-AMOUNT PIC 9(5)V99. + 05 NET-PAY PIC 9(5)V99. + +PROCEDURE DIVISION. +MAIN-LOGIC. + DISPLAY "--- COBOL Payroll Calculator ---". + + DISPLAY "Enter Employee Name: ". + ACCEPT EMPLOYEE-NAME. + + DISPLAY "Enter Hours Worked (e.g., 40.5): ". + ACCEPT HOURS-WORKED. + + DISPLAY "Enter Hourly Rate (e.g., 15.75): ". + ACCEPT HOURLY-RATE. + + PERFORM CALCULATE-GROSS-PAY. + PERFORM CALCULATE-TAX. + PERFORM CALCULATE-NET-PAY. + PERFORM DISPLAY-RESULTS. + + STOP RUN. + +CALCULATE-GROSS-PAY. + IF HOURS-WORKED > 40 THEN + COMPUTE OVERTIME-HOURS = HOURS-WORKED - 40 + COMPUTE OVERTIME-PAY = OVERTIME-HOURS * HOURLY-RATE * 1.5 + COMPUTE GROSS-PAY = (40 * HOURLY-RATE) + OVERTIME-PAY + ELSE + COMPUTE GROSS-PAY = HOURS-WORKED * HOURLY-RATE + END-IF. + +CALCULATE-TAX. + COMPUTE TAX-AMOUNT = GROSS-PAY * TAX-RATE. + +CALCULATE-NET-PAY. + COMPUTE NET-PAY = GROSS-PAY - TAX-AMOUNT. + +DISPLAY-RESULTS. + DISPLAY "----------------------------------". + DISPLAY "Employee Name: " EMPLOYEE-NAME. + DISPLAY "Hours Worked: " HOURS-WORKED. + DISPLAY "Hourly Rate: " HOURLY-RATE. + DISPLAY "Gross Pay: " GROSS-PAY. + DISPLAY "Tax (10%): " TAX-AMOUNT. + DISPLAY "Net Pay: " NET-PAY. + DISPLAY "----------------------------------". \ No newline at end of file diff --git a/tests/syntax-tests/source/COBOL/payroll.cbl b/tests/syntax-tests/source/COBOL/payroll.cbl new file mode 100644 index 00000000..ab552167 --- /dev/null +++ b/tests/syntax-tests/source/COBOL/payroll.cbl @@ -0,0 +1,61 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. PAYROLL-CALC. + +DATA DIVISION. +WORKING-STORAGE SECTION. + 01 EMPLOYEE-DETAILS. + 05 EMPLOYEE-NAME PIC X(30). + 05 HOURS-WORKED PIC 99V9. + 05 HOURLY-RATE PIC 99V99. + 01 PAY-CALCULATIONS. + 05 GROSS-PAY PIC 9(5)V99. + 05 OVERTIME-HOURS PIC 99V9. + 05 OVERTIME-PAY PIC 9(5)V99. + 05 TAX-RATE PIC V99 VALUE 0.10. + 05 TAX-AMOUNT PIC 9(5)V99. + 05 NET-PAY PIC 9(5)V99. + +PROCEDURE DIVISION. +MAIN-LOGIC. + DISPLAY "--- COBOL Payroll Calculator ---". + + DISPLAY "Enter Employee Name: ". + ACCEPT EMPLOYEE-NAME. + + DISPLAY "Enter Hours Worked (e.g., 40.5): ". + ACCEPT HOURS-WORKED. + + DISPLAY "Enter Hourly Rate (e.g., 15.75): ". + ACCEPT HOURLY-RATE. + + PERFORM CALCULATE-GROSS-PAY. + PERFORM CALCULATE-TAX. + PERFORM CALCULATE-NET-PAY. + PERFORM DISPLAY-RESULTS. + + STOP RUN. + +CALCULATE-GROSS-PAY. + IF HOURS-WORKED > 40 THEN + COMPUTE OVERTIME-HOURS = HOURS-WORKED - 40 + COMPUTE OVERTIME-PAY = OVERTIME-HOURS * HOURLY-RATE * 1.5 + COMPUTE GROSS-PAY = (40 * HOURLY-RATE) + OVERTIME-PAY + ELSE + COMPUTE GROSS-PAY = HOURS-WORKED * HOURLY-RATE + END-IF. + +CALCULATE-TAX. + COMPUTE TAX-AMOUNT = GROSS-PAY * TAX-RATE. + +CALCULATE-NET-PAY. + COMPUTE NET-PAY = GROSS-PAY - TAX-AMOUNT. + +DISPLAY-RESULTS. + DISPLAY "----------------------------------". + DISPLAY "Employee Name: " EMPLOYEE-NAME. + DISPLAY "Hours Worked: " HOURS-WORKED. + DISPLAY "Hourly Rate: " HOURLY-RATE. + DISPLAY "Gross Pay: " GROSS-PAY. + DISPLAY "Tax (10%): " TAX-AMOUNT. + DISPLAY "Net Pay: " NET-PAY. + DISPLAY "----------------------------------". \ No newline at end of file From 927873392b87e50b349c2ec11a0d07d5f400d3de Mon Sep 17 00:00:00 2001 From: Alex Dukhan Date: Wed, 11 Mar 2026 15:06:47 +0000 Subject: [PATCH 45/47] Drop .ss extension from COBOL --- assets/syntaxes/02_Extra/COBOL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/syntaxes/02_Extra/COBOL b/assets/syntaxes/02_Extra/COBOL index 00a12937..27825ee0 160000 --- a/assets/syntaxes/02_Extra/COBOL +++ b/assets/syntaxes/02_Extra/COBOL @@ -1 +1 @@ -Subproject commit 00a12937cfff328f24db55ce7955682542cc872d +Subproject commit 27825ee0220fb7fee73617f7404d4d214dc71b8f From ac97356930695dd33b3ff664758406b2384b7440 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:54:46 +0000 Subject: [PATCH 46/47] build(deps): bump assets/syntaxes/02_Extra/cmd-help Bumps [assets/syntaxes/02_Extra/cmd-help](https://github.com/victor-gp/cmd-help-sublime-syntax) from `273cb98` to `db0a616`. - [Commits](https://github.com/victor-gp/cmd-help-sublime-syntax/compare/273cb988177e96f4187e06008b13fa72ad22ae4d...db0a616cfa367a4f52fd81d1a46c8ebea11e867d) --- updated-dependencies: - dependency-name: assets/syntaxes/02_Extra/cmd-help dependency-version: db0a616cfa367a4f52fd81d1a46c8ebea11e867d dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- assets/syntaxes/02_Extra/cmd-help | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/syntaxes/02_Extra/cmd-help b/assets/syntaxes/02_Extra/cmd-help index 273cb988..2757ac16 160000 --- a/assets/syntaxes/02_Extra/cmd-help +++ b/assets/syntaxes/02_Extra/cmd-help @@ -1 +1 @@ -Subproject commit 273cb988177e96f4187e06008b13fa72ad22ae4d +Subproject commit 2757ac168123de230cd4374d6a44e0db9cd10f4c From b1f04995a64e3359d4519c04f31efa56ac282292 Mon Sep 17 00:00:00 2001 From: XploitMonk0x01 Date: Sat, 14 Mar 2026 11:44:38 +0530 Subject: [PATCH 47/47] docs: clarify supported custom theme format --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 771db7ad..95f4e2a4 100644 --- a/README.md +++ b/README.md @@ -581,7 +581,8 @@ syntax: This works very similar to how we add new syntax definitions. > [!NOTE] -> Themes are stored in [`.tmTheme` files](https://www.sublimetext.com/docs/color_schemes_tmtheme.html). +> Custom themes must be stored in [`.tmTheme` files](https://www.sublimetext.com/docs/color_schemes_tmtheme.html). +> Newer `.sublime-color-scheme` files are currently not supported. First, create a folder with the new syntax highlighting themes: ```bash