@xzzpig/pi-starline 0.1.0
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.
- package/LICENSE +22 -0
- package/README.md +223 -0
- package/extensions/starline/config.ts +1317 -0
- package/extensions/starline/editor-cursor.ts +55 -0
- package/extensions/starline/editor-factory-marker.ts +49 -0
- package/extensions/starline/editor-metadata-format.ts +259 -0
- package/extensions/starline/extension-status.ts +88 -0
- package/extensions/starline/footer-format.ts +279 -0
- package/extensions/starline/footer.ts +744 -0
- package/extensions/starline/format.ts +473 -0
- package/extensions/starline/git.ts +467 -0
- package/extensions/starline/icons.ts +294 -0
- package/extensions/starline/index.ts +651 -0
- package/extensions/starline/live-context.ts +75 -0
- package/extensions/starline/mouse/capabilities.ts +207 -0
- package/extensions/starline/mouse/component-tree.ts +230 -0
- package/extensions/starline/mouse/editor-caret.ts +498 -0
- package/extensions/starline/mouse/editor-hit-test.ts +133 -0
- package/extensions/starline/mouse/editor-mouse.ts +142 -0
- package/extensions/starline/mouse/editor-scroll.ts +119 -0
- package/extensions/starline/mouse/editor-text-cursor.ts +209 -0
- package/extensions/starline/mouse/editor-text-edit.ts +90 -0
- package/extensions/starline/mouse/hint.ts +14 -0
- package/extensions/starline/mouse/hit-test.ts +144 -0
- package/extensions/starline/mouse/index.ts +1119 -0
- package/extensions/starline/mouse/selection-state.ts +68 -0
- package/extensions/starline/mouse/tool-box.ts +290 -0
- package/extensions/starline/mouse/transcript-copy.ts +166 -0
- package/extensions/starline/mouse/word-select.ts +145 -0
- package/extensions/starline/package-version.ts +650 -0
- package/extensions/starline/paste-collapse.ts +312 -0
- package/extensions/starline/pill-config.ts +122 -0
- package/extensions/starline/pill.ts +295 -0
- package/extensions/starline/project-refresh.ts +104 -0
- package/extensions/starline/project-state.ts +66 -0
- package/extensions/starline/prototype-patch-registry.ts +136 -0
- package/extensions/starline/runtime.ts +841 -0
- package/extensions/starline/selector-border.ts +77 -0
- package/extensions/starline/session-lifecycle.ts +60 -0
- package/extensions/starline/settings-command.ts +925 -0
- package/extensions/starline/state.ts +65 -0
- package/extensions/starline/style.ts +528 -0
- package/extensions/starline/ui.ts +611 -0
- package/extensions/starline/user-message.ts +267 -0
- package/package.json +60 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor cursor styling.
|
|
3
|
+
*
|
|
4
|
+
* Pi's editor draws its cursor in software, as a reverse-video run around the
|
|
5
|
+
* grapheme under the cursor (`\x1b[7m` … `\x1b[0m`), emitted right after the
|
|
6
|
+
* zero-width CURSOR_MARKER that tells the TUI where to put the real terminal
|
|
7
|
+
* cursor. Restyling is therefore a string rewrite over the rendered lines —
|
|
8
|
+
* no editor internals involved.
|
|
9
|
+
*
|
|
10
|
+
* - `block` leave it alone (upstream behaviour)
|
|
11
|
+
* - `underline` draw an underline instead of the reverse block
|
|
12
|
+
* - `terminal` remove it, letting the real terminal cursor show through, so
|
|
13
|
+
* its shape and blink follow the terminal's own configuration
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type EditorCursorStyle = "block" | "underline" | "terminal";
|
|
17
|
+
|
|
18
|
+
export function parseEditorCursorStyle(value: unknown): EditorCursorStyle {
|
|
19
|
+
return value === "underline" || value === "terminal" || value === "block" ? value : "block";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Reverse-video run containing no further escapes. The single-grapheme check
|
|
24
|
+
* happens in the replacer: a longer run is deliberate styling somewhere else in
|
|
25
|
+
* the line (a selection highlight, say) and must not be rewritten.
|
|
26
|
+
*/
|
|
27
|
+
const REVERSE_RUN = /\x1b\[7m([^\x1b]*)\x1b\[0m/g;
|
|
28
|
+
|
|
29
|
+
const graphemeSegmenter =
|
|
30
|
+
typeof Intl !== "undefined" && typeof Intl.Segmenter === "function"
|
|
31
|
+
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
32
|
+
: undefined;
|
|
33
|
+
|
|
34
|
+
function isSingleGrapheme(text: string): boolean {
|
|
35
|
+
if (text.length === 0) return false;
|
|
36
|
+
if (!graphemeSegmenter) return [...text].length === 1;
|
|
37
|
+
const iterator = graphemeSegmenter.segment(text)[Symbol.iterator]();
|
|
38
|
+
const first = iterator.next();
|
|
39
|
+
return !first.done && first.value.segment === text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Restyle the software cursor in one rendered line. */
|
|
43
|
+
export function applyEditorCursorStyle(line: string, style: EditorCursorStyle): string {
|
|
44
|
+
if (style === "block" || !line.includes("\x1b[7m")) return line;
|
|
45
|
+
return line.replace(REVERSE_RUN, (match, run: string) => {
|
|
46
|
+
if (!isSingleGrapheme(run)) return match;
|
|
47
|
+
return style === "underline" ? `\x1b[4m${run}\x1b[0m` : run;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Restyle the software cursor across rendered editor lines. */
|
|
52
|
+
export function applyEditorCursorStyleToLines(lines: string[], style: EditorCursorStyle): string[] {
|
|
53
|
+
if (style === "block") return lines;
|
|
54
|
+
return lines.map((line) => applyEditorCursorStyle(line, style));
|
|
55
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks the editor factory Starline installed, so a re-entrant install
|
|
3
|
+
* recognises its own work instead of wrapping it a second time.
|
|
4
|
+
*
|
|
5
|
+
* Marks are written under BOTH `pi-starline.*` and the pre-rename
|
|
6
|
+
* `pi-zentui.*` keys, and both are read back. Package load order is not
|
|
7
|
+
* controlled by either side: pi-zentui loading first only needs the legacy
|
|
8
|
+
* keys read, but pi-starline loading first (the alphabetically-first, and
|
|
9
|
+
* therefore common, order) needs a pi-zentui reader loading second to still
|
|
10
|
+
* find the mark under the legacy key it looks for. Writing only the new key
|
|
11
|
+
* would leave that direction unprotected and a second, stacked editor
|
|
12
|
+
* factory would get built.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const EDITOR_FACTORY = Symbol.for("pi-starline.editor-factory");
|
|
16
|
+
const EDITOR_BASE_FACTORY = Symbol.for("pi-starline.editor-base-factory");
|
|
17
|
+
const LEGACY_EDITOR_FACTORY = Symbol.for("pi-zentui.editor-factory");
|
|
18
|
+
const LEGACY_EDITOR_BASE_FACTORY = Symbol.for("pi-zentui.editor-base-factory");
|
|
19
|
+
|
|
20
|
+
type Marked = Record<PropertyKey, unknown>;
|
|
21
|
+
|
|
22
|
+
function asMarked(factory: unknown): Marked | undefined {
|
|
23
|
+
return typeof factory === "function" || (typeof factory === "object" && factory !== null)
|
|
24
|
+
? (factory as Marked)
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function markEditorFactory<T extends object>(factory: T, baseFactory?: object): T {
|
|
29
|
+
const marked = factory as unknown as Marked;
|
|
30
|
+
marked[EDITOR_FACTORY] = true;
|
|
31
|
+
marked[LEGACY_EDITOR_FACTORY] = true;
|
|
32
|
+
if (baseFactory) {
|
|
33
|
+
marked[EDITOR_BASE_FACTORY] = baseFactory;
|
|
34
|
+
marked[LEGACY_EDITOR_BASE_FACTORY] = baseFactory;
|
|
35
|
+
}
|
|
36
|
+
return factory;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isStarlineEditorFactory(factory: unknown): boolean {
|
|
40
|
+
const marked = asMarked(factory);
|
|
41
|
+
if (!marked) return false;
|
|
42
|
+
return marked[EDITOR_FACTORY] === true || marked[LEGACY_EDITOR_FACTORY] === true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getStarlineEditorBaseFactory<T>(factory: unknown): T | undefined {
|
|
46
|
+
const marked = asMarked(factory);
|
|
47
|
+
if (!marked) return undefined;
|
|
48
|
+
return (marked[EDITOR_BASE_FACTORY] ?? marked[LEGACY_EDITOR_BASE_FACTORY]) as T | undefined;
|
|
49
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { PolishedTuiConfig } from "./config";
|
|
3
|
+
import { type FormatToken, parseFooterFormat } from "./footer-format";
|
|
4
|
+
import { EDITOR_ACCENT_FALLBACK, renderStyleForSourceOrFallback, safeThemeFg } from "./style";
|
|
5
|
+
|
|
6
|
+
export type EditorMetadataValues = {
|
|
7
|
+
model: string;
|
|
8
|
+
modelId: string;
|
|
9
|
+
modelName: string;
|
|
10
|
+
provider: string;
|
|
11
|
+
thinking: string;
|
|
12
|
+
sessionName: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type RenderedTokens = {
|
|
16
|
+
styled: string;
|
|
17
|
+
hasDynamic: boolean;
|
|
18
|
+
hasNonEmptyDynamic: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const ESC = 0x1b;
|
|
22
|
+
const BEL = 0x07;
|
|
23
|
+
const CAN = 0x18;
|
|
24
|
+
const SUB = 0x1a;
|
|
25
|
+
const C1_DCS = 0x90;
|
|
26
|
+
const C1_CSI = 0x9b;
|
|
27
|
+
const C1_ST = 0x9c;
|
|
28
|
+
const C1_OSC = 0x9d;
|
|
29
|
+
const C1_SOS = 0x98;
|
|
30
|
+
const C1_PM = 0x9e;
|
|
31
|
+
const C1_APC = 0x9f;
|
|
32
|
+
|
|
33
|
+
function consumeCsi(value: string, start: number): number {
|
|
34
|
+
for (let index = start; index < value.length; index++) {
|
|
35
|
+
const code = value.charCodeAt(index);
|
|
36
|
+
if (code === CAN || code === SUB) return index + 1;
|
|
37
|
+
if (code >= 0x40 && code <= 0x7e) return index + 1;
|
|
38
|
+
}
|
|
39
|
+
return value.length;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function consumeControlString(value: string, start: number, allowBel: boolean): number {
|
|
43
|
+
for (let index = start; index < value.length; index++) {
|
|
44
|
+
const code = value.charCodeAt(index);
|
|
45
|
+
if (code === CAN || code === SUB) return index + 1;
|
|
46
|
+
if (allowBel && code === BEL) return index + 1;
|
|
47
|
+
if (code === C1_ST) return index + 1;
|
|
48
|
+
if (code === ESC && value.charCodeAt(index + 1) === 0x5c) return index + 2;
|
|
49
|
+
}
|
|
50
|
+
return value.length;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function consumeEscape(value: string, start: number): number {
|
|
54
|
+
if (start + 1 >= value.length) return value.length;
|
|
55
|
+
const next = value.charCodeAt(start + 1);
|
|
56
|
+
if (next === 0x5b) return consumeCsi(value, start + 2);
|
|
57
|
+
if (next === 0x5d) return consumeControlString(value, start + 2, true);
|
|
58
|
+
if (next === 0x50 || next === 0x58 || next === 0x5e || next === 0x5f) {
|
|
59
|
+
return consumeControlString(value, start + 2, false);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let index = start + 1;
|
|
63
|
+
while (index < value.length) {
|
|
64
|
+
const code = value.charCodeAt(index);
|
|
65
|
+
if (code >= 0x20 && code <= 0x2f) {
|
|
66
|
+
index += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
return code >= 0x30 && code <= 0x7e ? index + 1 : index;
|
|
70
|
+
}
|
|
71
|
+
return value.length;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isNormalizedWhitespace(code: number): boolean {
|
|
75
|
+
return (
|
|
76
|
+
code === 0x09 ||
|
|
77
|
+
code === 0x0a ||
|
|
78
|
+
code === 0x0b ||
|
|
79
|
+
code === 0x0c ||
|
|
80
|
+
code === 0x0d ||
|
|
81
|
+
code === 0x85 ||
|
|
82
|
+
code === 0x2028 ||
|
|
83
|
+
code === 0x2029
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function sanitizeEditorMetadataText(value: string): string {
|
|
88
|
+
let sanitized = "";
|
|
89
|
+
for (let index = 0; index < value.length; ) {
|
|
90
|
+
const code = value.charCodeAt(index);
|
|
91
|
+
if (code === ESC) {
|
|
92
|
+
index = consumeEscape(value, index);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (code === C1_CSI) {
|
|
96
|
+
index = consumeCsi(value, index + 1);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (code === C1_OSC) {
|
|
100
|
+
index = consumeControlString(value, index + 1, true);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (code === C1_DCS || code === C1_SOS || code === C1_PM || code === C1_APC) {
|
|
104
|
+
index = consumeControlString(value, index + 1, false);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (isNormalizedWhitespace(code)) {
|
|
108
|
+
sanitized += " ";
|
|
109
|
+
do index += 1;
|
|
110
|
+
while (index < value.length && isNormalizedWhitespace(value.charCodeAt(index)));
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (code < 0x20 || (code >= 0x7f && code <= 0x9f)) {
|
|
114
|
+
index += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
sanitized += value[index];
|
|
118
|
+
index += 1;
|
|
119
|
+
}
|
|
120
|
+
return sanitized;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function editorThinkingStyle(config: PolishedTuiConfig, level: string): string | undefined {
|
|
124
|
+
switch (level.toLowerCase()) {
|
|
125
|
+
case "minimal":
|
|
126
|
+
return config.colors.editorThinkingMinimal ?? config.colors.editorThinking;
|
|
127
|
+
case "low":
|
|
128
|
+
return config.colors.editorThinkingLow ?? config.colors.editorThinking;
|
|
129
|
+
case "medium":
|
|
130
|
+
return config.colors.editorThinkingMedium ?? config.colors.editorThinking;
|
|
131
|
+
case "high":
|
|
132
|
+
return config.colors.editorThinkingHigh ?? config.colors.editorThinking;
|
|
133
|
+
case "xhigh":
|
|
134
|
+
return config.colors.editorThinkingXhigh ?? config.colors.editorThinking;
|
|
135
|
+
default:
|
|
136
|
+
return config.colors.editorThinking;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderVariable(
|
|
141
|
+
name: string,
|
|
142
|
+
values: EditorMetadataValues,
|
|
143
|
+
uiTheme: Theme,
|
|
144
|
+
config: PolishedTuiConfig,
|
|
145
|
+
): { plain: string; styled: string } {
|
|
146
|
+
const colorSource = config.colorSources.editor;
|
|
147
|
+
const thinking = values.thinking.toLowerCase() === "off" ? "" : values.thinking;
|
|
148
|
+
const raw =
|
|
149
|
+
name === "model"
|
|
150
|
+
? values.model
|
|
151
|
+
: name === "model_id"
|
|
152
|
+
? values.modelId
|
|
153
|
+
: name === "model_name"
|
|
154
|
+
? values.modelName
|
|
155
|
+
: name === "provider"
|
|
156
|
+
? values.provider
|
|
157
|
+
: name === "thinking"
|
|
158
|
+
? thinking
|
|
159
|
+
: name === "session_name"
|
|
160
|
+
? values.sessionName
|
|
161
|
+
: "";
|
|
162
|
+
const plain = sanitizeEditorMetadataText(raw);
|
|
163
|
+
if (!plain) return { plain: "", styled: "" };
|
|
164
|
+
|
|
165
|
+
if (name === "model" || name === "model_id" || name === "model_name") {
|
|
166
|
+
return {
|
|
167
|
+
plain,
|
|
168
|
+
styled: renderStyleForSourceOrFallback(
|
|
169
|
+
uiTheme,
|
|
170
|
+
colorSource,
|
|
171
|
+
config.colors.editorModel,
|
|
172
|
+
EDITOR_ACCENT_FALLBACK,
|
|
173
|
+
plain,
|
|
174
|
+
),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (name === "provider") {
|
|
178
|
+
return {
|
|
179
|
+
plain,
|
|
180
|
+
styled: renderStyleForSourceOrFallback(
|
|
181
|
+
uiTheme,
|
|
182
|
+
colorSource,
|
|
183
|
+
config.colors.editorProvider,
|
|
184
|
+
"text",
|
|
185
|
+
plain,
|
|
186
|
+
),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (name === "thinking") {
|
|
190
|
+
return {
|
|
191
|
+
plain,
|
|
192
|
+
styled: renderStyleForSourceOrFallback(
|
|
193
|
+
uiTheme,
|
|
194
|
+
colorSource,
|
|
195
|
+
editorThinkingStyle(config, plain),
|
|
196
|
+
"muted",
|
|
197
|
+
plain,
|
|
198
|
+
),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (name === "session_name") {
|
|
202
|
+
return { plain, styled: safeThemeFg(uiTheme, "border", plain) };
|
|
203
|
+
}
|
|
204
|
+
return { plain: "", styled: "" };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function renderTokens(
|
|
208
|
+
tokens: FormatToken[],
|
|
209
|
+
values: EditorMetadataValues,
|
|
210
|
+
uiTheme: Theme,
|
|
211
|
+
config: PolishedTuiConfig,
|
|
212
|
+
): RenderedTokens {
|
|
213
|
+
let styled = "";
|
|
214
|
+
let hasDynamic = false;
|
|
215
|
+
let hasNonEmptyDynamic = false;
|
|
216
|
+
|
|
217
|
+
for (const token of tokens) {
|
|
218
|
+
if (token.kind === "text") {
|
|
219
|
+
const plain = sanitizeEditorMetadataText(token.value);
|
|
220
|
+
if (plain) styled += safeThemeFg(uiTheme, "border", plain);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (token.kind === "fill") {
|
|
224
|
+
hasDynamic = true;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (token.kind === "var") {
|
|
228
|
+
hasDynamic = true;
|
|
229
|
+
const rendered = renderVariable(token.name, values, uiTheme, config);
|
|
230
|
+
styled += rendered.styled;
|
|
231
|
+
if (rendered.plain) hasNonEmptyDynamic = true;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const rendered = renderTokens(token.tokens, values, uiTheme, config);
|
|
236
|
+
const visible = !rendered.hasDynamic || rendered.hasNonEmptyDynamic;
|
|
237
|
+
hasDynamic = true;
|
|
238
|
+
if (visible) {
|
|
239
|
+
styled += rendered.styled;
|
|
240
|
+
hasNonEmptyDynamic = true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { styled, hasDynamic, hasNonEmptyDynamic };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function renderEditorMetadataFormat(
|
|
248
|
+
format: string,
|
|
249
|
+
values: EditorMetadataValues,
|
|
250
|
+
uiTheme: Theme,
|
|
251
|
+
config: PolishedTuiConfig,
|
|
252
|
+
): string {
|
|
253
|
+
return renderTokens(
|
|
254
|
+
parseFooterFormat(sanitizeEditorMetadataText(format)),
|
|
255
|
+
values,
|
|
256
|
+
uiTheme,
|
|
257
|
+
config,
|
|
258
|
+
).styled;
|
|
259
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import type {
|
|
3
|
+
ExtensionStatusColorMode,
|
|
4
|
+
ExtensionStatusPlacement,
|
|
5
|
+
PolishedTuiConfig,
|
|
6
|
+
} from "./config";
|
|
7
|
+
import { getExtensionStatusColorMode, getExtensionStatusPlacement } from "./config";
|
|
8
|
+
|
|
9
|
+
export type ExtensionStatusSegment = {
|
|
10
|
+
key: string;
|
|
11
|
+
text: string;
|
|
12
|
+
placement: ExtensionStatusPlacement;
|
|
13
|
+
colorMode: ExtensionStatusColorMode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type ExtensionStatusSegmentsByPlacement = {
|
|
17
|
+
left: ExtensionStatusSegment[];
|
|
18
|
+
middle: ExtensionStatusSegment[];
|
|
19
|
+
right: ExtensionStatusSegment[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const safeSgrPattern = /\x1b\[[0-9;:]*m/g;
|
|
23
|
+
const sgrPlaceholderPattern = /__STARLINE_SGR_(\d+)__/g;
|
|
24
|
+
|
|
25
|
+
function compareKeys(a: ExtensionStatusSegment, b: ExtensionStatusSegment): number {
|
|
26
|
+
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeStatusWhitespace(value: string): string {
|
|
30
|
+
return value
|
|
31
|
+
.replace(/[\r\n\t\f\v]+/g, " ")
|
|
32
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, "")
|
|
33
|
+
.replace(/\s+/g, " ")
|
|
34
|
+
.trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function sanitizeExtensionStatusText(value: string): string {
|
|
38
|
+
return normalizeStatusWhitespace(stripVTControlCharacters(value));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasVisibleStatusText(value: string): boolean {
|
|
42
|
+
return sanitizeExtensionStatusText(value).length > 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sanitizeExtensionStatusOriginalText(value: string): string {
|
|
46
|
+
const safeSequences: string[] = [];
|
|
47
|
+
const protectedValue = value.replace(safeSgrPattern, (sequence) => {
|
|
48
|
+
const index = safeSequences.push(sequence) - 1;
|
|
49
|
+
return `__STARLINE_SGR_${index}__`;
|
|
50
|
+
});
|
|
51
|
+
const cleaned = normalizeStatusWhitespace(stripVTControlCharacters(protectedValue));
|
|
52
|
+
const restored = cleaned.replace(sgrPlaceholderPattern, (_match, indexText: string) => {
|
|
53
|
+
const index = Number.parseInt(indexText, 10);
|
|
54
|
+
return safeSequences[index] ?? "";
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return hasVisibleStatusText(restored) ? restored : "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function collectExtensionStatusSegments(
|
|
61
|
+
statuses: ReadonlyMap<string, string>,
|
|
62
|
+
config: PolishedTuiConfig,
|
|
63
|
+
): ExtensionStatusSegmentsByPlacement {
|
|
64
|
+
const segments: ExtensionStatusSegmentsByPlacement = {
|
|
65
|
+
left: [],
|
|
66
|
+
middle: [],
|
|
67
|
+
right: [],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
for (const [key, value] of statuses.entries()) {
|
|
71
|
+
const placement = getExtensionStatusPlacement(config, key);
|
|
72
|
+
if (placement === "off") continue;
|
|
73
|
+
|
|
74
|
+
const colorMode = getExtensionStatusColorMode(config, key);
|
|
75
|
+
const text =
|
|
76
|
+
colorMode === "original"
|
|
77
|
+
? sanitizeExtensionStatusOriginalText(value)
|
|
78
|
+
: sanitizeExtensionStatusText(value);
|
|
79
|
+
if (!text) continue;
|
|
80
|
+
|
|
81
|
+
segments[placement].push({ key, text, placement, colorMode });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
segments.left.sort(compareKeys);
|
|
85
|
+
segments.middle.sort(compareKeys);
|
|
86
|
+
segments.right.sort(compareKeys);
|
|
87
|
+
return segments;
|
|
88
|
+
}
|