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

Add word wrapping mode (#3597)

* feat: add word wrapping mode for --wrap flag

* Run `cargo fmt` and add CHANGELOG entry

* Add word wrap tests, update manpage and shell completions

- Add integration tests for word wrapping: basic word boundary breaking,
  fallback to character wrapping for long words, line numbers, and
  short lines that fit without wrapping
- Update manpage to document the new 'word' wrapping mode
- Update bash, fish, zsh, and PowerShell completions with 'word' option
- Avoid unnecessary clone of `line_buf` when word wrap is disabled

* make clippy and cargo fmt happy

---------

Co-authored-by: Keith Hall <keith-hall@users.noreply.github.com>
This commit is contained in:
Varun Chawla
2026-03-02 19:18:49 -08:00
committed by GitHub
parent 1424f9d6bf
commit cc5f782d28
14 changed files with 130 additions and 15 deletions
+1
View File
@@ -4,6 +4,7 @@
## Features ## Features
- Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey)
- Implement `--unbuffered` mode for streaming input, allowing partial lines to display immediately (e.g. `tail -f | bat -u`). Closes #3555, see #3583 (@mainnebula) - 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) - 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) - Add `--quiet-empty` (`-E`) flag to suppress output when input is empty. Closes #1936, see #3563 (@NORMAL-EX)
+2 -2
View File
@@ -9,7 +9,7 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
$ArrayCompletion = @('bash', 'fish', 'zsh', 'ps1') $ArrayCompletion = @('bash', 'fish', 'zsh', 'ps1')
$ArrayWhen = @('auto', 'never', 'always') $ArrayWhen = @('auto', 'never', 'always')
$ArrayYesNo = @('never', 'always') $ArrayYesNo = @('never', 'always')
$ArrayWrap = @('always', 'never', 'character') $ArrayWrap = @('always', 'never', 'character', 'word')
$ArrayBinary = @('no-printing', 'as-text') $ArrayBinary = @('no-printing', 'as-text')
$ArrayPrint = @('unicode', 'caret') $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('--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('--diff-context' , 'diff-context' , [CompletionResultType]::ParameterName, 'diff-context')
[CompletionResult]::new('--tabs' , 'tabs' , [CompletionResultType]::ParameterName, 'Set the tab width to T spaces.') [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('--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('--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*)') [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 return 0
;; ;;
--wrap) --wrap)
COMPREPLY=($(compgen -W "auto never character" -- "$cur")) COMPREPLY=($(compgen -W "auto never character word" -- "$cur"))
return 0 return 0
;; ;;
--binary) --binary)
+1
View File
@@ -118,6 +118,7 @@ set -l wrap_opts '
auto\tdefault auto\tdefault
never\t never\t
character\t character\t
word\t
' '
# While --tabs theoretically takes any number, most people should be OK with these. # 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]' '(-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' --diff-context='[specify lines of context around added/removed/modified lines when using `--diff`]:lines'
--tabs='[set the tab width]:tab width [4]' --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} '!(--wrap)'{-S,--chop-long-lines}
--terminal-width='[explicitly set the width of the terminal instead of determining it automatically]:width' --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]' '(-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 .HP
\fB\-\-wrap\fR <mode> \fB\-\-wrap\fR <mode>
.IP .IP
Specify the text\-wrapping mode (*auto*, never, character). The '\-\-terminal\-width' option Specify the text\-wrapping mode (*auto*, never, character, word). The '\-\-terminal\-width' option
can be used in addition to control the output width. 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 .HP
\fB\-S\fR, \fB\-\-chop\-long\-lines\fR \fB\-S\fR, \fB\-\-chop\-long\-lines\fR
.IP .IP
+2 -2
View File
@@ -61,8 +61,8 @@ Options:
Set the tab width to T spaces. Use a width of 0 to pass tabs through directly Set the tab width to T spaces. Use a width of 0 to pass tabs through directly
--wrap <mode> --wrap <mode>
Specify the text-wrapping mode (*auto*, never, character). The '--terminal-width' option Specify the text-wrapping mode (*auto*, never, character, word). The '--terminal-width'
can be used in addition to control the output width. option can be used in addition to control the output width.
-S, --chop-long-lines -S, --chop-long-lines
Truncate all lines longer than screen width. Alias for '--wrap=never'. Truncate all lines longer than screen width. Alias for '--wrap=never'.
+1 -1
View File
@@ -26,7 +26,7 @@ Options:
--tabs <T> --tabs <T>
Set the tab width to T spaces. Set the tab width to T spaces.
--wrap <mode> --wrap <mode>
Specify the text-wrapping mode (*auto*, never, character). Specify the text-wrapping mode (*auto*, never, character, word).
-S, --chop-long-lines -S, --chop-long-lines
Truncate all lines longer than screen width. Alias for '--wrap=never'. Truncate all lines longer than screen width. Alias for '--wrap=never'.
-n, --number -n, --number
+1
View File
@@ -405,6 +405,7 @@ impl App {
} else { } else {
match self.matches.get_one::<String>("wrap").map(|s| s.as_str()) { match self.matches.get_one::<String>("wrap").map(|s| s.as_str()) {
Some("character") => WrappingMode::Character, Some("character") => WrappingMode::Character,
Some("word") => WrappingMode::Word,
Some("never") => WrappingMode::NoWrapping(true), Some("never") => WrappingMode::NoWrapping(true),
Some("auto") | None => { Some("auto") | None => {
if self.interactive_output || maybe_term_width.is_some() { if self.interactive_output || maybe_term_width.is_some() {
+3 -3
View File
@@ -211,11 +211,11 @@ pub fn build_app(interactive_output: bool) -> Command {
.long("wrap") .long("wrap")
.overrides_with("wrap") .overrides_with("wrap")
.value_name("mode") .value_name("mode")
.value_parser(["auto", "never", "character"]) .value_parser(["auto", "never", "character", "word"])
.default_value("auto") .default_value("auto")
.hide_default_value(true) .hide_default_value(true)
.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). \ .long_help("Specify the text-wrapping mode (*auto*, never, character, word). \
The '--terminal-width' option can be used in addition to \ The '--terminal-width' option can be used in addition to \
control the output width."), control the output width."),
) )
+50 -3
View File
@@ -781,11 +781,21 @@ impl Printer for InteractivePrinter<'_> {
// Displayed width of line_buf // Displayed width of line_buf
let mut current_width = 0; 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() { for c in text.chars() {
// calculate the displayed width for next character // calculate the displayed width for next character
let cw = c.width().unwrap_or(0); let cw = c.width().unwrap_or(0);
current_width += cw; 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, // if next character cannot be printed on this line,
// flush the buffer. // flush the buffer.
if current_width > max_width { if current_width > max_width {
@@ -807,13 +817,37 @@ impl Printer for InteractivePrinter<'_> {
} }
} }
// Determine the break point and remainder
// for word wrapping.
let (emit_end, rest_start) = if word_wrap {
if let Some(ws_idx) = last_ws_idx {
// Skip the whitespace character itself
// and carry the rest to the next line.
let rs = ws_idx
+ line_buf[ws_idx..]
.chars()
.next()
.map(|ch| ch.len_utf8())
.unwrap_or(0);
(ws_idx, Some(rs))
} else {
(line_buf.len(), None)
}
} else {
(line_buf.len(), None)
};
// It wraps. // It wraps.
write!( write!(
handle, handle,
"{}{}\n{}", "{}{}\n{}",
as_terminal_escaped( as_terminal_escaped(
style, style,
&format!("{}{line_buf}", self.ansi_style), &format!(
"{}{}",
self.ansi_style,
&line_buf[..emit_end]
),
self.config.true_color, self.config.true_color,
self.config.colored_output, self.config.colored_output,
self.config.use_italic_text, self.config.use_italic_text,
@@ -826,8 +860,21 @@ impl Printer for InteractivePrinter<'_> {
cursor = 0; cursor = 0;
max_width = cursor_max; max_width = cursor_max;
line_buf.clear(); if let Some(rs) = rest_start {
current_width = cw; // 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); line_buf.push(c);
+1
View File
@@ -1,6 +1,7 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WrappingMode { pub enum WrappingMode {
Character, Character,
Word,
// The bool specifies whether wrapping has been explicitly disabled by the user via --wrap=never // The bool specifies whether wrapping has been explicitly disabled by the user via --wrap=never
NoWrapping(bool), 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
+59
View File
@@ -3791,3 +3791,62 @@ fn unbuffered_mode_plain_output() {
.success() .success()
.stdout("hello world\n"); .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");
}