Skip to content

Commit e5eae1d

Browse files
committed
fix(man): suppress groff formatting via grotty flags and GROFF_NO_SGR
Use GROFF_NO_SGR=1 to prevent SGR (ANSI escape) output, and -P -c/-b/-u to suppress backspace overstrikes for bold/underline. Keep strip_formatting as a safety net for any remaining escape sequences. https://claude.ai/code/session_01CrXuWDMVQsiUBoy6ceACsF
1 parent 6980273 commit e5eae1d

1 file changed

Lines changed: 21 additions & 7 deletions

File tree

cli/man_page.rs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ fn man_path(page_num: u8) -> String {
6262
fn render_man_output(page_num: u8) -> Result<String, String> {
6363
let roff_file = roff_path(page_num);
6464
let output = Command::new("groff")
65-
.args(["-man", "-T", "utf8", "-K", "utf8"])
65+
.args([
66+
"-man", "-T", "utf8", "-K", "utf8", "-P", "-c", "-P", "-b", "-P", "-u",
67+
])
68+
.env("GROFF_NO_SGR", "1")
6669
.arg(format!("-rLL={LINE_LENGTH}n"))
6770
.arg(format!("./{roff_file}"))
6871
.output()
@@ -73,19 +76,30 @@ fn render_man_output(page_num: u8) -> Result<String, String> {
7376
}
7477
let content = String::from_utf8(output.stdout)
7578
.map_err(|error| format!("groff output is not UTF-8: {error}"))?;
76-
Ok(normalize_text(&strip_overstrikes(&content)))
79+
Ok(normalize_text(&strip_formatting(&content)))
7780
}
7881

79-
/// Strips backspace-based overstriking sequences produced by grotty.
82+
/// Strips terminal formatting from grotty output.
8083
///
81-
/// Grotty uses `X\x08X` for bold and `_\x08X` for underline. This function
82-
/// removes the overstrike prefix (char + `\x08`) leaving only the visible character.
83-
fn strip_overstrikes(text: &str) -> String {
84+
/// Handles two styles grotty may use:
85+
/// - **SGR mode** (default): ANSI escape sequences like `\x1b[1m` (bold), `\x1b[0m` (reset).
86+
/// - **Legacy mode** (`-c`): Backspace overstrikes like `X\x08X` (bold), `_\x08X` (underline).
87+
fn strip_formatting(text: &str) -> String {
8488
let chars: Vec<char> = text.chars().collect();
8589
let mut result = String::with_capacity(text.len());
8690
let mut index = 0;
8791
while index < chars.len() {
88-
if index + 1 < chars.len() && chars[index + 1] == '\x08' {
92+
if chars[index] == '\x1b' && index + 1 < chars.len() && chars[index + 1] == '[' {
93+
// Skip ANSI escape: ESC [ ... m
94+
index += 2;
95+
while index < chars.len() && chars[index] != 'm' {
96+
index += 1;
97+
}
98+
if index < chars.len() {
99+
index += 1; // skip the 'm'
100+
}
101+
} else if index + 1 < chars.len() && chars[index + 1] == '\x08' {
102+
// Skip backspace overstrike: char + BS
89103
index += 2;
90104
} else {
91105
result.push(chars[index]);

0 commit comments

Comments
 (0)