From 3ebfbb7ac50a0b2ccebb4fcef9ba37e4d52c1068 Mon Sep 17 00:00:00 2001 From: Guy Barzilai Date: Tue, 17 Feb 2026 21:44:46 +0200 Subject: [PATCH 01/26] 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 02/26] 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 03/26] 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 04/26] 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 05/26] 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 06/26] 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 07/26] 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 08/26] 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 09/26] 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 10/26] 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 11/26] 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 12/26] 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 13/26] 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 14/26] 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 15/26] 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 16/26] 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 17/26] 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 18/26] 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 19/26] 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 20/26] 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 21/26] 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 22/26] =?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 23/26] 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 24/26] 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 25/26] 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 26/26] 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" + ); + } }