@zenodinh/pi-render 0.1.7 → 0.1.8
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/package.json
CHANGED
package/src/core/paint.ts
CHANGED
|
@@ -60,7 +60,9 @@ export function createRowPaint(theme: HostTheme): RowPaint {
|
|
|
60
60
|
accent: (text) => fgOrPlain(theme, "accent", text),
|
|
61
61
|
error: (text) => fgOrPlain(theme, "error", text),
|
|
62
62
|
warning: (text) => fgOrPlain(theme, "warning", text),
|
|
63
|
-
|
|
63
|
+
// The settled ink is `muted` by owner pick (#58): the theme's green read too bright. The role
|
|
64
|
+
// keeps its name, so the three state call sites stay one expression.
|
|
65
|
+
success: (text) => fgOrPlain(theme, "muted", text),
|
|
64
66
|
gutter: (text) => fgOrPlain(theme, "muted", text),
|
|
65
67
|
match: (text) => matchOrAccent(theme, text),
|
|
66
68
|
diffAdded: (text) => fgOrPlain(theme, "toolDiffAdded", text),
|
package/src/core/types/paint.ts
CHANGED
|
@@ -26,7 +26,10 @@ export interface RowPaint {
|
|
|
26
26
|
error(text: string): string;
|
|
27
27
|
/** Warning text for a degraded or partial result. Required. */
|
|
28
28
|
warning(text: string): string;
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* The settled ink: a call that did not fail, drawn in the `muted` token (thinking's gray) by owner
|
|
31
|
+
* pick (#58). The role keeps its name so the state call sites stay one expression. Required.
|
|
32
|
+
*/
|
|
30
33
|
success(text: string): string;
|
|
31
34
|
/** The row's left gutter marker. Required. */
|
|
32
35
|
gutter(text: string): string;
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The header row is stamped through the header-band role — bold over the accent ink inside a
|
|
11
11
|
* full-bleed band spanning each cell's whole interior plus its trailing separator, so the header
|
|
12
|
-
* reads as one bar (FP-11)
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* reads as one bar (FP-11); where the band abuts the outer frame, the frame column joins it too
|
|
13
|
+
* (#57), so the field reaches the border. When the header row is empty (`| | |`), that stamp moves
|
|
14
|
+
* onto the first column instead of spending a blank band. `**strong**` and `*em*` cells route
|
|
15
|
+
* through their own roles.
|
|
15
16
|
*
|
|
16
17
|
* Boundary: lane-local. The transformer passes markdown in and splices the returned markdown back
|
|
17
18
|
* out, so every escape in the output originates in the injected ContentPaint (T-08) — the #22 fix.
|
|
@@ -165,7 +166,13 @@ function renderTable(token: TableToken, width: number, paint: ContentPaint): str
|
|
|
165
166
|
const unit = index < cells.length - 1 ? ` ${padded} ${bar}` : ` ${padded} `;
|
|
166
167
|
return banded(index) ? paint.headerBand(unit) : unit;
|
|
167
168
|
});
|
|
168
|
-
|
|
169
|
+
// #57 (owner pick): the band reaches the outer frame where it abuts it — a banded first cell
|
|
170
|
+
// takes the left frame column into the band, a banded last cell the right one. The frame glyph
|
|
171
|
+
// joins the band's own style (dark-on-green), not the rule ink, so no unbanded sliver remains
|
|
172
|
+
// between the field and the border.
|
|
173
|
+
const left = banded(0) ? paint.headerBand("│") : bar;
|
|
174
|
+
const right = banded(cells.length - 1) ? paint.headerBand("│") : bar;
|
|
175
|
+
physical.push(`${left}${parts.join("")}${right}`);
|
|
169
176
|
}
|
|
170
177
|
};
|
|
171
178
|
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* renderCall/renderResult methods, with no subclassing.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
18
19
|
import type {
|
|
19
20
|
HostTheme,
|
|
20
21
|
RenderContext,
|
|
@@ -166,6 +167,11 @@ interface PaddedText extends UiComponent {
|
|
|
166
167
|
setText(body: string): void;
|
|
167
168
|
/** Re-reads the row's indent, so a host padding change lands on the next frame. Required. */
|
|
168
169
|
setPad(pad: number): void;
|
|
170
|
+
/**
|
|
171
|
+
* Sets how an over-wide line is spent: false cuts it to the budget with an ellipsis, true reflows it.
|
|
172
|
+
* Required: one component serves both row states, so the mode must follow the frame.
|
|
173
|
+
*/
|
|
174
|
+
setWrap(wrap: boolean): void;
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
/** `in`-narrowed, never cast: a recorded component is the host's until it answers this protocol. */
|
|
@@ -180,7 +186,8 @@ function textComponent(body: string): UiComponent {
|
|
|
180
186
|
render(_width: number): string[] {
|
|
181
187
|
// One entry per line; an empty body still yields one line, so a degraded row never vanishes.
|
|
182
188
|
// Width is the component's own: renderResult receives none, so nothing here is width-keyed.
|
|
183
|
-
// The offer minus the indent is handed down by the pad wrapper
|
|
189
|
+
// The offer minus the indent is handed down by the pad wrapper, which also spends what comes back —
|
|
190
|
+
// cut when collapsed, reflowed when expanded.
|
|
184
191
|
return current.split("\n");
|
|
185
192
|
},
|
|
186
193
|
invalidate(): void {
|
|
@@ -193,22 +200,39 @@ function textComponent(body: string): UiComponent {
|
|
|
193
200
|
}
|
|
194
201
|
|
|
195
202
|
/**
|
|
196
|
-
* The pad wrapper:
|
|
197
|
-
* the
|
|
203
|
+
* The pad wrapper: the inner component gets the offer minus the indent, every line it draws takes the
|
|
204
|
+
* indent, and every line is fitted to the budget — an inner is told the budget, not obliged to spend it.
|
|
198
205
|
*
|
|
199
206
|
* Both halves are ours because a row asks the host for the "self" shell (#51), and that shell skips the
|
|
200
207
|
* Box(1,1) which used to indent each line and render its child at `width - paddingX * 2` (pi-tui
|
|
201
208
|
* box.js:83 → :91 → :115). The budget half is what a reused host `Text` needs: it wraps at the width it
|
|
202
209
|
* is given (pi-tui text.js:53-56), so a full-width title would wrap at the whole offer and then be
|
|
203
|
-
* pushed one column past it by the indent.
|
|
210
|
+
* pushed one column past it by the indent. Spending is the other half of the same duty (#56): our own
|
|
211
|
+
* text component ignores the width it is handed, so an over-wide title used to run past the offer.
|
|
204
212
|
*
|
|
205
|
-
*
|
|
213
|
+
* How an over-wide line is spent follows the row's state — the owner's review on #59: an expansion must
|
|
214
|
+
* show the FULL request and result. Collapsed cuts to the budget, ellipsis-marked: one logical line is the
|
|
215
|
+
* collapse contract. Expanded reflows with pi-tui's `wrapTextWithAnsi` — the call the table and code-panel
|
|
216
|
+
* surfaces already spend their cells with — while truncation stays only as the backstop for a slice that
|
|
217
|
+
* no width can hold (table.ts:187's rule). A line within budget passes through untouched either way.
|
|
218
|
+
*
|
|
219
|
+
* shape: closure returning an object literal — trigger #4: one stored pad and one spending mode behind
|
|
220
|
+
* render/setPad/setWrap.
|
|
206
221
|
*/
|
|
207
|
-
function padComponent(inner: UiComponent, pad: number): PaddedText {
|
|
222
|
+
function padComponent(inner: UiComponent, pad: number, wrap: boolean): PaddedText {
|
|
208
223
|
let indent = pad;
|
|
224
|
+
let wrapping = wrap;
|
|
209
225
|
return {
|
|
210
226
|
render(width: number): string[] {
|
|
211
|
-
const
|
|
227
|
+
const budget = Math.max(0, width - indent);
|
|
228
|
+
const lines = inner.render(budget).flatMap((line) => {
|
|
229
|
+
if (visibleWidth(line) <= budget) return [line];
|
|
230
|
+
// Collapsed cuts, expanded reflows (see the block comment): only an over-wide line is spent.
|
|
231
|
+
const spent = wrapping && budget > 0 ? wrapTextWithAnsi(line, budget) : [truncateToWidth(line, budget, "…")];
|
|
232
|
+
// Backstop: a wrapped slice can still exceed the budget (a wide grapheme), and a line no
|
|
233
|
+
// width can hold is cut rather than left to run past the edge (#56).
|
|
234
|
+
return spent.map((piece) => (visibleWidth(piece) > budget ? truncateToWidth(piece, budget, "…") : piece));
|
|
235
|
+
});
|
|
212
236
|
if (indent === 0) return lines;
|
|
213
237
|
const spaces = " ".repeat(indent);
|
|
214
238
|
return lines.map((line) => spaces + line);
|
|
@@ -222,10 +246,13 @@ function padComponent(inner: UiComponent, pad: number): PaddedText {
|
|
|
222
246
|
setPad(value: number): void {
|
|
223
247
|
indent = value;
|
|
224
248
|
},
|
|
249
|
+
setWrap(value: boolean): void {
|
|
250
|
+
wrapping = value;
|
|
251
|
+
},
|
|
225
252
|
};
|
|
226
253
|
}
|
|
227
254
|
|
|
228
|
-
function textFor(ctx: RenderContext, body: string): UiComponent {
|
|
255
|
+
function textFor(ctx: RenderContext, body: string, wrap: boolean): UiComponent {
|
|
229
256
|
const pad = padOf(ctx);
|
|
230
257
|
const reuse = ctx.lastComponent;
|
|
231
258
|
// Only a component this runtime wrapped can be told a new pad, so it is the only one it reuses. A
|
|
@@ -234,6 +261,7 @@ function textFor(ctx: RenderContext, body: string): UiComponent {
|
|
|
234
261
|
if (reuse !== undefined && isPaddedText(reuse)) {
|
|
235
262
|
reuse.setText(body);
|
|
236
263
|
reuse.setPad(pad);
|
|
264
|
+
reuse.setWrap(wrap);
|
|
237
265
|
return reuse;
|
|
238
266
|
}
|
|
239
267
|
// A host text component in the slot is still worth mutating in place: it caches its own frame, and
|
|
@@ -241,9 +269,9 @@ function textFor(ctx: RenderContext, body: string): UiComponent {
|
|
|
241
269
|
const setText = reuse?.setText;
|
|
242
270
|
if (reuse !== undefined && setText !== undefined) {
|
|
243
271
|
setText.call(reuse, body);
|
|
244
|
-
return padComponent(reuse, pad);
|
|
272
|
+
return padComponent(reuse, pad, wrap);
|
|
245
273
|
}
|
|
246
|
-
return padComponent(textComponent(body), pad);
|
|
274
|
+
return padComponent(textComponent(body), pad, wrap);
|
|
247
275
|
}
|
|
248
276
|
|
|
249
277
|
/**
|
|
@@ -374,11 +402,13 @@ export function createRowRenderers(spec: RowSpec, deps: { paint: RowPaintFactory
|
|
|
374
402
|
const ink = ctx.isError === true ? paint.error : ctx.isPartial === true ? paint.dim : paint.success;
|
|
375
403
|
const title = ink(model.title);
|
|
376
404
|
// Collapsed rows are one line: the summary the result slot settled rides this title line.
|
|
377
|
-
|
|
378
|
-
|
|
405
|
+
// Expanded reflows, so a long title stays readable in full; collapsed cuts to its one line.
|
|
406
|
+
const expanded = ctx.expanded === true;
|
|
407
|
+
const summary = expanded ? undefined : readSummary(ctx);
|
|
408
|
+
return textFor(ctx, summary === undefined ? title : `${title} · ${summary}`, expanded);
|
|
379
409
|
} catch (err) {
|
|
380
410
|
logFailure(deps.log, ctx, err);
|
|
381
|
-
return textFor(ctx, "");
|
|
411
|
+
return textFor(ctx, "", ctx.expanded === true);
|
|
382
412
|
}
|
|
383
413
|
};
|
|
384
414
|
|
|
@@ -391,10 +421,11 @@ export function createRowRenderers(spec: RowSpec, deps: { paint: RowPaintFactory
|
|
|
391
421
|
try {
|
|
392
422
|
const paint = deps.paint(theme);
|
|
393
423
|
const model = modelFor(result, ctx);
|
|
394
|
-
// A degraded row keeps
|
|
395
|
-
|
|
424
|
+
// A degraded row keeps the raw one-line body and settles no summary for the call slot to adopt;
|
|
425
|
+
// an expanded frame spends that line by the same reflow rule as any other.
|
|
426
|
+
if (model === UNKNOWN) return textFor(ctx, rawBody(result, paint), options.expanded === true);
|
|
396
427
|
const body = spec.project(model, viewOf(options, ctx), paint);
|
|
397
|
-
if (options.expanded) return textFor(ctx, body);
|
|
428
|
+
if (options.expanded) return textFor(ctx, body, true);
|
|
398
429
|
// Collapsed: hand the one-line summary to the call slot through the shared state bag and draw no
|
|
399
430
|
// line here. Redraw only when it changed — an unchanged summary is not a new frame.
|
|
400
431
|
if (readSummary(ctx) !== body) {
|
|
@@ -405,7 +436,7 @@ export function createRowRenderers(spec: RowSpec, deps: { paint: RowPaintFactory
|
|
|
405
436
|
} catch (err) {
|
|
406
437
|
// The paint may itself be the thrower, so the degraded row is built without it.
|
|
407
438
|
logFailure(deps.log, ctx, err);
|
|
408
|
-
return textFor(ctx, rawBody(result, undefined));
|
|
439
|
+
return textFor(ctx, rawBody(result, undefined), options.expanded === true);
|
|
409
440
|
}
|
|
410
441
|
};
|
|
411
442
|
|