1
0
mirror of https://github.com/sharkdp/bat synced 2026-06-09 10:03:18 +00:00

Merge branch 'master' into add-cobol-syntax

This commit is contained in:
Alex Dukhan
2026-03-11 11:26:01 +00:00
committed by GitHub
24 changed files with 1413 additions and 73 deletions
+9
View File
@@ -1,15 +1,23 @@
# unreleased
- Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l)
- Fixed syntax tests path, see #3610 (@foxfromworld)
## 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)
- 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
- Fix `BAT_CONFIG_DIR` pointing at system config directory causing duplicate flag errors. Closes #3589, see #3620 (@Xavrir)
- Fix syntax highlighting for symlinked files when the symlink name has no extension but the target does. Closes #1001, see #3621 (@Xavrir)
- Report error when pager is missing instead of silently falling back, see #3588 (@IMaloney)
- Fix `--wrap=never` and `-S` flags being ignored when piping to pager, see #3592 (@IMaloney)
- Fix crash with BusyBox `less` on Windows, see #3527 (@Anchal-T)
- Fix `bat cache --help` failing with 'unexpected argument' error, see #3580 and #3560 (@NORMAL-EX)
- `--help` now correctly honors `--pager=builtin`. See #3516 (@keith-hall)
@@ -25,6 +33,7 @@
- Change the URL of Zig submodule from GitHub to Codeberg, see #3519 (@sorairolake)
- Don't color strings inside CSV files, to make it easier to tell which column they belong to, see #3521 (@keith-hall)
- Add syntax highlighting support for COBOL, see #3584 (@adukhan99)
- Fixed manpage syntax so that ANSI escape codes don't get incorrectly highlighted and thus broken, see #3586 (@BlueElectivire)
## Themes
+26
View File
@@ -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.
Generated
+18 -18
View File
@@ -223,9 +223,9 @@ checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
[[package]]
name = "bytesize"
version = "1.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc"
checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3"
[[package]]
name = "cc"
@@ -252,18 +252,18 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clap"
version = "4.5.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"
@@ -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",
@@ -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",
@@ -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",
+5 -5
View File
@@ -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"
@@ -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"]
@@ -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"
@@ -117,13 +117,13 @@ 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"
[build-dependencies.clap]
version = "4.5.56"
version = "4.5.60"
optional = true
features = ["wrap_help", "cargo"]
+2 -2
View File
@@ -9,7 +9,7 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
$ArrayCompletion = @('bash', 'fish', 'zsh', 'ps1')
$ArrayWhen = @('auto', 'never', 'always')
$ArrayYesNo = @('never', 'always')
$ArrayWrap = @('always', 'never', 'character')
$ArrayWrap = @('always', 'never', 'character', 'word')
$ArrayBinary = @('no-printing', 'as-text')
$ArrayPrint = @('unicode', 'caret')
@@ -135,7 +135,7 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
[CompletionResult]::new('--file-name' , 'file-name' , [CompletionResultType]::ParameterName, 'Specify the name to display for a file.')
[CompletionResult]::new('--diff-context' , 'diff-context' , [CompletionResultType]::ParameterName, 'diff-context')
[CompletionResult]::new('--tabs' , 'tabs' , [CompletionResultType]::ParameterName, 'Set the tab width to T spaces.')
[CompletionResult]::new('--wrap' , 'wrap' , [CompletionResultType]::ParameterName, 'Specify the text-wrapping mode (*auto*, character).')
[CompletionResult]::new('--wrap' , 'wrap' , [CompletionResultType]::ParameterName, 'Specify the text-wrapping mode (*auto*, never, character, word).')
[CompletionResult]::new('--terminal-width' , 'terminal-width' , [CompletionResultType]::ParameterName, 'Explicitly set the width of the terminal instead of determining it automatically. If prefixed with ''+'' or ''-'', the value will be treated as an offset to the actual terminal width. See also: ''--wrap''.')
[CompletionResult]::new('--color' , 'color' , [CompletionResultType]::ParameterName, 'When to use colors (*auto*, never, always).')
[CompletionResult]::new('--italic-text' , 'italic-text' , [CompletionResultType]::ParameterName, 'Use italics in output (always, *never*)')
+1 -1
View File
@@ -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)
+1
View File
@@ -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.
+1 -1
View File
@@ -34,7 +34,7 @@ _{{PROJECT_EXECUTABLE}}_main() {
'(-d --diff)'--diff'[only show lines that have been added/removed/modified]'
--diff-context='[specify lines of context around added/removed/modified lines when using `--diff`]:lines'
--tabs='[set the tab width]:tab width [4]'
--wrap='[specify the text-wrapping mode]:mode [auto]:(auto never character)'
--wrap='[specify the text-wrapping mode]:mode [auto]:(auto never character word)'
'!(--wrap)'{-S,--chop-long-lines}
--terminal-width='[explicitly set the width of the terminal instead of determining it automatically]:width'
'(-n --number --diff --diff-context)'{-n,--number}'[show line numbers]'
+4 -2
View File
@@ -102,8 +102,10 @@ Set the tab width to T spaces. Use a width of 0 to pass tabs through directly
.HP
\fB\-\-wrap\fR <mode>
.IP
Specify the text\-wrapping mode (*auto*, never, character). The '\-\-terminal\-width' option
can be used in addition to control the output width.
Specify the text\-wrapping mode (*auto*, never, character, word). The '\-\-terminal\-width' option
can be used in addition to control the output width. In \fBword\fR mode, lines are broken at
whitespace boundaries. If a single word exceeds the terminal width, it falls back to
character wrapping.
.HP
\fB\-S\fR, \fB\-\-chop\-long\-lines\fR
.IP
+2 -1
View File
@@ -9,6 +9,7 @@ scope: source.man
variables:
section_heading: '^(?!#)\S.*$'
command_line_option: '(--?[A-Za-z0-9][_A-Za-z0-9-]*)'
ansi_escape_sequence: '\e\[[\?=]?(?:\d+;?)*[A-Za-z]'
contexts:
prototype:
@@ -69,7 +70,7 @@ contexts:
escape: '(?={{section_heading}})'
function-call:
- match: '\b([A-Za-z0-9_\-]+\.)?([A-Za-z0-9_\-]+)(\()([^)]*)(\))'
- match: '(?<!\e\[)(?:\b|\s)(?:{{ansi_escape_sequence}})?([A-Za-z0-9_\-]+\.)?([A-Za-z0-9_\-]+)(?:{{ansi_escape_sequence}})?(\()([^)]*)(\))'
captures:
1: entity.name.function.man
2: entity.name.function.man
+9 -2
View File
@@ -37,6 +37,13 @@ Options:
name (like 'C++' or 'LaTeX') or possible file extension (like 'cpp', 'hpp' or 'md'). Use
'--list-languages' to show all supported language names and file extensions.
--fallback-syntax <fallback-syntax>
Set a fallback language for syntax highlighting when auto-detection fails. Unlike
'--language', this is only used when no syntax could be detected from filename, custom
syntax mappings, or first-line detection.
[aliases: --fallback-language]
-H, --highlight-line <N:M>
Highlight the specified line ranges with a different background color For example:
'--highlight-line 40' highlights line 40
@@ -61,8 +68,8 @@ Options:
Set the tab width to T spaces. Use a width of 0 to pass tabs through directly
--wrap <mode>
Specify the text-wrapping mode (*auto*, never, character). The '--terminal-width' option
can be used in addition to control the output width.
Specify the text-wrapping mode (*auto*, never, character, word). The '--terminal-width'
option can be used in addition to control the output width.
-S, --chop-long-lines
Truncate all lines longer than screen width. Alias for '--wrap=never'.
+3 -1
View File
@@ -17,6 +17,8 @@ Options:
Show plain style (alias for '--style=plain').
-l, --language <language>
Set the language for syntax highlighting.
--fallback-syntax <fallback-syntax>
Set a fallback language for undetected syntaxes. [aliases: --fallback-language]
-H, --highlight-line <N:M>
Highlight lines N through M.
--file-name <name>
@@ -26,7 +28,7 @@ Options:
--tabs <T>
Set the tab width to T spaces.
--wrap <mode>
Specify the text-wrapping mode (*auto*, never, character).
Specify the text-wrapping mode (*auto*, never, character, word).
-S, --chop-long-lines
Truncate all lines longer than screen width. Alias for '--wrap=never'.
-n, --number
+143 -15
View File
@@ -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<SyntaxReferenceInSet<'_>> {
@@ -222,21 +223,50 @@ impl HighlightingAssets {
}
let path = input.path();
let path_syntax = if let Some(path) = path {
self.get_syntax_for_path(
PathAbs::new(path).map_or_else(|_| path.to_owned(), |p| p.as_path().to_path_buf()),
mapping,
)
let absolute_path = path.and_then(|p| {
PathAbs::new(p)
.ok()
.map(|abs| abs.as_path().to_path_buf())
.or_else(|| Some(p.to_owned()))
});
let path_syntax = if let Some(ref path) = absolute_path {
self.get_syntax_for_path(path, mapping).or_else(|e| {
// If syntax detection failed on the given path, retry with the
// canonicalized path (which resolves symlinks). This handles
// cases like `Aliases/0install -> ../Formula/zero-install.rb`
// where the symlink name has no extension but the target does.
// See #1001.
if matches!(e, Error::UndetectedSyntax(_)) {
if let Ok(resolved) = fs::canonicalize(path) {
if resolved != *path {
return match self.get_syntax_for_path(&resolved, mapping) {
Ok(syntax) => Ok(syntax),
Err(Error::UndetectedSyntax(_)) => Err(e),
Err(err) => Err(err),
};
}
}
}
Err(e)
})
} else {
Err(Error::UndetectedSyntax("[unknown]".into()))
};
// If a path wasn't provided, or if path based syntax detection
// above failed, we fall back to first-line syntax detection.
match path_syntax {
// If a path wasn't provided, or if path based syntax detection
// above failed, we fall back to first-line syntax detection.
Err(Error::UndetectedSyntax(path)) => self
.get_first_line_syntax(&mut input.reader)?
.ok_or(Error::UndetectedSyntax(path)),
Err(Error::UndetectedSyntax(path)) => {
if let Some(syntax_in_set) = self.get_first_line_syntax(&mut input.reader)? {
Ok(syntax_in_set)
} else if let Some(language) = fallback_syntax {
self.find_syntax_by_token(language)?
.ok_or_else(|| Error::UnknownSyntax(language.to_owned()))
} else {
Err(Error::UndetectedSyntax(path))
}
}
_ => path_syntax,
}
}
@@ -262,6 +292,24 @@ impl HighlightingAssets {
.map(|syntax| SyntaxReferenceInSet { syntax, syntax_set }))
}
fn find_syntax_by_hidden_file_name(
&self,
file_name: &OsStr,
) -> Result<Option<SyntaxReferenceInSet<'_>>> {
let Some(hidden_file_extension) = file_name
.to_str()
.and_then(|name| name.strip_prefix('.'))
.filter(|name| !name.is_empty())
else {
return Ok(None);
};
// syntect stores `hidden_file_extensions` in the same extension list as
// regular file extensions, but dotfiles must be queried without the
// leading period.
self.find_syntax_by_extension(Some(OsStr::new(hidden_file_extension)))
}
fn find_syntax_by_token(&self, token: &str) -> Result<Option<SyntaxReferenceInSet<'_>>> {
let syntax_set = self.get_syntax_set()?;
Ok(syntax_set
@@ -275,6 +323,9 @@ impl HighlightingAssets {
ignored_suffixes: &IgnoredSuffixes,
) -> Result<Option<SyntaxReferenceInSet<'_>>> {
let mut syntax = self.find_syntax_by_extension(Some(file_name))?;
if syntax.is_none() {
syntax = self.find_syntax_by_hidden_file_name(file_name)?;
}
if syntax.is_none() {
syntax =
ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| {
@@ -395,11 +446,12 @@ mod tests {
fn get_syntax_name(
&self,
language: Option<&str>,
fallback_syntax: Option<&str>,
input: &mut OpenedInput,
mapping: &SyntaxMapping,
) -> String {
self.assets
.get_syntax(language, input, mapping)
.get_syntax(language, fallback_syntax, input, mapping)
.map(|syntax_in_set| syntax_in_set.syntax.name.clone())
.unwrap_or_else(|_| "!no syntax!".to_owned())
}
@@ -419,7 +471,7 @@ mod tests {
let dummy_stdin: &[u8] = &[];
let mut opened_input = input.open(dummy_stdin, None).unwrap();
self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
self.get_syntax_name(None, None, &mut opened_input, &self.syntax_mapping)
}
fn syntax_for_file_with_content_os(&self, file_name: &OsStr, first_line: &str) -> String {
@@ -429,7 +481,7 @@ mod tests {
let dummy_stdin: &[u8] = &[];
let mut opened_input = input.open(dummy_stdin, None).unwrap();
self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
self.get_syntax_name(None, None, &mut opened_input, &self.syntax_mapping)
}
#[cfg(unix)]
@@ -449,7 +501,7 @@ mod tests {
let input = Input::stdin().with_name(Some(file_name));
let mut opened_input = input.open(content, None).unwrap();
self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping)
self.get_syntax_name(None, None, &mut opened_input, &self.syntax_mapping)
}
fn syntax_is_same_for_inputkinds(&self, file_name: &str, content: &str) -> bool {
@@ -661,6 +713,55 @@ mod tests {
);
}
#[cfg(feature = "build-assets")]
#[test]
fn syntax_detection_hidden_file_extensions() {
let source_dir = TempDir::new().expect("creation of temporary source directory");
let cache_dir = TempDir::new().expect("creation of temporary cache directory");
let syntax_dir = source_dir.path().join("syntaxes");
std::fs::create_dir_all(&syntax_dir).expect("creation of syntax directory succeeds");
std::fs::write(
syntax_dir.join("HiddenFileExtension.sublime-syntax"),
r#"%YAML 1.2
---
name: Hidden File Extension
hidden_file_extensions:
- testrc
scope: source.hiddenfileextension
contexts:
main:
- match: .
scope: source.hiddenfileextension
"#,
)
.expect("custom syntax can be written");
build(
source_dir.path(),
false,
false,
cache_dir.path(),
env!("CARGO_PKG_VERSION"),
)
.expect("custom assets can be built");
let test = SyntaxDetectionTest {
assets: HighlightingAssets::from_cache(cache_dir.path())
.expect("custom syntax cache can be loaded"),
syntax_mapping: SyntaxMapping::new(),
temp_dir: TempDir::new().expect("creation of temporary directory"),
};
assert_eq!(test.syntax_for_file(".testrc"), "Hidden File Extension");
assert_eq!(
test.syntax_for_stdin_with_content(".testrc", b""),
"Hidden File Extension"
);
assert!(test.syntax_is_same_for_inputkinds(".testrc", ""));
}
#[cfg(unix)]
#[test]
fn syntax_detection_for_symlinked_file() {
@@ -682,8 +783,35 @@ mod tests {
let mut opened_input = input.open(dummy_stdin, None).unwrap();
assert_eq!(
test.get_syntax_name(None, &mut opened_input, &test.syntax_mapping),
test.get_syntax_name(None, None, &mut opened_input, &test.syntax_mapping),
"SSH Config"
);
}
#[cfg(unix)]
#[test]
fn syntax_detection_for_symlinked_file_by_target_extension() {
use std::os::unix::fs::symlink;
let test = SyntaxDetectionTest::new();
let formula_dir = test.temp_dir.path().join("Formula");
std::fs::create_dir(&formula_dir).unwrap();
let target = formula_dir.join("zero-install.rb");
File::create(&target).unwrap();
let aliases_dir = test.temp_dir.path().join("Aliases");
std::fs::create_dir(&aliases_dir).unwrap();
let link = aliases_dir.join("0install");
symlink(&target, &link).unwrap();
let input = Input::ordinary_file(&link);
let dummy_stdin: &[u8] = &[];
let mut opened_input = input.open(dummy_stdin, None).unwrap();
assert_eq!(
test.get_syntax_name(None, None, &mut opened_input, &test.syntax_mapping),
"Ruby"
);
}
}
+18 -11
View File
@@ -384,6 +384,10 @@ impl App {
None
}
}),
fallback_syntax: self
.matches
.get_one::<String>("fallback-syntax")
.map(|s| s.as_str()),
show_nonprintable: self.matches.get_flag("show-all"),
nonprintable_notation: match self
.matches
@@ -399,27 +403,30 @@ impl App {
Some("no-printing") => BinaryBehavior::NoPrinting,
_ => unreachable!("other values for --binary are not allowed"),
},
wrapping_mode: if self.interactive_output || maybe_term_width.is_some() {
if !self.matches.get_flag("chop-long-lines") {
wrapping_mode: {
if self.matches.get_flag("chop-long-lines") {
WrappingMode::NoWrapping(true)
} else {
match self.matches.get_one::<String>("wrap").map(|s| s.as_str()) {
Some("character") => WrappingMode::Character,
Some("word") => WrappingMode::Word,
Some("never") => WrappingMode::NoWrapping(true),
Some("auto") | None => {
if style_components.plain() && maybe_term_width.is_none() {
WrappingMode::NoWrapping(false)
if self.interactive_output || maybe_term_width.is_some() {
if style_components.plain() && maybe_term_width.is_none() {
WrappingMode::NoWrapping(false)
} else {
WrappingMode::Character
}
} else {
WrappingMode::Character
// We don't have the tty width when piping to another program.
// There's no point in wrapping when this is the case.
WrappingMode::NoWrapping(false)
}
}
_ => unreachable!("other values for --wrap are not allowed"),
}
} else {
WrappingMode::NoWrapping(true)
}
} else {
// We don't have the tty width when piping to another program.
// There's no point in wrapping when this is the case.
WrappingMode::NoWrapping(false)
},
colored_output: self.matches.get_flag("force-colorization")
|| match self.matches.get_one::<String>("color").map(|s| s.as_str()) {
+14 -3
View File
@@ -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")
@@ -211,11 +222,11 @@ pub fn build_app(interactive_output: bool) -> Command {
.long("wrap")
.overrides_with("wrap")
.value_name("mode")
.value_parser(["auto", "never", "character"])
.value_parser(["auto", "never", "character", "word"])
.default_value("auto")
.hide_default_value(true)
.help("Specify the text-wrapping mode (*auto*, never, character).")
.long_help("Specify the text-wrapping mode (*auto*, never, character). \
.help("Specify the text-wrapping mode (*auto*, never, character, word).")
.long_help("Specify the text-wrapping mode (*auto*, never, character, word). \
The '--terminal-width' option can be used in addition to \
control the output width."),
)
+55 -4
View File
@@ -2,7 +2,7 @@ use std::env;
use std::ffi::OsString;
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use crate::directories::PROJECT_DIRS;
@@ -104,18 +104,32 @@ pub fn generate_config_file() -> bat::error::Result<()> {
pub fn get_args_from_config_file() -> Result<Vec<OsString>, shell_words::ParseError> {
let mut config = String::new();
if let Ok(c) = fs::read_to_string(system_config_file()) {
let system_config = system_config_file();
let user_config = config_file();
if let Ok(c) = fs::read_to_string(&system_config) {
config.push_str(&c);
config.push('\n');
}
if let Ok(c) = fs::read_to_string(config_file()) {
config.push_str(&c);
// Skip the user config if it resolves to the same file as the system config,
// which can happen when BAT_CONFIG_DIR is set to e.g. "/etc/bat". See #3589.
if !same_file(&system_config, &user_config) {
if let Ok(c) = fs::read_to_string(&user_config) {
config.push_str(&c);
}
}
get_args_from_str(&config)
}
fn same_file(a: &Path, b: &Path) -> bool {
match (fs::canonicalize(a), fs::canonicalize(b)) {
(Ok(a), Ok(b)) => a == b,
_ => a == b,
}
}
pub fn get_args_from_env_opts_var() -> Option<Result<Vec<OsString>, shell_words::ParseError>> {
env::var("BAT_OPTS").ok().map(|s| get_args_from_str(&s))
}
@@ -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));
}
+3
View File
@@ -38,6 +38,9 @@ pub struct Config<'a> {
/// The explicitly configured language, if any
pub language: Option<&'a str>,
/// The fallback syntax used when auto-detection fails
pub fallback_syntax: Option<&'a str>,
/// Whether or not to show/replace non-printable characters like space, tab and newline.
pub show_nonprintable: bool,
+13 -3
View File
@@ -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
);
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 {
@@ -215,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(_) => (),
+56 -4
View File
@@ -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
@@ -781,11 +786,21 @@ impl Printer for InteractivePrinter<'_> {
// Displayed width of line_buf
let mut current_width = 0;
let word_wrap = matches!(self.config.wrapping_mode, WrappingMode::Word);
// For word wrapping, track last whitespace position.
let mut last_ws_idx: Option<usize> = None;
for c in text.chars() {
// calculate the displayed width for next character
let cw = c.width().unwrap_or(0);
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 +822,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 +865,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);
+1
View File
@@ -1,6 +1,7 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WrappingMode {
Character,
Word,
// The bool specifies whether wrapping has been explicitly disabled by the user via --wrap=never
NoWrapping(bool),
}
+3
View File
@@ -0,0 +1,3 @@
The quick brown fox jumps over the lazy dog and then runs away
superlongwordthatdefinitelyexceedstheterminalwidthandshouldfallbacktocharacterwrapping
short words here
+228
View File
@@ -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() {
@@ -1442,6 +1457,7 @@ fn env_var_bat_paging() {
#[test]
fn basic_set_terminal_title() {
bat()
.env("BAT_PAGER", "cat")
.arg("--paging=always")
.arg("--set-terminal-title")
.arg("test.txt")
@@ -2454,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()
@@ -2884,6 +3015,44 @@ fn no_wrapping_with_chop_long_lines() {
wrapping_test("--chop-long-lines", false);
}
#[test]
#[serial]
fn wrap_never_flag_respected_with_paging_always() {
mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| {
bat()
.arg("--pager=cat")
.arg("--paging=always")
.arg("--wrap=never")
.arg("--color=never")
.arg("--decorations=never")
.arg("--style=plain")
.write_stdin("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz\n")
.assert()
.success()
.stdout(predicate::str::contains("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz").normalize())
.stderr("");
});
}
#[test]
#[serial]
fn s_flag_respected_with_paging_always() {
mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| {
bat()
.arg("--pager=cat")
.arg("--paging=always")
.arg("-S")
.arg("--color=never")
.arg("--decorations=never")
.arg("--style=plain")
.write_stdin("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz\n")
.assert()
.success()
.stdout(predicate::str::contains("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz").normalize())
.stderr("");
});
}
#[test]
fn theme_arg_overrides_env() {
bat()
@@ -3737,3 +3906,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");
}
+399
View File
@@ -0,0 +1,399 @@
UWSM(1) General Commands Manual UWSM(1)
NAME
 UWSM - Universal Wayland Session Manager.
SYNOPSIS
 uwsm [-h|-v] {subcommand} [options ...]
DESCRIPTION
 Launches arbitrary wayland compositor via a set of systemd user units to provide graphical user
 session with environment management, XDG autostart support, clean shutdown. Provides helpers
 for launching applications as scopes or services.
SUBCOMMANDS
 select Select default compositor Entry.
 start Start compositor and graphical session.
 finalize Send compositor-set variables and unit startup notification to systemd user manager.
 stop Stop graphical session and compositor.
 app Application unit launcher (with Desktop Entry support).
 check Perform state checks (for scripting and info).
 aux Technical functions for use inside units.
 See corresponding SUBCOMMANDS subsections below for further info.
 Help for each subcommand is accessible by running "uwsm {subcommand} -h".
CONFIGURATION
 Files
 In XDG config hierarchy:
 uwsm/env
 uwsm/env.d/*
 uwsm/env-${compositor}
 uwsm/env-${compositor}.d/* Environment (shell) to be sourced for the graphical session.
 Sourced from directories of increasing priority, in each directory
 common file is sourced first, then suffixed files in the order of
 items listed in XDG_CURRENT_SESSION var (lowercased).
 uwsm/default-id Stores Desktop Entry ID of default compositor.
 Fallback is also extended into the system part of XDG data hierarchy, this can be used for dis
 tro level defaults.
 Environment vars
 UWSM_UNIT_RUNG (run|home)
 Which rung of systemd/user/ hierarchy to manage generated unit
 and drop-in files in: $XDG_RUNTIME_DIR or $XDG_CONFIG_HOME.
 UWSM_TWEAKS (boolean value)
 Set to False to remove and not generate tweak drop-ins for
 other software.
 UWSM_FINALIZE_VARNAMES (whitespace-separated names of env vars)
 Additional variables for "uwsm finalize".
 UWSM_WAIT_VARNAMES (whitespace-separated names of env vars)
 Variables to wait for in activation environment before proceed
 ing to graphical session (in addition to WAYLAND_DISPLAY).
 UWSM_WAIT_VARNAMES_TIMEOUT (int value)
 Seconds to wait for variables to appear in activation environ
 ment. Essentially, startup timeout (default: 10).
 UWSM_WAIT_VARNAMES_SETTLETIME (float value)
 Seconds to pause after all expected vars found in activation
 environment (default: 0.2).
 UWSM_APP_UNIT_TYPE (scope|service)
 Default unit type for launching apps (default: scope).
 UWSM_SILENT_START (int or boolean value)
 True or 1 to inhibit stdout messages from "uwsm start". 2 to
 also inhibit warnings.
 DEBUG (int or boolean value)
 True or positive number to dump debug info to stderr.
OPERATION OVERVIEW
 Login Sequence Integration
 uwsm can be launched by using conditional exec in shell profile to replace login shell (see
 Shell Profile Integration section).
 Alternatively "uwsm start ..." command can be put into wayland session's Desktop Entry to be
 launched by a display manager (see Use Inside Desktop Entry section).
 Compositor Selection
 uwsm can run arbitrary compositor command line or a Desktop Entry by ID (specifying Action ID
 is also supported).
 Desktop Entry can also be selected via a whiptail menu (see select subcommand section).
 Startup
 See start subcommand section for command syntax.
 UWSM uses a set of units bound to standard user session targets:
 • wayland-session-pre@.target (bound to graphical-session-pre.target)
 • wayland-wm-env@.service (environment preloader service)
 • wayland-session@.target (bound to graphical-session.target)
 • wayland-wm@.service (service for the selected compositor)
 • wayland-session-xdg-autostart@.target (bound to xdg-desktop-autostart.target)
 • wayland-session-envelope@.target (lives through entire lifecycle)
 • wayland-session-shutdown.target (conflicts with targets above for shutdown)
 • wayland-session-bindpid@.service (PID-tracking session killswitch)
 • wayland-session-waitenv.service (delays graphical session until vars appear)
 Compositor ID (Desktop Entry ID or executable name) becomes the specifier for all templated
 units.
 At the stage of graphical-session-pre.target, the environment saved from "uwsm start" context
 is loaded (or POSIX shell profile is sourced), uwsm environment files are sourced. The delta is
 exported to the systemd and D-Bus activation environments by the environment preloader service
 and is marked for cleanup at shutdown stage. Preloader shell context for convenience has
 IN_UWSM_ENV_PRELOADER var set to true.
 At the stage of graphical-session.target (before it) the main compositor unit wayland-
 wm@${ID}.service and wayland-session-waitenv.service are started.
 Compositor should at least put WAYLAND_DISPLAY variable to systemd activation environment. This
 will trigger uwsm's automatic finalization logic. Without WAYLAND_DISPLAY in activation envi
 ronment startup will timeout in 10 seconds.
 Manual finalization is possible by running "uwsm finalize" (see finalize subcommand section),
 also in combination with tweaking UWSM_WAIT_VARNAMES and UWSM_WAIT_VARNAMES_SETTLETIME vars
 (see Environment vars section).
 Successful activation of compositor unit and existence of WAYLAND_DISPLAY in activation envi
 ronment will allow graphical-session.target to be declared reached.
 Finally, xdg-desktop-autostart.target is activated.
 Inside session
 It is highly recommended to configure the compositor or app launcher to launch apps as scopes
 or services in special user session slices (app.slice, background.slice, session.slice). uwsm
 provides custom nested slices for apps to live in and be terminated on session end:
 • app-graphical.slice
 • background-graphical.slice
 • session-graphical.slice
 A helper app subcommand is provided to handle all the systemd-run invocations for you (see app
 subcommand section).
 The compositor is launched in session.slice by default (as recommended by systemd.special(7)).
 Shutdown
 Can be initiated by either:
 • running uwsm stop
 • stopping wayland-wm@*.service or wayland-session-envelope@*.target
 • starting wayland-session-shutdown.target
 Systemd stops all user units in reverse, as it usually does. During deactivation of graphical-
 session-pre.target, the environment preloader service cleans activation environments by unset
 ting all variables that were marked for removal during startup and finalization stages.
 Do not use compositor's native exit mechanism or kill its process directly.
SUBCOMMANDS
 select
 Selects default wayland session compositor Desktop Entry.
 uwsm select
 Invokes a whiptail menu to select default session among Desktop Entries in wayland-sessions XDG
 data hierarchy. Writes to ${XDG_CONFIG_HOME}/uwsm/default-id. Nothing else is done. Returns 1
 if selection is cancelled. Can be used for scripting launch condition in shell profile.
 check
 Performs tests, returns 0 on success, 1 on failure.
 is-active:
 uwsm check is-active [-h] [-v] [compositor]
 -v show additional info
 compositor check for specific compositor
 Checks if unit of specific compositor or graphical-session*.target in general is in active or
 activating state.
 may-start:
 uwsm check may-start [-h] [-g [S]] [-v|-q] [N ...]
 N ... allowed VT numbers (default: 1)
 -g S wait S seconds for graphical.target in queue (default: 60; 0 or less disables
 check).
 -i do not check for login shell
 -r do not check for local session (allow remote session)
 -v show all failed tests
 -q be quiet
 Checks whether it is OK to launch a wayland session via the following conditions:
 • DBUS_SESSION_BUS_ADDRESS is set
 • Running from login shell
 • System is at graphical.target
 • User graphical-session*.target units are not yet active
 • Foreground VT is among allowed (default: 1)
 • Login session's VT is matching
 start
 Generates units for given compositor command line or Desktop Entry and starts them.
 uwsm start [-h] [-D name[:name...]] [-a|-e] [-N Name] [-C Comment] [-U {run|home}] [-t]
 [-o] [-n] -- compositor [args ...]
 -F Hardcode mode, always write command line to unit drop-ins and use full
 paths.
 -D name[:name...] Names to fill XDG_CURRENT_DESKTOP with (:-separated). Existing var con
 tent is a starting point if no active session is running.
 -a Append desktop names set by -D to other sources (default).
 -e Use desktop names set by -D exclusively, discard other sources.
 -N Name Fancy name for compositor (filled from Desktop Entry by default).
 -C Comment Fancy description for compositor (filled from Desktop Entry by de
 fault).
 -U {run|home} Select rung for generated unit files: run: $XDG_RUNTIME_DIR/sys
 temd/user (default), or home: $XDG_CONFIG_HOME/systemd/user. Permanent
 destination will save some time by removing need for reloading systemd.
 Managed files from other rung will be removed. Can be preset with
 UWSM_UNIT_RUNG environment var.
 -t Do not generate (and remove) tweak unit files. Can be preset with
 UWSM_TWEAKS=false environment var.
 -T Generate tweak unit files for other software. This is default behavior.
 -g S Wait for S seconds for system graphical.target in queue and warn if
 timed out or not in queue (default: 60, negative to disable).
 -G S Wait for S seconds for system graphical.target in queue and abort if
 timed out or not in queue (overrides -g, default: -1, (disabled)).
 -o Only generate units, but do not start.
 -n Dry run, do not write or start anything.
 The first argument of the compositor command line acts as an ID and should be either one of:
 • Executable name
 • Desktop Entry ID (optionally with ":"-delimited action ID)
 • Special value:
 • select - invoke menu to select compositor.
 • default - run previously selected compositor (or select if no selection was saved).
 If given as path, hardcode mode will be used implicitly.
 Always use "--" to disambiguate dashed arguments intended for compositor itself.
 After units are (re)generated, wayland-session-bindpid@${PID}.service is started, to track the
 PID of invoking uwsm, then uwsm process replaces itself with systemctl execution that starts
 wayland-wm@${ID}.service and waits for it to finish.
 In order to complete the startup sequence, the compositor has to put WAYLAND_DISPLAY into the
 systemd activation environment. This can be done explicitly by making compositor run "uwsm fi
 nalize" command (see the next subsection).
 finalize
 For running by a compositor on startup.
 uwsm finalize [-h] [VAR_NAME ...]
 Exports WAYLAND_DISPLAY, DISPLAY and any defined vars mentioned by names in arguments or in
 UWSM_FINALIZE_VARNAMES variable (whitespace-separated). Then sends startup notification for the
 unit to systemd user manager.
 This is required if compositor itself does not put WAYLAND_DISPLAY to systemd activation envi
 ronment, otherwise wayland-session@.service unit or a dedicated wayland-session-waitenv.service
 unit will terminate due to startup timeout.
 UWSM_FINALIZE_VARNAMES variable can be prefilled by plugins.
 Direct assignment as VAR_NAME=value is also possible, but recommended only for creating flags
 for UWSM_WAIT_VARNAMES mechanism.
 stop
 Stops compositor and optionally removes generated units.
 uwsm stop [-h] [-r [compositor] [-U {run|home}] [-n]
 -r [compositor] Also remove units (all or only compositor-specific).
 -U {run|home} Select rung for generated unit files: run: $XDG_RUNTIME_DIR/systemd/user
 (default), or home: $XDG_CONFIG_HOME/systemd/user. Permanent destination
 will save some time by removing need for reloading systemd. Managed files
 from other rung will be removed. Can be preset with UWSM_UNIT_RUNG envi
 ronment var.
 -n Dry run, do not stop or remove anything.
 app
 Application-to-unit launcher with Desktop Entry support.
 uwsm app [-h] [-s {a,b,s,custom.slice}] [-t {scope,service}] [-a app_name] [-u unit_name]
 [-d unit_description] [-S ] [-T] -- application [args ...]
 -s {a,b,s,custom.slice} Slice selector (default: a):
 a - app-graphical.slice
 b - background-graphical.slice
 s - session-graphical.slice
 any slice by full name
 -t {scope,service} Type of unit to launch (default: scope, can be preset by
 UWSM_APP_UNIT_TYPE env var).
 -a app_name Override app name (a substring in unit name).
 -u unit_name Override the whole autogenerated unit name.
 -d unit_description Unit Description.
 -p Property=value Set additional unit property (option is repeatable).
 -S {out,err,both} Silence stdout, stderr, or both.
 -T Launch app in a terminal. Allows command to be empty to just
 launch a terminal.
 Application can be provided as a command with optional arguments, or a Desktop Entry ID, op
 tionally suffixed with ":"-delimited Action ID. If Desktop Entry is being launched, arguments
 should be compatible with it.
 Always use "--" to disambiguate dashed arguments intended for application itself.
 aux
 For use in systemd user services. Can only be called by systemd user manager.
 prepare-env Prepares environment (for use in ExecStart in wayland-wm-env@.service bound to
 wayland-session-pre@.target).
 cleanup-env Cleans up environment (for use ExecStop in in wayland-wm-env@.service bound to
 wayland-session-pre@.target).
 exec Executes a command with arguments or a desktop entry (for use in Exec in wayland-
 wm@.service bound to wayland-session@.target).
 app-daemon Daemon for faster app argument generation, used by uwsm-app client.
APP DAEMON
 Provided as wayland-wm-app-daemon.service to be started on-demand.
 Daemon receives app arguments from ${XDG_RUNTIME_DIR}/uwsm-app-daemon-in pipe. Resulting argu
 ments are formatted as shell code and written to ${XDG_RUNTIME_DIR}/uwsm-app-daemon-out pipe.
 Arguments are expected to be \0-delimited, leading \0 are stripped. One command is received per
 write+close.
 The first argument determines the behavior:
 • app the rest is processed the same as in "uwsm app"
 • ping just "pong" is returnedn
 • stop daemon is stoppedn
 Single commands are prepended with exec, iterated commands are assembled with trailing & each,
 followed by wait.
 The purpose of all this is to skip all the expensive Python startup and import routines that
 slow things down every time "uwsm app" is called. Instead the daemon does it once and then lis
 tens for requests, while a simple shell script may dump arguments to one pipe and run the code
 received from another via eval, which is much faster.
 The simplest script is:
 #!/bin/sh
 printf '0%s' app "$@" > "${XDG_RUNTIME_DIR}/uwsm-app-daemon-in"
 IFS='' read -r cmd < "${XDG_RUNTIME_DIR}/uwsm-app-daemon-out"
 eval "$cmd"
 Provided uwsm-app client script is a bit smarter: it can start the daemon, applies timeouts,
 and supports newlines in returned args.
SHELL PROFILE INTEGRATION
 To launch uwsm automatically on login, add one of constructs below (or similar) to shell pro
 file.
 This asks to select a compositor (or refuse and continue with login shell) when logged in on VT
 1:
 if uwsm check may-start && uwsm select; then
 exec systemd-cat -t uwsm_start uwsm start default
 fi
 This just starts a specific compositor depending on foreground VT:
 if uwsm check may-start 1; then
 exec systemd-cat -t uwsm_start uwsm start sway.desktop
 elif uwsm check may-start 2; then
 exec systemd-cat -t uwsm_start uwsm start labwc.desktop
 fi
 Using "uwsm check may-start" as a condition is essential, not only to prevent accidental
 startup attempts where they are not expected, but also since startup may involve sourcing shell
 profile, which might lead to nasty loops.
 See check subcommand section for info on may-start checker.
 exec allows uwsm to replace login shell in order to properly bind to user session and handle
 session termination.
 "systemd-cat -t uwsm_start" (optional) executes the command given to it (uwsm) with its stdout
 and stderr connected to the systemd journal, tagged with identifier "uwsm_start". See systemd-
 cat(1) for more options.
USE INSIDE DESKTOP ENTRY
 To launch uwsm from a display/login manager, "uwsm start" can be used inside Desktop Entries.
 Example /usr/local/share/wayland-sessions/my-compositor.desktop:
 [Desktop Entry]
 Name=My compositor (with UWSM)
 Comment=My cool compositor
 Exec=uwsm start -N "My compositor" -D mycompositor -C "My cool compositor" mywm
 DesktopNames=mycompositor
 Type=Application
 Things to keep in mind:
 • For consistency, command line arguments should mirror the keys of the entry
 • Command in Exec= should start with "uwsm start"
 • It should not point to itself (as a combination of Desktop Entry ID and Action ID)
 • It should not point to a Desktop Entry ID and Action ID that also uses uwsm
 Potentially such entries may be found and used by uwsm itself, i.e. in shell profile integra
 tion situation, or when launched manually. Following the principles above ensures uwsm will
 properly recognize itself and parse requested arguments inside the entry without any side ef
 fects.
SEE ALSO
 uwsm-plugins(3), systemd-run(1), systemd-cat(1), systemd.special(7)
 2026-02-14 UWSM(1)
+399
View File
@@ -0,0 +1,399 @@
UWSM(1) General Commands Manual UWSM(1)
NAME
UWSM - Universal Wayland Session Manager.
SYNOPSIS
uwsm [-h|-v] {subcommand} [options ...]
DESCRIPTION
Launches arbitrary wayland compositor via a set of systemd user units to provide graphical user
session with environment management, XDG autostart support, clean shutdown. Provides helpers
for launching applications as scopes or services.
SUBCOMMANDS
select Select default compositor Entry.
start Start compositor and graphical session.
finalize Send compositor-set variables and unit startup notification to systemd user manager.
stop Stop graphical session and compositor.
app Application unit launcher (with Desktop Entry support).
check Perform state checks (for scripting and info).
aux Technical functions for use inside units.
See corresponding SUBCOMMANDS subsections below for further info.
Help for each subcommand is accessible by running "uwsm {subcommand} -h".
CONFIGURATION
Files
In XDG config hierarchy:
uwsm/env
uwsm/env.d/*
uwsm/env-${compositor}
uwsm/env-${compositor}.d/* Environment (shell) to be sourced for the graphical session.
Sourced from directories of increasing priority, in each directory
common file is sourced first, then suffixed files in the order of
items listed in XDG_CURRENT_SESSION var (lowercased).
uwsm/default-id Stores Desktop Entry ID of default compositor.
Fallback is also extended into the system part of XDG data hierarchy, this can be used for dis
tro level defaults.
Environment vars
UWSM_UNIT_RUNG (run|home)
Which rung of systemd/user/ hierarchy to manage generated unit
and drop-in files in: $XDG_RUNTIME_DIR or $XDG_CONFIG_HOME.
UWSM_TWEAKS (boolean value)
Set to False to remove and not generate tweak drop-ins for
other software.
UWSM_FINALIZE_VARNAMES (whitespace-separated names of env vars)
Additional variables for "uwsm finalize".
UWSM_WAIT_VARNAMES (whitespace-separated names of env vars)
Variables to wait for in activation environment before proceed
ing to graphical session (in addition to WAYLAND_DISPLAY).
UWSM_WAIT_VARNAMES_TIMEOUT (int value)
Seconds to wait for variables to appear in activation environ
ment. Essentially, startup timeout (default: 10).
UWSM_WAIT_VARNAMES_SETTLETIME (float value)
Seconds to pause after all expected vars found in activation
environment (default: 0.2).
UWSM_APP_UNIT_TYPE (scope|service)
Default unit type for launching apps (default: scope).
UWSM_SILENT_START (int or boolean value)
True or 1 to inhibit stdout messages from "uwsm start". 2 to
also inhibit warnings.
DEBUG (int or boolean value)
True or positive number to dump debug info to stderr.
OPERATION OVERVIEW
Login Sequence Integration
uwsm can be launched by using conditional exec in shell profile to replace login shell (see
Shell Profile Integration section).
Alternatively "uwsm start ..." command can be put into wayland session's Desktop Entry to be
launched by a display manager (see Use Inside Desktop Entry section).
Compositor Selection
uwsm can run arbitrary compositor command line or a Desktop Entry by ID (specifying Action ID
is also supported).
Desktop Entry can also be selected via a whiptail menu (see select subcommand section).
Startup
See start subcommand section for command syntax.
UWSM uses a set of units bound to standard user session targets:
• wayland-session-pre@.target (bound to graphical-session-pre.target)
• wayland-wm-env@.service (environment preloader service)
• wayland-session@.target (bound to graphical-session.target)
• wayland-wm@.service (service for the selected compositor)
• wayland-session-xdg-autostart@.target (bound to xdg-desktop-autostart.target)
• wayland-session-envelope@.target (lives through entire lifecycle)
• wayland-session-shutdown.target (conflicts with targets above for shutdown)
• wayland-session-bindpid@.service (PID-tracking session killswitch)
• wayland-session-waitenv.service (delays graphical session until vars appear)
Compositor ID (Desktop Entry ID or executable name) becomes the specifier for all templated
units.
At the stage of graphical-session-pre.target, the environment saved from "uwsm start" context
is loaded (or POSIX shell profile is sourced), uwsm environment files are sourced. The delta is
exported to the systemd and D-Bus activation environments by the environment preloader service
and is marked for cleanup at shutdown stage. Preloader shell context for convenience has
IN_UWSM_ENV_PRELOADER var set to true.
At the stage of graphical-session.target (before it) the main compositor unit wayland-
wm@${ID}.service and wayland-session-waitenv.service are started.
Compositor should at least put WAYLAND_DISPLAY variable to systemd activation environment. This
will trigger uwsm's automatic finalization logic. Without WAYLAND_DISPLAY in activation envi
ronment startup will timeout in 10 seconds.
Manual finalization is possible by running "uwsm finalize" (see finalize subcommand section),
also in combination with tweaking UWSM_WAIT_VARNAMES and UWSM_WAIT_VARNAMES_SETTLETIME vars
(see Environment vars section).
Successful activation of compositor unit and existence of WAYLAND_DISPLAY in activation envi
ronment will allow graphical-session.target to be declared reached.
Finally, xdg-desktop-autostart.target is activated.
Inside session
It is highly recommended to configure the compositor or app launcher to launch apps as scopes
or services in special user session slices (app.slice, background.slice, session.slice). uwsm
provides custom nested slices for apps to live in and be terminated on session end:
• app-graphical.slice
• background-graphical.slice
• session-graphical.slice
A helper app subcommand is provided to handle all the systemd-run invocations for you (see app
subcommand section).
The compositor is launched in session.slice by default (as recommended by systemd.special(7)).
Shutdown
Can be initiated by either:
• running uwsm stop
• stopping wayland-wm@*.service or wayland-session-envelope@*.target
• starting wayland-session-shutdown.target
Systemd stops all user units in reverse, as it usually does. During deactivation of graphical-
session-pre.target, the environment preloader service cleans activation environments by unset
ting all variables that were marked for removal during startup and finalization stages.
Do not use compositor's native exit mechanism or kill its process directly.
SUBCOMMANDS
select
Selects default wayland session compositor Desktop Entry.
uwsm select
Invokes a whiptail menu to select default session among Desktop Entries in wayland-sessions XDG
data hierarchy. Writes to ${XDG_CONFIG_HOME}/uwsm/default-id. Nothing else is done. Returns 1
if selection is cancelled. Can be used for scripting launch condition in shell profile.
check
Performs tests, returns 0 on success, 1 on failure.
is-active:
uwsm check is-active [-h] [-v] [compositor]
-v show additional info
compositor check for specific compositor
Checks if unit of specific compositor or graphical-session*.target in general is in active or
activating state.
may-start:
uwsm check may-start [-h] [-g [S]] [-v|-q] [N ...]
N ... allowed VT numbers (default: 1)
-g S wait S seconds for graphical.target in queue (default: 60; 0 or less disables
check).
-i do not check for login shell
-r do not check for local session (allow remote session)
-v show all failed tests
-q be quiet
Checks whether it is OK to launch a wayland session via the following conditions:
• DBUS_SESSION_BUS_ADDRESS is set
• Running from login shell
• System is at graphical.target
• User graphical-session*.target units are not yet active
• Foreground VT is among allowed (default: 1)
• Login session's VT is matching
start
Generates units for given compositor command line or Desktop Entry and starts them.
uwsm start [-h] [-D name[:name...]] [-a|-e] [-N Name] [-C Comment] [-U {run|home}] [-t]
[-o] [-n] -- compositor [args ...]
-F Hardcode mode, always write command line to unit drop-ins and use full
paths.
-D name[:name...] Names to fill XDG_CURRENT_DESKTOP with (:-separated). Existing var con
tent is a starting point if no active session is running.
-a Append desktop names set by -D to other sources (default).
-e Use desktop names set by -D exclusively, discard other sources.
-N Name Fancy name for compositor (filled from Desktop Entry by default).
-C Comment Fancy description for compositor (filled from Desktop Entry by de
fault).
-U {run|home} Select rung for generated unit files: run: $XDG_RUNTIME_DIR/sys
temd/user (default), or home: $XDG_CONFIG_HOME/systemd/user. Permanent
destination will save some time by removing need for reloading systemd.
Managed files from other rung will be removed. Can be preset with
UWSM_UNIT_RUNG environment var.
-t Do not generate (and remove) tweak unit files. Can be preset with
UWSM_TWEAKS=false environment var.
-T Generate tweak unit files for other software. This is default behavior.
-g S Wait for S seconds for system graphical.target in queue and warn if
timed out or not in queue (default: 60, negative to disable).
-G S Wait for S seconds for system graphical.target in queue and abort if
timed out or not in queue (overrides -g, default: -1, (disabled)).
-o Only generate units, but do not start.
-n Dry run, do not write or start anything.
The first argument of the compositor command line acts as an ID and should be either one of:
• Executable name
• Desktop Entry ID (optionally with ":"-delimited action ID)
• Special value:
• select - invoke menu to select compositor.
• default - run previously selected compositor (or select if no selection was saved).
If given as path, hardcode mode will be used implicitly.
Always use "--" to disambiguate dashed arguments intended for compositor itself.
After units are (re)generated, wayland-session-bindpid@${PID}.service is started, to track the
PID of invoking uwsm, then uwsm process replaces itself with systemctl execution that starts
wayland-wm@${ID}.service and waits for it to finish.
In order to complete the startup sequence, the compositor has to put WAYLAND_DISPLAY into the
systemd activation environment. This can be done explicitly by making compositor run "uwsm fi
nalize" command (see the next subsection).
finalize
For running by a compositor on startup.
uwsm finalize [-h] [VAR_NAME ...]
Exports WAYLAND_DISPLAY, DISPLAY and any defined vars mentioned by names in arguments or in
UWSM_FINALIZE_VARNAMES variable (whitespace-separated). Then sends startup notification for the
unit to systemd user manager.
This is required if compositor itself does not put WAYLAND_DISPLAY to systemd activation envi
ronment, otherwise wayland-session@.service unit or a dedicated wayland-session-waitenv.service
unit will terminate due to startup timeout.
UWSM_FINALIZE_VARNAMES variable can be prefilled by plugins.
Direct assignment as VAR_NAME=value is also possible, but recommended only for creating flags
for UWSM_WAIT_VARNAMES mechanism.
stop
Stops compositor and optionally removes generated units.
uwsm stop [-h] [-r [compositor] [-U {run|home}] [-n]
-r [compositor] Also remove units (all or only compositor-specific).
-U {run|home} Select rung for generated unit files: run: $XDG_RUNTIME_DIR/systemd/user
(default), or home: $XDG_CONFIG_HOME/systemd/user. Permanent destination
will save some time by removing need for reloading systemd. Managed files
from other rung will be removed. Can be preset with UWSM_UNIT_RUNG envi
ronment var.
-n Dry run, do not stop or remove anything.
app
Application-to-unit launcher with Desktop Entry support.
uwsm app [-h] [-s {a,b,s,custom.slice}] [-t {scope,service}] [-a app_name] [-u unit_name]
[-d unit_description] [-S ] [-T] -- application [args ...]
-s {a,b,s,custom.slice} Slice selector (default: a):
a - app-graphical.slice
b - background-graphical.slice
s - session-graphical.slice
any slice by full name
-t {scope,service} Type of unit to launch (default: scope, can be preset by
UWSM_APP_UNIT_TYPE env var).
-a app_name Override app name (a substring in unit name).
-u unit_name Override the whole autogenerated unit name.
-d unit_description Unit Description.
-p Property=value Set additional unit property (option is repeatable).
-S {out,err,both} Silence stdout, stderr, or both.
-T Launch app in a terminal. Allows command to be empty to just
launch a terminal.
Application can be provided as a command with optional arguments, or a Desktop Entry ID, op
tionally suffixed with ":"-delimited Action ID. If Desktop Entry is being launched, arguments
should be compatible with it.
Always use "--" to disambiguate dashed arguments intended for application itself.
aux
For use in systemd user services. Can only be called by systemd user manager.
prepare-env Prepares environment (for use in ExecStart in wayland-wm-env@.service bound to
wayland-session-pre@.target).
cleanup-env Cleans up environment (for use ExecStop in in wayland-wm-env@.service bound to
wayland-session-pre@.target).
exec Executes a command with arguments or a desktop entry (for use in Exec in wayland-
wm@.service bound to wayland-session@.target).
app-daemon Daemon for faster app argument generation, used by uwsm-app client.
APP DAEMON
Provided as wayland-wm-app-daemon.service to be started on-demand.
Daemon receives app arguments from ${XDG_RUNTIME_DIR}/uwsm-app-daemon-in pipe. Resulting argu
ments are formatted as shell code and written to ${XDG_RUNTIME_DIR}/uwsm-app-daemon-out pipe.
Arguments are expected to be \0-delimited, leading \0 are stripped. One command is received per
write+close.
The first argument determines the behavior:
• app the rest is processed the same as in "uwsm app"
• ping just "pong" is returnedn
• stop daemon is stoppedn
Single commands are prepended with exec, iterated commands are assembled with trailing & each,
followed by wait.
The purpose of all this is to skip all the expensive Python startup and import routines that
slow things down every time "uwsm app" is called. Instead the daemon does it once and then lis
tens for requests, while a simple shell script may dump arguments to one pipe and run the code
received from another via eval, which is much faster.
The simplest script is:
#!/bin/sh
printf '0%s' app "$@" > "${XDG_RUNTIME_DIR}/uwsm-app-daemon-in"
IFS='' read -r cmd < "${XDG_RUNTIME_DIR}/uwsm-app-daemon-out"
eval "$cmd"
Provided uwsm-app client script is a bit smarter: it can start the daemon, applies timeouts,
and supports newlines in returned args.
SHELL PROFILE INTEGRATION
To launch uwsm automatically on login, add one of constructs below (or similar) to shell pro
file.
This asks to select a compositor (or refuse and continue with login shell) when logged in on VT
1:
if uwsm check may-start && uwsm select; then
exec systemd-cat -t uwsm_start uwsm start default
fi
This just starts a specific compositor depending on foreground VT:
if uwsm check may-start 1; then
exec systemd-cat -t uwsm_start uwsm start sway.desktop
elif uwsm check may-start 2; then
exec systemd-cat -t uwsm_start uwsm start labwc.desktop
fi
Using "uwsm check may-start" as a condition is essential, not only to prevent accidental
startup attempts where they are not expected, but also since startup may involve sourcing shell
profile, which might lead to nasty loops.
See check subcommand section for info on may-start checker.
exec allows uwsm to replace login shell in order to properly bind to user session and handle
session termination.
"systemd-cat -t uwsm_start" (optional) executes the command given to it (uwsm) with its stdout
and stderr connected to the systemd journal, tagged with identifier "uwsm_start". See systemd-
cat(1) for more options.
USE INSIDE DESKTOP ENTRY
To launch uwsm from a display/login manager, "uwsm start" can be used inside Desktop Entries.
Example /usr/local/share/wayland-sessions/my-compositor.desktop:
[Desktop Entry]
Name=My compositor (with UWSM)
Comment=My cool compositor
Exec=uwsm start -N "My compositor" -D mycompositor -C "My cool compositor" mywm
DesktopNames=mycompositor
Type=Application
Things to keep in mind:
• For consistency, command line arguments should mirror the keys of the entry
• Command in Exec= should start with "uwsm start"
• It should not point to itself (as a combination of Desktop Entry ID and Action ID)
• It should not point to a Desktop Entry ID and Action ID that also uses uwsm
Potentially such entries may be found and used by uwsm itself, i.e. in shell profile integra
tion situation, or when launched manually. Following the principles above ensures uwsm will
properly recognize itself and parse requested arguments inside the entry without any side ef
fects.
SEE ALSO
uwsm-plugins(3), systemd-run(1), systemd-cat(1), systemd.special(7)
2026-02-14 UWSM(1)