Skip to content

Commit fa39c97

Browse files
xXJSONDeruloXxaliou
authored andcommitted
fix: sanitize process log control chars
1 parent d4e70e6 commit fa39c97

3 files changed

Lines changed: 57 additions & 16 deletions

File tree

src/components/log-dock-component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { Component } from "@mariozechner/pi-tui";
1515
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
1616
import { LIVE_STATUSES } from "../constants";
1717
import type { ProcessManager } from "../manager";
18+
import { stripAnsi } from "../utils";
1819
import { LogFileViewer } from "./log-file-viewer";
1920

2021
const PROCESS_COLORS: ThemeColor[] = [
@@ -153,7 +154,7 @@ export class LogDockComponent implements Component {
153154
const lastLogs = this.manager.getCombinedOutput(running[0].id, 1);
154155
if (lastLogs && lastLogs.length > 0) {
155156
const lastLog = truncateToWidth(
156-
lastLogs[lastLogs.length - 1].text,
157+
stripAnsi(lastLogs[lastLogs.length - 1].text),
157158
innerWidth,
158159
);
159160
lines.push(padLine(dim(lastLog)));

src/utils/ansi.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, it } from "vitest";
2+
import { stripAnsi } from "./ansi";
3+
4+
describe("stripAnsi", () => {
5+
it("strips CSI styling sequences", () => {
6+
expect(stripAnsi("\u001b[31mred\u001b[0m")).toBe("red");
7+
});
8+
9+
it("strips generic OSC sequences", () => {
10+
expect(stripAnsi("\u001b]0;title\u0007hello")).toBe("hello");
11+
});
12+
13+
it("strips carriage returns and other control chars that can corrupt TUI rendering", () => {
14+
const output = stripAnsi("step 1\rstep 2\b\b done");
15+
expect(output).toBe("step 1step 2 done");
16+
for (const char of output) {
17+
const code = char.codePointAt(0) ?? 0;
18+
const isDisallowedC0 =
19+
(code >= 0x00 && code <= 0x08) ||
20+
(code >= 0x0b && code <= 0x1f) ||
21+
code === 0x7f;
22+
expect(isDisallowedC0).toBe(false);
23+
}
24+
});
25+
});

src/utils/ansi.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/**
2-
* Strip ANSI escape codes from a string.
2+
* Strip ANSI escape codes and other terminal control characters from a string.
33
*
44
* Removes:
55
* - All CSI sequences (\x1b[...X) - SGR, cursor movement, erase, scroll, etc.
6-
* - OSC 8 hyperlinks (\x1b]8;;URL\x07)
6+
* - OSC sequences (\x1b]...\x07 or \x1b]...\x1b\\)
77
* - APC sequences (\x1b_...\x07 or \x1b_...\x1b\\)
8+
* - Remaining C0 control chars except tab/newline
89
*/
910
/**
1011
* Check if a string contains ANSI escape codes.
@@ -18,19 +19,33 @@ export function stripAnsi(str: string): string {
1819
const ESC = String.fromCodePoint(0x001b);
1920
const BEL = String.fromCodePoint(0x0007);
2021

21-
if (!str.includes(ESC)) {
22-
return str;
23-
}
22+
let clean = str;
2423

25-
// Strip all CSI sequences (ESC[...X where X is any letter)
26-
let clean = str.replace(new RegExp(`${ESC}\\[[0-9;]*[A-Za-z]`, "gu"), "");
27-
// Strip OSC 8 hyperlinks: ESC]8;;URL<BEL> and ESC]8;;<BEL>
28-
clean = clean.replace(new RegExp(`${ESC}\\]8;;[^${BEL}]*${BEL}`, "gu"), "");
29-
// Strip APC sequences: ESC_...<BEL> or ESC_...<ESC>\\ (used for cursor marker)
30-
clean = clean.replace(
31-
new RegExp(`${ESC}_[^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)`, "gu"),
32-
"",
33-
);
24+
if (str.includes(ESC)) {
25+
// Strip all CSI sequences (ESC[...X where X is any letter)
26+
clean = clean.replace(new RegExp(`${ESC}\\[[0-9;]*[A-Za-z]`, "gu"), "");
27+
// Strip OSC sequences: ESC]...<BEL> or ESC]...<ESC>\\
28+
clean = clean.replace(
29+
new RegExp(`${ESC}\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)`, "gu"),
30+
"",
31+
);
32+
// Strip APC sequences: ESC_...<BEL> or ESC_...<ESC>\\ (used for cursor marker)
33+
clean = clean.replace(
34+
new RegExp(`${ESC}_[^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)`, "gu"),
35+
"",
36+
);
37+
}
3438

35-
return clean;
39+
// Strip terminal control chars like carriage return/backspace that can
40+
// corrupt TUI layout when rendered back into pi.
41+
return Array.from(clean)
42+
.filter((char) => {
43+
const code = char.codePointAt(0) ?? 0;
44+
const isDisallowedC0 =
45+
(code >= 0x00 && code <= 0x08) ||
46+
(code >= 0x0b && code <= 0x1f) ||
47+
code === 0x7f;
48+
return !isDisallowedC0;
49+
})
50+
.join("");
3651
}

0 commit comments

Comments
 (0)