diff --git a/CHANGELOG.md b/CHANGELOG.md index 181e1bc5..bd979d14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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 diff --git a/Cargo.lock b/Cargo.lock index 6684e40b..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" @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 4a2025cb..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" @@ -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"] 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/assets/syntaxes/02_Extra/Manpage.sublime-syntax b/assets/syntaxes/02_Extra/Manpage.sublime-syntax index 0779ce04..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: '\b([A-Za-z0-9_\-]+\.)?([A-Za-z0-9_\-]+)(\()([^)]*)(\))' + - match: '(? + 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 @@ -61,8 +68,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..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 @@ -26,7 +28,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/assets.rs b/src/assets.rs index 82c160c9..29247bd7 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> { @@ -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>> { + 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 +323,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| { @@ -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" + ); + } } diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 521674f2..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 @@ -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::("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::("color").map(|s| s.as_str()) { diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index d286904a..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") @@ -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."), ) 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)); +} 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/output.rs b/src/output.rs index e5c8a654..0205f48e 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 + ); 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(_) => (), diff --git a/src/printer.rs b/src/printer.rs index 3c3facf5..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 @@ -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 = 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); 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 da8b21eb..cfbad253 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() { @@ -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"); +} diff --git a/tests/syntax-tests/highlighted/Manpage/uwsm-0.26.3.man b/tests/syntax-tests/highlighted/Manpage/uwsm-0.26.3.man new file mode 100644 index 00000000..319020e8 --- /dev/null +++ b/tests/syntax-tests/highlighted/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) 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)