pi-supernova 0.3.1 → 0.4.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.
@@ -1,18 +1,25 @@
1
1
  const toStr = Object.prototype.toString;
2
2
 
3
3
  export const isString = (v) => toStr.call(v) === "[object String]";
4
+
4
5
  export const isObject = (v) => toStr.call(v) === "[object Object]";
6
+
5
7
  export const isFunction = (v) => toStr.call(v) === "[object Function]" || v instanceof Function;
8
+
6
9
  export const isNumber = (v) => toStr.call(v) === "[object Number]" && Number.isFinite(v);
7
10
 
8
11
  const MAX_DEPTH = 64;
12
+
9
13
  const MAX_TYPED_ARRAY = 4096;
10
14
 
11
15
  function plainFromBinary(value) {
12
16
  const bytes = value.byteLength;
17
+
13
18
  if (value instanceof ArrayBuffer) value = new Uint8Array(value);
14
19
  else if (value instanceof DataView) value = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
20
+
15
21
  if (value.length > MAX_TYPED_ARRAY) return "[" + value.constructor.name + " " + bytes + " bytes]";
22
+
16
23
  return value instanceof BigInt64Array || value instanceof BigUint64Array
17
24
  ? Array.from(value, x => x.toString() + "n")
18
25
  : Array.from(value);
@@ -20,18 +27,25 @@ function plainFromBinary(value) {
20
27
 
21
28
  function plainFromMap(value, seen, depth) {
22
29
  const allStringKeys = [...value.keys()].every(isString);
30
+
23
31
  if (!allStringKeys) return [...value].map(([k, v]) => [toPlain(k, seen, depth + 1), toPlain(v, seen, depth + 1)]);
24
32
  const out = Object.create(null);
33
+
25
34
  for (const [k, v] of value) out[k] = toPlain(v, seen, depth + 1);
35
+
26
36
  return out;
27
37
  }
28
38
 
29
39
  function plainFromCollection(value, seen, depth) {
30
40
  if (Array.isArray(value)) return value.map((x) => toPlain(x, seen, depth + 1));
41
+
31
42
  if (value instanceof Set) return [...value].map((x) => toPlain(x, seen, depth + 1));
43
+
32
44
  if (value instanceof Map) return plainFromMap(value, seen, depth);
33
45
  const out = Object.create(null);
46
+
34
47
  for (const k of Object.keys(value)) out[k] = toPlain(value[k], seen, depth + 1);
48
+
35
49
  return out;
36
50
  }
37
51
 
@@ -39,23 +53,38 @@ function plainFromCollection(value, seen, depth) {
39
53
  export function toPlain(value, seen = new Set(), depth = 0) {
40
54
  if (value === null || value === undefined) return value;
41
55
  const tag = toStr.call(value);
56
+
42
57
  if (tag === "[object String]" || tag === "[object Number]" || tag === "[object Boolean]") return value.valueOf();
58
+
43
59
  if (tag === "[object BigInt]") return value.toString() + "n";
60
+
44
61
  if (isFunction(value)) return "[Function" + (value.name ? " " + value.name : "") + "]";
62
+
45
63
  if (tag === "[object Symbol]") return value.toString();
64
+
46
65
  if (depth > MAX_DEPTH) return "[Depth]";
66
+
47
67
  if (seen.has(value)) return "[Circular]";
68
+
48
69
  if (value instanceof Date) return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
70
+
49
71
  if (value instanceof RegExp) return value.toString();
72
+
50
73
  if (value instanceof Error) {
51
74
  const out = { name: value.name, message: value.message };
75
+
52
76
  if (value.cause !== undefined) out.cause = toPlain(value.cause, seen, depth + 1);
77
+
53
78
  return out;
54
79
  }
80
+
55
81
  if (value instanceof Promise) return "[Promise]";
82
+
56
83
  if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return plainFromBinary(value);
84
+
57
85
  if (isFunction(value.toJSON)) return toPlain(value.toJSON(), seen, depth + 1);
58
86
  seen.add(value);
87
+
59
88
  try {
60
89
  return plainFromCollection(value, seen, depth);
61
90
  } finally {
@@ -21,7 +21,9 @@ const DEFAULT_BOX = {
21
21
 
22
22
  function boxOf(theme) {
23
23
  const b = theme?.boxRound;
24
+
24
25
  if (b && b.topLeft && b.horizontal && b.vertical) return b;
26
+
25
27
  return DEFAULT_BOX;
26
28
  }
27
29
 
@@ -33,6 +35,7 @@ function borderKeyFor(state) {
33
35
 
34
36
  function borderPaint(theme, state, borderColor) {
35
37
  const key = borderColor || borderKeyFor(state);
38
+
36
39
  if (theme && isFunction(theme.fg)) {
37
40
  try {
38
41
  return (text) => theme.fg(key, text);
@@ -40,6 +43,7 @@ function borderPaint(theme, state, borderColor) {
40
43
  /* fall through */
41
44
  }
42
45
  }
46
+
43
47
  return (text) => text;
44
48
  }
45
49
 
@@ -51,6 +55,7 @@ function statusHeader(theme, { title, description, state, icon }) {
51
55
  const prefix = prefixSpec ? (theme?.fg ? theme.fg(prefixSpec[0], prefixSpec[1]) : prefixSpec[1]) : "";
52
56
  const titleText = theme?.fg ? theme.fg("accent", title) : title;
53
57
  const descText = description ? (theme?.fg ? theme.fg("muted", description) : description) : "";
58
+
54
59
  return descText ? `${prefix}${titleText}: ${descText}` : `${prefix}${titleText}`;
55
60
  }
56
61
 
@@ -59,6 +64,7 @@ function padLine(line, width, bgFn) {
59
64
  const vis = measureWidth(line);
60
65
  const pad = Math.max(0, w - vis);
61
66
  const padded = line + " ".repeat(pad);
67
+
62
68
  return bgFn ? bgFn(padded) : padded;
63
69
  }
64
70
 
@@ -71,6 +77,7 @@ function bgKeyFor(state) {
71
77
  function wrapBg(paint) {
72
78
  return (text) => {
73
79
  const out = paint(text);
80
+
74
81
  return isString(out) ? out : text;
75
82
  };
76
83
  }
@@ -78,18 +85,24 @@ function wrapBg(paint) {
78
85
  function bgFnForState(theme, state) {
79
86
  if (!state || !theme) return undefined;
80
87
  const key = bgKeyFor(state);
88
+
81
89
  if (isFunction(theme.bg)) {
82
90
  try {
83
91
  if (!isString(theme.bg(key, "x"))) return undefined;
84
92
  } catch {
85
93
  return undefined;
86
94
  }
95
+
87
96
  return wrapBg((text) => theme.bg(key, text));
88
97
  }
98
+
89
99
  if (!isFunction(theme.getBgAnsi)) return undefined;
100
+
90
101
  try {
91
102
  const ansi = theme.getBgAnsi(key);
103
+
92
104
  if (!ansi) return undefined;
105
+
93
106
  return (text) => `${ansi}${text}\x1b[49m`;
94
107
  } catch {
95
108
  return undefined;
@@ -100,8 +113,10 @@ function frameBodyLines(sections, contentWidth, box, border, bgFn, w, paintBar)
100
113
  const lines = [];
101
114
  const normalized = sections.length > 0 ? sections : [{ lines: [] }];
102
115
  const v = box.vertical;
116
+
103
117
  for (const section of normalized) {
104
118
  if (section.label) lines.push(paintBar(box.teeRight || "├", box.teeLeft || "┤", section.label));
119
+
105
120
  for (const raw of section.lines || []) {
106
121
  for (const piece of String(raw).split("\n")) {
107
122
  const body = clampLine(piece, contentWidth);
@@ -110,19 +125,24 @@ function frameBodyLines(sections, contentWidth, box, border, bgFn, w, paintBar)
110
125
  }
111
126
  }
112
127
  }
128
+
113
129
  return lines;
114
130
  }
115
131
 
116
132
  function renderPortableFrame(theme, { header, sections = [], state = "pending", borderColor, width }) {
117
133
  const w = Math.max(1, width | 0);
134
+
118
135
  if (w < 8) {
119
136
  const rawLines = [header];
137
+
120
138
  for (const section of sections) {
121
139
  if (section.label) rawLines.push(section.label);
122
140
  rawLines.push(...(section.lines || []));
123
141
  }
124
- return rawLines.filter(Boolean).map((line) => clampLine(line, w));
142
+
143
+ return rawLines.flatMap((line) => line ? [clampLine(line, w)] : []);
125
144
  }
145
+
126
146
  const box = boxOf(theme);
127
147
  const border = borderPaint(theme, state, borderColor);
128
148
  const bgFn = bgFnForState(theme, state);
@@ -132,14 +152,18 @@ function renderPortableFrame(theme, { header, sections = [], state = "pending",
132
152
  const paintBar = (leftChar, rightChar, label) => {
133
153
  const left = `${leftChar}${cap}`;
134
154
  const right = rightChar;
155
+
135
156
  if (!label) {
136
157
  const fill = Math.max(0, w - measureWidth(left) - measureWidth(right));
158
+
137
159
  return padLine(`${border(left)}${border(h.repeat(fill))}${border(right)}`, w, bgFn);
138
160
  }
161
+
139
162
  const rawLabel = ` ${label} `;
140
163
  const maxLabel = Math.max(0, w - measureWidth(left) - measureWidth(right));
141
164
  const trimmed = clampLine(rawLabel, maxLabel);
142
165
  const fill = Math.max(0, w - measureWidth(left) - measureWidth(trimmed) - measureWidth(right));
166
+
143
167
  return padLine(`${border(left)}${trimmed}${border(h.repeat(fill))}${border(right)}`, w, bgFn);
144
168
  };
145
169
 
@@ -148,6 +172,7 @@ function renderPortableFrame(theme, { header, sections = [], state = "pending",
148
172
  lines.push(paintBar(box.topLeft, box.topRight, header));
149
173
  lines.push(...frameBodyLines(sections, contentWidth, box, border, bgFn, w, paintBar));
150
174
  lines.push(paintBar(box.bottomLeft, box.bottomRight, null));
175
+
151
176
  return lines;
152
177
  }
153
178
 
@@ -155,16 +180,20 @@ function createPortableFramedComponent(theme, build) {
155
180
  let cacheWidth;
156
181
  let cacheKey;
157
182
  let cacheLines;
183
+
158
184
  return {
159
185
  render(width) {
160
186
  const opts = build(width);
187
+
161
188
  const key = `${opts.state}|${opts.borderColor}|${opts.header}|${(opts.sections || [])
162
189
  .map((s) => (s.lines || []).join("\n"))
163
190
  .join("||")}`;
191
+
164
192
  if (cacheLines && cacheWidth === width && cacheKey === key) return cacheLines;
165
193
  cacheLines = renderPortableFrame(theme, opts);
166
194
  cacheWidth = width;
167
195
  cacheKey = key;
196
+
168
197
  return cacheLines;
169
198
  },
170
199
  invalidate() {
@@ -2,24 +2,30 @@ import { stripVTControlCharacters } from "node:util";
2
2
  import stringWidth from "string-width";
3
3
 
4
4
  const ELLIPSIS = "…";
5
+
5
6
  const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
6
7
 
7
8
  const widthCache = new Map();
9
+
8
10
  let cachedWidthChars = 0;
11
+
9
12
  const MAX_WIDTH_CACHE_CHARS = 512_000;
10
13
 
11
14
  export function measureWidth(text) {
12
15
  const raw = String(text ?? "");
13
16
  const cached = widthCache.get(raw);
17
+
14
18
  if (cached !== undefined) return cached;
15
19
  const normalized = raw.replace(/\t/g, " ");
16
20
  // eslint-disable-next-line no-control-regex -- intentional ANSI SGR recognition
17
21
  const plain = normalized.replace(/\x1b\[(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?m/g, "");
22
+
18
23
  // ASCII and these single-column chrome glyphs need no Unicode segmentation.
19
24
  // Any other character/control/escape sequence uses the full oracle.
20
25
  const width = /^[\x20-\x7e\u2500-\u257f\u00b7\u00d7\u2026\u2713\u2717]*$/.test(plain)
21
26
  ? plain.length
22
27
  : stringWidth(normalized);
28
+
23
29
  // Cache immutable text only, never host/theme/result objects. Bound both
24
30
  // bookkeeping and retained text; unusually long lines bypass retention.
25
31
  if (raw.length <= 4096) {
@@ -28,32 +34,41 @@ export function measureWidth(text) {
28
34
  widthCache.delete(oldest);
29
35
  cachedWidthChars -= oldest.length;
30
36
  }
37
+
31
38
  widthCache.set(raw, width);
32
39
  cachedWidthChars += raw.length;
33
40
  }
41
+
34
42
  return width;
35
43
  }
36
44
 
37
45
  function takePrefix(text, width) {
38
46
  let end = 0;
39
47
  let columns = 0;
48
+
40
49
  for (const { segment, index } of segmenter.segment(text)) {
41
50
  const next = measureWidth(segment);
51
+
42
52
  if (columns + next > width) break;
43
53
  columns += next;
44
54
  end = index + segment.length;
45
55
  }
56
+
46
57
  return text.slice(0, end);
47
58
  }
48
59
 
49
60
  export function hardTruncate(text, maxWidth, ellipsis = ELLIPSIS) {
50
61
  const width = Math.max(0, Math.floor(maxWidth));
62
+
51
63
  if (!width) return "";
52
64
  const raw = String(text ?? "").replace(/\t/g, " ");
65
+
53
66
  if (measureWidth(raw) <= width) return raw;
54
67
  const suffix = stripVTControlCharacters(String(ellipsis));
55
68
  const suffixWidth = measureWidth(suffix);
69
+
56
70
  if (suffixWidth >= width) return takePrefix(suffix, width);
71
+
57
72
  return takePrefix(stripVTControlCharacters(raw), width - suffixWidth) + suffix;
58
73
  }
59
74
 
@@ -65,27 +80,36 @@ export function clampLine(line, width) {
65
80
  export function wrapLine(line, width) {
66
81
  if (width <= 0) return [];
67
82
  const text = String(line).replace(/\t/g, " ");
83
+
68
84
  if (measureWidth(text) <= width) return [text];
69
85
  const out = [];
70
86
  let current = "";
71
87
  let columns = 0;
88
+
72
89
  for (const { segment } of segmenter.segment(text)) {
73
90
  const size = measureWidth(segment);
91
+
74
92
  if (columns + size > width && current) { out.push(current); current = ""; columns = 0; }
93
+
75
94
  if (size > width) { out.push(ELLIPSIS); continue; }
95
+
76
96
  current += segment;
77
97
  columns += size;
78
98
  }
99
+
79
100
  if (current) out.push(current);
101
+
80
102
  return out;
81
103
  }
82
104
 
83
105
  export function fitPath(pathText, budget) {
84
106
  const width = Math.max(0, Math.floor(budget));
85
107
  const text = String(pathText ?? "").replace(/\\/g, "/");
108
+
86
109
  if (measureWidth(text) <= width) return text;
87
110
  const parts = text.split("/").filter(Boolean);
88
111
  const base = parts.at(-1) ?? text;
89
112
  const suffix = parts.length > 1 ? "…/" + base : base;
113
+
90
114
  return measureWidth(suffix) <= width ? suffix : hardTruncate(base, width);
91
115
  }