@aliou/pi-processes 0.10.3 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { compileLineMatcher } from "../../../src/utils/match-line";
|
|
2
|
+
import { plainTextForDisplay } from "../../shared/display-text";
|
|
2
3
|
import type { NotifyConfig } from "./registry";
|
|
3
4
|
import type { Attention } from "./types";
|
|
4
5
|
|
|
@@ -130,8 +131,12 @@ function splitAppendedIntoLines(
|
|
|
130
131
|
for (const entry of appended) {
|
|
131
132
|
const split = entry.text.split("\n");
|
|
132
133
|
for (const line of split) {
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
// Match the same per-line visible text the log views render. This keeps
|
|
135
|
+
// CR-overwritten progress and invisible escape bytes from firing
|
|
136
|
+
// watches, without making watches depend on viewport width.
|
|
137
|
+
const displayLine = plainTextForDisplay(line);
|
|
138
|
+
if (displayLine.length === 0) continue;
|
|
139
|
+
lines.push({ type: entry.type, text: displayLine });
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
142
|
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
plainTextForDisplay,
|
|
5
|
+
sanitizeForDisplay,
|
|
6
|
+
} from "../../shared/display-text";
|
|
4
7
|
import { trimToBudget } from "../../shared/line-buffer";
|
|
5
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
displayTextOf,
|
|
10
|
+
type LogLineEmphasis,
|
|
11
|
+
renderLogLine,
|
|
12
|
+
} from "../../shared/log-line";
|
|
6
13
|
import { truncateToWidth } from "../../shared/truncate";
|
|
7
14
|
import type { ProcessLogLine } from "../logs-client";
|
|
8
15
|
|
|
@@ -122,8 +129,9 @@ export class LogFileViewer {
|
|
|
122
129
|
* priority (search current match > search match > notify match > stream).
|
|
123
130
|
*/
|
|
124
131
|
addNotifyMatch(match: { line: string }): void {
|
|
125
|
-
// Stored
|
|
126
|
-
|
|
132
|
+
// Stored as plain visible text so invisible escape bytes cannot be what
|
|
133
|
+
// makes a notification marker match.
|
|
134
|
+
const line = plainTextForDisplay(match.line);
|
|
127
135
|
if (line) this.notifyLines.add(line);
|
|
128
136
|
}
|
|
129
137
|
|
|
@@ -192,7 +200,8 @@ export class LogFileViewer {
|
|
|
192
200
|
? "search-current"
|
|
193
201
|
: matchSet.has(visibleIndex)
|
|
194
202
|
? "search"
|
|
195
|
-
: this.notifyLines.has(line.text)
|
|
203
|
+
: this.notifyLines.has(line.text) ||
|
|
204
|
+
this.notifyLines.has(displayTextOf(line))
|
|
196
205
|
? "notify"
|
|
197
206
|
: "none";
|
|
198
207
|
return renderLogLine(line, { theme: this.theme, width, emphasis });
|
|
@@ -13,10 +13,17 @@ const ESC = String.fromCodePoint(0x001b);
|
|
|
13
13
|
const BEL = String.fromCodePoint(0x0007);
|
|
14
14
|
const ST = String.fromCodePoint(0x009c);
|
|
15
15
|
const RESET = `${ESC}[0m`;
|
|
16
|
+
const C1_DCS = String.fromCodePoint(0x0090);
|
|
17
|
+
const C1_SOS = String.fromCodePoint(0x0098);
|
|
18
|
+
const C1_OSC = String.fromCodePoint(0x009d);
|
|
19
|
+
const C1_PM = String.fromCodePoint(0x009e);
|
|
20
|
+
const C1_APC = String.fromCodePoint(0x009f);
|
|
21
|
+
const C1_STRING_INTRODUCERS = [C1_DCS, C1_SOS, C1_OSC, C1_PM, C1_APC];
|
|
16
22
|
|
|
17
|
-
// Control characters a single display row must never contain. Tabs
|
|
18
|
-
// separately; newlines are dropped because they
|
|
19
|
-
// C1 controls are included: a raw \u009b is an
|
|
23
|
+
// Control characters a single display row must never contain. Tabs and
|
|
24
|
+
// carriage returns are handled separately; newlines are dropped because they
|
|
25
|
+
// would shift the whole frame. C1 controls are included: a raw \u009b is an
|
|
26
|
+
// alias for CSI on some terminals.
|
|
20
27
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: this regex intentionally targets terminal control characters.
|
|
21
28
|
const DISPLAY_CONTROL_CHARS = /[\u0000-\u0008\u000a-\u001f\u007f-\u009f]/gu;
|
|
22
29
|
|
|
@@ -41,7 +48,9 @@ const TAB_WIDTH = 8;
|
|
|
41
48
|
* into the rest of the frame.
|
|
42
49
|
*/
|
|
43
50
|
export function sanitizeForDisplay(text: string): string {
|
|
44
|
-
if (!text.includes(ESC)
|
|
51
|
+
if (!text.includes(ESC) && !hasC1StringIntroducer(text)) {
|
|
52
|
+
return cleanPlainText(text, 0).text;
|
|
53
|
+
}
|
|
45
54
|
|
|
46
55
|
let out = "";
|
|
47
56
|
let cursor = 0;
|
|
@@ -49,21 +58,34 @@ export function sanitizeForDisplay(text: string): string {
|
|
|
49
58
|
let keptSgr = false;
|
|
50
59
|
|
|
51
60
|
while (cursor < text.length) {
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
|
|
61
|
+
const sequenceAt = findNextSequenceStart(text, cursor);
|
|
62
|
+
if (sequenceAt === -1) {
|
|
63
|
+
const chunk = cleanPlainText(text.slice(cursor), column);
|
|
64
|
+
if (chunk.resetsRow) {
|
|
65
|
+
out = "";
|
|
66
|
+
keptSgr = false;
|
|
67
|
+
}
|
|
68
|
+
out += chunk.text;
|
|
55
69
|
break;
|
|
56
70
|
}
|
|
57
|
-
const chunk = cleanPlainText(text.slice(cursor,
|
|
71
|
+
const chunk = cleanPlainText(text.slice(cursor, sequenceAt), column);
|
|
72
|
+
if (chunk.resetsRow) {
|
|
73
|
+
out = "";
|
|
74
|
+
keptSgr = false;
|
|
75
|
+
}
|
|
58
76
|
out += chunk.text;
|
|
59
77
|
column = chunk.column;
|
|
60
78
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
79
|
+
if (text[sequenceAt] === ESC) {
|
|
80
|
+
const sequence = readEscapeSequence(text, sequenceAt);
|
|
81
|
+
if (sequence.isSgr) {
|
|
82
|
+
out += text.slice(sequenceAt, sequence.end);
|
|
83
|
+
keptSgr = true;
|
|
84
|
+
}
|
|
85
|
+
cursor = sequence.end;
|
|
86
|
+
} else {
|
|
87
|
+
cursor = readC1StringSequence(text, sequenceAt).end;
|
|
65
88
|
}
|
|
66
|
-
cursor = sequence.end;
|
|
67
89
|
}
|
|
68
90
|
|
|
69
91
|
if (!keptSgr || out.endsWith(RESET)) return out;
|
|
@@ -79,6 +101,22 @@ export function truncateForDisplay(text: string, width: number): string {
|
|
|
79
101
|
return closeSgr(truncateToWidth(sanitizeForDisplay(text), width, "…"));
|
|
80
102
|
}
|
|
81
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Return the unstyled text a user can actually read after terminal controls
|
|
106
|
+
* have been interpreted/dropped. Use this for comparisons such as search,
|
|
107
|
+
* notify markers, and log watches so invisible escape bytes cannot match.
|
|
108
|
+
*/
|
|
109
|
+
export function plainTextForDisplay(text: string): string {
|
|
110
|
+
return stripSgr(sanitizeForDisplay(text));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const SGR = new RegExp(`${ESC}\\[[0-9;:]*m`, "gu");
|
|
114
|
+
|
|
115
|
+
/** Drop the SGR sequences `sanitizeForDisplay` kept for rendering colors. */
|
|
116
|
+
export function stripSgr(text: string): string {
|
|
117
|
+
return text.replace(SGR, "");
|
|
118
|
+
}
|
|
119
|
+
|
|
82
120
|
/**
|
|
83
121
|
* Re-close colors after truncation. `sanitizeForDisplay` ends a colored string
|
|
84
122
|
* with a reset, but truncating can cut that reset off and let the color bleed
|
|
@@ -97,21 +135,42 @@ export function closeSgr(text: string): string {
|
|
|
97
135
|
function cleanPlainText(
|
|
98
136
|
text: string,
|
|
99
137
|
column: number,
|
|
100
|
-
): { text: string; column: number } {
|
|
101
|
-
const
|
|
138
|
+
): { text: string; column: number; resetsRow: boolean } {
|
|
139
|
+
const carriageReturn = text.lastIndexOf("\r");
|
|
140
|
+
const resetsRow = carriageReturn !== -1;
|
|
141
|
+
const visibleText = resetsRow ? text.slice(carriageReturn + 1) : text;
|
|
142
|
+
const startColumn = resetsRow ? 0 : column;
|
|
143
|
+
const clean = visibleText.replace(DISPLAY_CONTROL_CHARS, "");
|
|
102
144
|
if (!clean.includes("\t")) {
|
|
103
|
-
return {
|
|
145
|
+
return {
|
|
146
|
+
text: clean,
|
|
147
|
+
column: startColumn + visibleWidth(clean),
|
|
148
|
+
resetsRow,
|
|
149
|
+
};
|
|
104
150
|
}
|
|
105
151
|
|
|
106
152
|
const parts = clean.split("\t");
|
|
107
153
|
let out = parts[0] ?? "";
|
|
108
|
-
let col =
|
|
154
|
+
let col = startColumn + visibleWidth(out);
|
|
109
155
|
for (const part of parts.slice(1)) {
|
|
110
156
|
const spaces = TAB_WIDTH - (col % TAB_WIDTH);
|
|
111
157
|
out += " ".repeat(spaces) + part;
|
|
112
158
|
col += spaces + visibleWidth(part);
|
|
113
159
|
}
|
|
114
|
-
return { text: out, column: col };
|
|
160
|
+
return { text: out, column: col, resetsRow };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function findNextSequenceStart(text: string, cursor: number): number {
|
|
164
|
+
let next = text.indexOf(ESC, cursor);
|
|
165
|
+
for (const introducer of C1_STRING_INTRODUCERS) {
|
|
166
|
+
const index = text.indexOf(introducer, cursor);
|
|
167
|
+
if (index !== -1 && (next === -1 || index < next)) next = index;
|
|
168
|
+
}
|
|
169
|
+
return next;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function hasC1StringIntroducer(text: string): boolean {
|
|
173
|
+
return C1_STRING_INTRODUCERS.some((introducer) => text.includes(introducer));
|
|
115
174
|
}
|
|
116
175
|
|
|
117
176
|
/**
|
|
@@ -151,6 +210,12 @@ function readEscapeSequence(
|
|
|
151
210
|
return { end: start + 2, isSgr: false };
|
|
152
211
|
}
|
|
153
212
|
|
|
213
|
+
function readC1StringSequence(text: string, start: number): { end: number } {
|
|
214
|
+
const introducer = text[start];
|
|
215
|
+
const allowBel = introducer === C1_OSC || introducer === C1_APC;
|
|
216
|
+
return { end: findStringTerminator(text, start + 1, allowBel) };
|
|
217
|
+
}
|
|
218
|
+
|
|
154
219
|
/** Index just past the terminator of a string sequence, else end of input. */
|
|
155
220
|
function findStringTerminator(
|
|
156
221
|
text: string,
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
closeSgr,
|
|
15
|
+
plainTextForDisplay,
|
|
16
|
+
sanitizeForDisplay,
|
|
17
|
+
stripSgr,
|
|
18
|
+
} from "./display-text";
|
|
14
19
|
import { truncateToWidth } from "./truncate";
|
|
15
20
|
|
|
16
21
|
export interface DisplayLogLine {
|
|
@@ -60,7 +65,7 @@ export function renderLogLine(
|
|
|
60
65
|
|
|
61
66
|
/** Text of a log line as the views display it, for match comparisons. */
|
|
62
67
|
export function displayTextOf(line: DisplayLogLine): string {
|
|
63
|
-
return
|
|
68
|
+
return plainTextForDisplay(line.text);
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
function toneLogText(
|
|
@@ -75,10 +80,3 @@ function toneLogText(
|
|
|
75
80
|
if (type === "stderr") return theme.fg("warning", text);
|
|
76
81
|
return text;
|
|
77
82
|
}
|
|
78
|
-
|
|
79
|
-
const SGR = new RegExp(`${String.fromCodePoint(0x001b)}\\[[0-9;:]*m`, "gu");
|
|
80
|
-
|
|
81
|
-
/** Drop the SGR sequences `sanitizeForDisplay` kept. */
|
|
82
|
-
function stripSgr(text: string): string {
|
|
83
|
-
return text.replace(SGR, "");
|
|
84
|
-
}
|