@kolisachint/hoocode-tui 0.5.73 → 0.5.74
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/dist/components/box.d.ts +2 -2
- package/dist/components/box.d.ts.map +1 -1
- package/dist/components/box.js +14 -14
- package/dist/components/box.js.map +1 -1
- package/dist/tui.d.ts +31 -5
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +107 -9
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
package/dist/components/box.d.ts
CHANGED
|
@@ -54,8 +54,8 @@ export declare class Box implements Component {
|
|
|
54
54
|
* which paints a hairline across the top of its cells and so hugs the box's
|
|
55
55
|
* bottom edge — indented one column to give the offset a terminal cannot
|
|
56
56
|
* give in sub-pixels. An inset box gets a matching column of `▏` down its
|
|
57
|
-
* right edge and the
|
|
58
|
-
*
|
|
57
|
+
* right edge, and the run stops where that column starts, so the two meet as
|
|
58
|
+
* an L; with no inset the band already owns the last cell, so the bottom
|
|
59
59
|
* edge is all there is room for.
|
|
60
60
|
*
|
|
61
61
|
* Eighths, not halves. `▀` and `▌` are half a cell of solid ink, which at a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"box.d.ts","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAY3C;;;;GAIG;AACH,MAAM,WAAW,UAAU;IAC1B,0EAA0E;IAC1E,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAClC;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,qBAAa,GAAI,YAAW,SAAS;IACpC,QAAQ,EAAE,SAAS,EAAE,CAAM;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAC,CAA2B;IACxC,OAAO,CAAC,OAAO,CAAC,CAA+B;IAG/C,OAAO,CAAC,KAAK,CAAC,CAAc;IAE5B,YAAY,QAAQ,SAAI,EAAE,QAAQ,SAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAItE;IAED;;;;;;OAMG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIlC;IAED,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAGnC;IAED,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAMtC;IAED,KAAK,IAAI,IAAI,CAGZ;IAED,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAG7C;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,UAAU,GAAG,SAAS,GAAG,IAAI,CAGrD;IAED,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,UAAU;IAmBlB,UAAU,IAAI,IAAI,CAKjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA+G9B;IAED,OAAO,CAAC,OAAO;CAUf","sourcesContent":["import type { Component } from \"../tui.js\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.js\";\n\ntype RenderCache = {\n\tchildLines: string[];\n\twidth: number;\n\tbgSample: string | undefined;\n\tshadowSample: string | undefined;\n\tinset: number;\n\tlines: string[];\n};\n\n/**\n * The paper treatment a box asks its owner for on every frame: the ink of its\n * shadow and the gutter it holds back from the right margin. Both are optional,\n * and a sheet with neither is the plain full-width band.\n */\nexport interface PaperSheet {\n\t/** Paints the shadow's own glyphs. Omitted, the sheet casts no shadow. */\n\tshadow?: (text: string) => string;\n\t/**\n\t * Columns of page held back at the right margin.\n\t *\n\t * A block that runs the full width of the terminal has no right edge to\n\t * show: it is a band of colour between two screen edges, not a sheet on a\n\t * page. Reserving a few columns gives it one, which is what makes both the\n\t * shadow's right-hand column and a visible right edge possible at all.\n\t */\n\tinset?: number;\n}\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\tprivate paperFn?: () => PaperSheet | undefined;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\t/**\n\t * Change the horizontal padding after construction.\n\t *\n\t * For a box whose padding exists to carry a background band: with no band to\n\t * draw, the padding is just an indent that pushes the content out of line\n\t * with everything around it.\n\t */\n\tsetPaddingX(paddingX: number): void {\n\t\tif (this.paddingX === paddingX) return;\n\t\tthis.paddingX = paddingX;\n\t\tthis.invalidateCache();\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output\n\t}\n\n\t/**\n\t * Give the box the paper treatment, resolved on every frame.\n\t *\n\t * A shadow and the gutter it needs arrive together because they are one\n\t * decision, and it is a decision the *theme* makes — so the box asks for it\n\t * at render time rather than being told once. A box built under one theme\n\t * outlives it: the user switches theme with the block already on screen, and\n\t * a shadow function captured at construction would still be painting the old\n\t * theme's ink (or reaching for a colour the new theme never defined).\n\t *\n\t * The shadow itself is one extra row of `▔` — an upper *one-eighth* block,\n\t * which paints a hairline across the top of its cells and so hugs the box's\n\t * bottom edge — indented one column to give the offset a terminal cannot\n\t * give in sub-pixels. An inset box gets a matching column of `▏` down its\n\t * right edge and the bottom run reaches under it, so the two close the\n\t * corner; with no inset the band already owns the last cell, so the bottom\n\t * edge is all there is room for.\n\t *\n\t * Eighths, not halves. `▀` and `▌` are half a cell of solid ink, which at a\n\t * terminal's resolution is not a shadow but a second band of colour wrapped\n\t * around two sides of every message — heavy enough to pull the eye off the\n\t * text it is supposed to sit behind. A shadow's whole job is to be noticed\n\t * without being looked at, and an eighth is as close to that as a cell grid\n\t * gets.\n\t *\n\t * Passing no provider, or one that returns nothing, draws the full-width\n\t * band the box has always drawn.\n\t */\n\tsetPaper(paperFn?: () => PaperSheet | undefined): void {\n\t\tthis.paperFn = paperFn;\n\t\tthis.invalidateCache();\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t}\n\n\tprivate matchCache(\n\t\twidth: number,\n\t\tchildLines: string[],\n\t\tbgSample: string | undefined,\n\t\tshadowSample: string | undefined,\n\t\tinset: number,\n\t): boolean {\n\t\tconst cache = this.cache;\n\t\treturn (\n\t\t\t!!cache &&\n\t\t\tcache.width === width &&\n\t\t\tcache.bgSample === bgSample &&\n\t\t\tcache.shadowSample === shadowSample &&\n\t\t\tcache.inset === inset &&\n\t\t\tcache.childLines.length === childLines.length &&\n\t\t\tcache.childLines.every((line, i) => line === childLines[i])\n\t\t);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// The paper treatment is asked for per frame, so a box already on screen\n\t\t// follows a theme switch instead of holding the treatment it was built\n\t\t// with.\n\t\tconst paper = this.paperFn?.();\n\t\t// A sheet needs a band wide enough to carry its own bottom run. Narrower\n\t\t// than that the gutter has nowhere to go: the treatment degenerated into\n\t\t// a one-column band with a shadow beside it and no run under it. Below\n\t\t// the threshold the box falls back to the plain full-width band a theme\n\t\t// without paper draws, which is the honest answer at that size.\n\t\tconst wide = width - Math.max(0, paper?.inset ?? 0) > 1;\n\t\tconst shadowFn = wide ? paper?.shadow : undefined;\n\t\tconst inset = wide ? Math.max(0, paper?.inset ?? 0) : 0;\n\n\t\t// The band stops short of the right margin when inset, leaving a gutter of\n\t\t// page for the sheet's own edge and the shadow that follows it.\n\t\tconst bandWidth = Math.max(1, width - inset);\n\t\t// Padding only exists to carry the band, so it gives way rather than\n\t\t// pushing content off the end of it. Clamping `contentWidth` alone was\n\t\t// not enough: at a width where the gutter and two columns of padding\n\t\t// leave no room, the row came out wider than the band it was supposed\n\t\t// to fill, the shadow's column went with it, and the line wrapped past\n\t\t// the right margin. Both halves are derived from the band instead.\n\t\tconst paddingX = Math.min(this.paddingX, Math.max(0, Math.floor((bandWidth - 1) / 2)));\n\t\tconst contentWidth = Math.max(1, bandWidth - paddingX * 2);\n\t\tconst leftPad = \" \".repeat(paddingX);\n\n\t\t// Render all children\n\t\tconst childLines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\tif (childLines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\t\tconst shadowSample = shadowFn ? shadowFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childLines, bgSample, shadowSample, inset)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst rows: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\t\trows.push(...childLines);\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\n\t\t// The right-hand column only exists when a gutter was reserved for it.\n\t\tconst hasColumn = shadowFn !== undefined && inset > 0;\n\t\tconst result: string[] = [];\n\t\trows.forEach((line, index) => {\n\t\t\t// Every row of the sheet ends in the same column.\n\t\t\t//\n\t\t\t// The edge used to be nicked one column in on roughly every fifth\n\t\t\t// row, to read as cut by hand rather than ruled. At a terminal's\n\t\t\t// resolution it could not: a nick is a whole cell, which is a step\n\t\t\t// far too coarse to read as the wobble of a pair of scissors, and it\n\t\t\t// landed as damage instead. On the top row — the one row with no\n\t\t\t// shadow behind it, because the offset is down as well as right — it\n\t\t\t// showed as a bite taken out of the sheet's corner. Everywhere else\n\t\t\t// it was backfilled with a block of shadow ink, which put a tooth of\n\t\t\t// shadow *inside* the sheet's own outline. Both are the same mistake\n\t\t\t// seen from two sides: the fill was leaving holes and the shadow was\n\t\t\t// covering for them. A ruled edge has neither.\n\t\t\tconst band = this.applyBg(line, bandWidth);\n\t\t\t// `▏` paints the leftmost eighth of its cell, so the column reads as a\n\t\t\t// hairline hugging the sheet rather than a band beside it. The first row\n\t\t\t// has no column at all: the offset is down *and* right.\n\t\t\tconst column = hasColumn && index > 0 ? shadowFn(\"▏\") : \"\";\n\t\t\tresult.push(band + column);\n\t\t});\n\n\t\t// The shadow's bottom run, offset one column right of the band. It ends\n\t\t// under the right-hand column so the two close the corner; with no\n\t\t// gutter there is no such column, and the run stops one cell short of\n\t\t// the margin instead of wrapping past it.\n\t\t//\n\t\t// That last cell is the column's own glyph, not the run's. The run is a\n\t\t// full-width glyph and the column above it is an eighth of one, so a run\n\t\t// that ended on `▔` overshot the column by seven eighths of a cell and\n\t\t// left a tip poking out past the corner — a stray line coming out of the\n\t\t// shadow. `▏` under `▏` is the same eighth of a column carried down one\n\t\t// more row, which is what closes the corner at the width it is drawn at.\n\t\tif (shadowFn && bandWidth > 1) {\n\t\t\tconst run = \"▔\".repeat(bandWidth - 1);\n\t\t\tresult.push(` ${shadowFn(hasColumn ? `${run}▏` : run)}`);\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = {\n\t\t\tchildLines,\n\t\t\twidth,\n\t\t\tbgSample,\n\t\t\tshadowSample,\n\t\t\tinset,\n\t\t\tlines: result,\n\t\t};\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"box.d.ts","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAY3C;;;;GAIG;AACH,MAAM,WAAW,UAAU;IAC1B,0EAA0E;IAC1E,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAClC;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,qBAAa,GAAI,YAAW,SAAS;IACpC,QAAQ,EAAE,SAAS,EAAE,CAAM;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAC,CAA2B;IACxC,OAAO,CAAC,OAAO,CAAC,CAA+B;IAG/C,OAAO,CAAC,KAAK,CAAC,CAAc;IAE5B,YAAY,QAAQ,SAAI,EAAE,QAAQ,SAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAItE;IAED;;;;;;OAMG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIlC;IAED,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAGnC;IAED,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAMtC;IAED,KAAK,IAAI,IAAI,CAGZ;IAED,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAG7C;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,UAAU,GAAG,SAAS,GAAG,IAAI,CAGrD;IAED,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,UAAU;IAmBlB,UAAU,IAAI,IAAI,CAKjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA+G9B;IAED,OAAO,CAAC,OAAO;CAUf","sourcesContent":["import type { Component } from \"../tui.js\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.js\";\n\ntype RenderCache = {\n\tchildLines: string[];\n\twidth: number;\n\tbgSample: string | undefined;\n\tshadowSample: string | undefined;\n\tinset: number;\n\tlines: string[];\n};\n\n/**\n * The paper treatment a box asks its owner for on every frame: the ink of its\n * shadow and the gutter it holds back from the right margin. Both are optional,\n * and a sheet with neither is the plain full-width band.\n */\nexport interface PaperSheet {\n\t/** Paints the shadow's own glyphs. Omitted, the sheet casts no shadow. */\n\tshadow?: (text: string) => string;\n\t/**\n\t * Columns of page held back at the right margin.\n\t *\n\t * A block that runs the full width of the terminal has no right edge to\n\t * show: it is a band of colour between two screen edges, not a sheet on a\n\t * page. Reserving a few columns gives it one, which is what makes both the\n\t * shadow's right-hand column and a visible right edge possible at all.\n\t */\n\tinset?: number;\n}\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\tprivate paperFn?: () => PaperSheet | undefined;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\t/**\n\t * Change the horizontal padding after construction.\n\t *\n\t * For a box whose padding exists to carry a background band: with no band to\n\t * draw, the padding is just an indent that pushes the content out of line\n\t * with everything around it.\n\t */\n\tsetPaddingX(paddingX: number): void {\n\t\tif (this.paddingX === paddingX) return;\n\t\tthis.paddingX = paddingX;\n\t\tthis.invalidateCache();\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output\n\t}\n\n\t/**\n\t * Give the box the paper treatment, resolved on every frame.\n\t *\n\t * A shadow and the gutter it needs arrive together because they are one\n\t * decision, and it is a decision the *theme* makes — so the box asks for it\n\t * at render time rather than being told once. A box built under one theme\n\t * outlives it: the user switches theme with the block already on screen, and\n\t * a shadow function captured at construction would still be painting the old\n\t * theme's ink (or reaching for a colour the new theme never defined).\n\t *\n\t * The shadow itself is one extra row of `▔` — an upper *one-eighth* block,\n\t * which paints a hairline across the top of its cells and so hugs the box's\n\t * bottom edge — indented one column to give the offset a terminal cannot\n\t * give in sub-pixels. An inset box gets a matching column of `▏` down its\n\t * right edge, and the run stops where that column starts, so the two meet as\n\t * an L; with no inset the band already owns the last cell, so the bottom\n\t * edge is all there is room for.\n\t *\n\t * Eighths, not halves. `▀` and `▌` are half a cell of solid ink, which at a\n\t * terminal's resolution is not a shadow but a second band of colour wrapped\n\t * around two sides of every message — heavy enough to pull the eye off the\n\t * text it is supposed to sit behind. A shadow's whole job is to be noticed\n\t * without being looked at, and an eighth is as close to that as a cell grid\n\t * gets.\n\t *\n\t * Passing no provider, or one that returns nothing, draws the full-width\n\t * band the box has always drawn.\n\t */\n\tsetPaper(paperFn?: () => PaperSheet | undefined): void {\n\t\tthis.paperFn = paperFn;\n\t\tthis.invalidateCache();\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t}\n\n\tprivate matchCache(\n\t\twidth: number,\n\t\tchildLines: string[],\n\t\tbgSample: string | undefined,\n\t\tshadowSample: string | undefined,\n\t\tinset: number,\n\t): boolean {\n\t\tconst cache = this.cache;\n\t\treturn (\n\t\t\t!!cache &&\n\t\t\tcache.width === width &&\n\t\t\tcache.bgSample === bgSample &&\n\t\t\tcache.shadowSample === shadowSample &&\n\t\t\tcache.inset === inset &&\n\t\t\tcache.childLines.length === childLines.length &&\n\t\t\tcache.childLines.every((line, i) => line === childLines[i])\n\t\t);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// The paper treatment is asked for per frame, so a box already on screen\n\t\t// follows a theme switch instead of holding the treatment it was built\n\t\t// with.\n\t\tconst paper = this.paperFn?.();\n\t\t// A sheet needs a band wide enough to carry its own bottom run. Narrower\n\t\t// than that the gutter has nowhere to go: the treatment degenerated into\n\t\t// a one-column band with a shadow beside it and no run under it. Below\n\t\t// the threshold the box falls back to the plain full-width band a theme\n\t\t// without paper draws, which is the honest answer at that size.\n\t\tconst wide = width - Math.max(0, paper?.inset ?? 0) > 1;\n\t\tconst shadowFn = wide ? paper?.shadow : undefined;\n\t\tconst inset = wide ? Math.max(0, paper?.inset ?? 0) : 0;\n\n\t\t// The band stops short of the right margin when inset, leaving a gutter of\n\t\t// page for the sheet's own edge and the shadow that follows it.\n\t\tconst bandWidth = Math.max(1, width - inset);\n\t\t// Padding only exists to carry the band, so it gives way rather than\n\t\t// pushing content off the end of it. Clamping `contentWidth` alone was\n\t\t// not enough: at a width where the gutter and two columns of padding\n\t\t// leave no room, the row came out wider than the band it was supposed\n\t\t// to fill, the shadow's column went with it, and the line wrapped past\n\t\t// the right margin. Both halves are derived from the band instead.\n\t\tconst paddingX = Math.min(this.paddingX, Math.max(0, Math.floor((bandWidth - 1) / 2)));\n\t\tconst contentWidth = Math.max(1, bandWidth - paddingX * 2);\n\t\tconst leftPad = \" \".repeat(paddingX);\n\n\t\t// Render all children\n\t\tconst childLines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\tif (childLines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\t\tconst shadowSample = shadowFn ? shadowFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childLines, bgSample, shadowSample, inset)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst rows: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\t\trows.push(...childLines);\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\n\t\t// The right-hand column only exists when a gutter was reserved for it.\n\t\tconst hasColumn = shadowFn !== undefined && inset > 0;\n\t\tconst result: string[] = [];\n\t\trows.forEach((line, index) => {\n\t\t\t// Every row of the sheet ends in the same column.\n\t\t\t//\n\t\t\t// The edge used to be nicked one column in on roughly every fifth\n\t\t\t// row, to read as cut by hand rather than ruled. At a terminal's\n\t\t\t// resolution it could not: a nick is a whole cell, which is a step\n\t\t\t// far too coarse to read as the wobble of a pair of scissors, and it\n\t\t\t// landed as damage instead. On the top row — the one row with no\n\t\t\t// shadow behind it, because the offset is down as well as right — it\n\t\t\t// showed as a bite taken out of the sheet's corner. Everywhere else\n\t\t\t// it was backfilled with a block of shadow ink, which put a tooth of\n\t\t\t// shadow *inside* the sheet's own outline. Both are the same mistake\n\t\t\t// seen from two sides: the fill was leaving holes and the shadow was\n\t\t\t// covering for them. A ruled edge has neither.\n\t\t\tconst band = this.applyBg(line, bandWidth);\n\t\t\t// `▏` paints the leftmost eighth of its cell, so the column reads as a\n\t\t\t// hairline hugging the sheet rather than a band beside it. The first row\n\t\t\t// has no column at all: the offset is down *and* right.\n\t\t\tconst column = hasColumn && index > 0 ? shadowFn(\"▏\") : \"\";\n\t\t\tresult.push(band + column);\n\t\t});\n\n\t\t// The shadow's bottom run, offset one column right of the band, and\n\t\t// nothing after it — the gutter cell the column occupies stays empty on\n\t\t// this row.\n\t\t//\n\t\t// The corner closes on its own. `▔` paints its cell edge to edge, so a\n\t\t// run of `bandWidth - 1` starting in column 1 ends at exactly the cell\n\t\t// boundary the column's `▏` paints its hairline on: the horizontal leg's\n\t\t// right end abuts the vertical leg's left edge. Carrying `▏` down into\n\t\t// this row to \"close\" it instead overshot the other way — `▏` fills the\n\t\t// full height of its cell while `▔` fills the top eighth of one, so the\n\t\t// glyph hung a whole row below the shadow's bottom edge and the L came\n\t\t// out as a cross with a tick poking under it.\n\t\tif (shadowFn && bandWidth > 1) {\n\t\t\tresult.push(` ${shadowFn(\"▔\".repeat(bandWidth - 1))}`);\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = {\n\t\t\tchildLines,\n\t\t\twidth,\n\t\t\tbgSample,\n\t\t\tshadowSample,\n\t\t\tinset,\n\t\t\tlines: result,\n\t\t};\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
|
package/dist/components/box.js
CHANGED
|
@@ -61,8 +61,8 @@ export class Box {
|
|
|
61
61
|
* which paints a hairline across the top of its cells and so hugs the box's
|
|
62
62
|
* bottom edge — indented one column to give the offset a terminal cannot
|
|
63
63
|
* give in sub-pixels. An inset box gets a matching column of `▏` down its
|
|
64
|
-
* right edge and the
|
|
65
|
-
*
|
|
64
|
+
* right edge, and the run stops where that column starts, so the two meet as
|
|
65
|
+
* an L; with no inset the band already owns the last cell, so the bottom
|
|
66
66
|
* edge is all there is room for.
|
|
67
67
|
*
|
|
68
68
|
* Eighths, not halves. `▀` and `▌` are half a cell of solid ink, which at a
|
|
@@ -175,20 +175,20 @@ export class Box {
|
|
|
175
175
|
const column = hasColumn && index > 0 ? shadowFn("▏") : "";
|
|
176
176
|
result.push(band + column);
|
|
177
177
|
});
|
|
178
|
-
// The shadow's bottom run, offset one column right of the band
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
// the margin instead of wrapping past it.
|
|
178
|
+
// The shadow's bottom run, offset one column right of the band, and
|
|
179
|
+
// nothing after it — the gutter cell the column occupies stays empty on
|
|
180
|
+
// this row.
|
|
182
181
|
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
182
|
+
// The corner closes on its own. `▔` paints its cell edge to edge, so a
|
|
183
|
+
// run of `bandWidth - 1` starting in column 1 ends at exactly the cell
|
|
184
|
+
// boundary the column's `▏` paints its hairline on: the horizontal leg's
|
|
185
|
+
// right end abuts the vertical leg's left edge. Carrying `▏` down into
|
|
186
|
+
// this row to "close" it instead overshot the other way — `▏` fills the
|
|
187
|
+
// full height of its cell while `▔` fills the top eighth of one, so the
|
|
188
|
+
// glyph hung a whole row below the shadow's bottom edge and the L came
|
|
189
|
+
// out as a cross with a tick poking under it.
|
|
189
190
|
if (shadowFn && bandWidth > 1) {
|
|
190
|
-
|
|
191
|
-
result.push(` ${shadowFn(hasColumn ? `${run}▏` : run)}`);
|
|
191
|
+
result.push(` ${shadowFn("▔".repeat(bandWidth - 1))}`);
|
|
192
192
|
}
|
|
193
193
|
// Update cache
|
|
194
194
|
this.cache = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"box.js","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from \"../tui.js\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.js\";\n\ntype RenderCache = {\n\tchildLines: string[];\n\twidth: number;\n\tbgSample: string | undefined;\n\tshadowSample: string | undefined;\n\tinset: number;\n\tlines: string[];\n};\n\n/**\n * The paper treatment a box asks its owner for on every frame: the ink of its\n * shadow and the gutter it holds back from the right margin. Both are optional,\n * and a sheet with neither is the plain full-width band.\n */\nexport interface PaperSheet {\n\t/** Paints the shadow's own glyphs. Omitted, the sheet casts no shadow. */\n\tshadow?: (text: string) => string;\n\t/**\n\t * Columns of page held back at the right margin.\n\t *\n\t * A block that runs the full width of the terminal has no right edge to\n\t * show: it is a band of colour between two screen edges, not a sheet on a\n\t * page. Reserving a few columns gives it one, which is what makes both the\n\t * shadow's right-hand column and a visible right edge possible at all.\n\t */\n\tinset?: number;\n}\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\tprivate paperFn?: () => PaperSheet | undefined;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\t/**\n\t * Change the horizontal padding after construction.\n\t *\n\t * For a box whose padding exists to carry a background band: with no band to\n\t * draw, the padding is just an indent that pushes the content out of line\n\t * with everything around it.\n\t */\n\tsetPaddingX(paddingX: number): void {\n\t\tif (this.paddingX === paddingX) return;\n\t\tthis.paddingX = paddingX;\n\t\tthis.invalidateCache();\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output\n\t}\n\n\t/**\n\t * Give the box the paper treatment, resolved on every frame.\n\t *\n\t * A shadow and the gutter it needs arrive together because they are one\n\t * decision, and it is a decision the *theme* makes — so the box asks for it\n\t * at render time rather than being told once. A box built under one theme\n\t * outlives it: the user switches theme with the block already on screen, and\n\t * a shadow function captured at construction would still be painting the old\n\t * theme's ink (or reaching for a colour the new theme never defined).\n\t *\n\t * The shadow itself is one extra row of `▔` — an upper *one-eighth* block,\n\t * which paints a hairline across the top of its cells and so hugs the box's\n\t * bottom edge — indented one column to give the offset a terminal cannot\n\t * give in sub-pixels. An inset box gets a matching column of `▏` down its\n\t * right edge and the bottom run reaches under it, so the two close the\n\t * corner; with no inset the band already owns the last cell, so the bottom\n\t * edge is all there is room for.\n\t *\n\t * Eighths, not halves. `▀` and `▌` are half a cell of solid ink, which at a\n\t * terminal's resolution is not a shadow but a second band of colour wrapped\n\t * around two sides of every message — heavy enough to pull the eye off the\n\t * text it is supposed to sit behind. A shadow's whole job is to be noticed\n\t * without being looked at, and an eighth is as close to that as a cell grid\n\t * gets.\n\t *\n\t * Passing no provider, or one that returns nothing, draws the full-width\n\t * band the box has always drawn.\n\t */\n\tsetPaper(paperFn?: () => PaperSheet | undefined): void {\n\t\tthis.paperFn = paperFn;\n\t\tthis.invalidateCache();\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t}\n\n\tprivate matchCache(\n\t\twidth: number,\n\t\tchildLines: string[],\n\t\tbgSample: string | undefined,\n\t\tshadowSample: string | undefined,\n\t\tinset: number,\n\t): boolean {\n\t\tconst cache = this.cache;\n\t\treturn (\n\t\t\t!!cache &&\n\t\t\tcache.width === width &&\n\t\t\tcache.bgSample === bgSample &&\n\t\t\tcache.shadowSample === shadowSample &&\n\t\t\tcache.inset === inset &&\n\t\t\tcache.childLines.length === childLines.length &&\n\t\t\tcache.childLines.every((line, i) => line === childLines[i])\n\t\t);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// The paper treatment is asked for per frame, so a box already on screen\n\t\t// follows a theme switch instead of holding the treatment it was built\n\t\t// with.\n\t\tconst paper = this.paperFn?.();\n\t\t// A sheet needs a band wide enough to carry its own bottom run. Narrower\n\t\t// than that the gutter has nowhere to go: the treatment degenerated into\n\t\t// a one-column band with a shadow beside it and no run under it. Below\n\t\t// the threshold the box falls back to the plain full-width band a theme\n\t\t// without paper draws, which is the honest answer at that size.\n\t\tconst wide = width - Math.max(0, paper?.inset ?? 0) > 1;\n\t\tconst shadowFn = wide ? paper?.shadow : undefined;\n\t\tconst inset = wide ? Math.max(0, paper?.inset ?? 0) : 0;\n\n\t\t// The band stops short of the right margin when inset, leaving a gutter of\n\t\t// page for the sheet's own edge and the shadow that follows it.\n\t\tconst bandWidth = Math.max(1, width - inset);\n\t\t// Padding only exists to carry the band, so it gives way rather than\n\t\t// pushing content off the end of it. Clamping `contentWidth` alone was\n\t\t// not enough: at a width where the gutter and two columns of padding\n\t\t// leave no room, the row came out wider than the band it was supposed\n\t\t// to fill, the shadow's column went with it, and the line wrapped past\n\t\t// the right margin. Both halves are derived from the band instead.\n\t\tconst paddingX = Math.min(this.paddingX, Math.max(0, Math.floor((bandWidth - 1) / 2)));\n\t\tconst contentWidth = Math.max(1, bandWidth - paddingX * 2);\n\t\tconst leftPad = \" \".repeat(paddingX);\n\n\t\t// Render all children\n\t\tconst childLines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\tif (childLines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\t\tconst shadowSample = shadowFn ? shadowFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childLines, bgSample, shadowSample, inset)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst rows: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\t\trows.push(...childLines);\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\n\t\t// The right-hand column only exists when a gutter was reserved for it.\n\t\tconst hasColumn = shadowFn !== undefined && inset > 0;\n\t\tconst result: string[] = [];\n\t\trows.forEach((line, index) => {\n\t\t\t// Every row of the sheet ends in the same column.\n\t\t\t//\n\t\t\t// The edge used to be nicked one column in on roughly every fifth\n\t\t\t// row, to read as cut by hand rather than ruled. At a terminal's\n\t\t\t// resolution it could not: a nick is a whole cell, which is a step\n\t\t\t// far too coarse to read as the wobble of a pair of scissors, and it\n\t\t\t// landed as damage instead. On the top row — the one row with no\n\t\t\t// shadow behind it, because the offset is down as well as right — it\n\t\t\t// showed as a bite taken out of the sheet's corner. Everywhere else\n\t\t\t// it was backfilled with a block of shadow ink, which put a tooth of\n\t\t\t// shadow *inside* the sheet's own outline. Both are the same mistake\n\t\t\t// seen from two sides: the fill was leaving holes and the shadow was\n\t\t\t// covering for them. A ruled edge has neither.\n\t\t\tconst band = this.applyBg(line, bandWidth);\n\t\t\t// `▏` paints the leftmost eighth of its cell, so the column reads as a\n\t\t\t// hairline hugging the sheet rather than a band beside it. The first row\n\t\t\t// has no column at all: the offset is down *and* right.\n\t\t\tconst column = hasColumn && index > 0 ? shadowFn(\"▏\") : \"\";\n\t\t\tresult.push(band + column);\n\t\t});\n\n\t\t// The shadow's bottom run, offset one column right of the band. It ends\n\t\t// under the right-hand column so the two close the corner; with no\n\t\t// gutter there is no such column, and the run stops one cell short of\n\t\t// the margin instead of wrapping past it.\n\t\t//\n\t\t// That last cell is the column's own glyph, not the run's. The run is a\n\t\t// full-width glyph and the column above it is an eighth of one, so a run\n\t\t// that ended on `▔` overshot the column by seven eighths of a cell and\n\t\t// left a tip poking out past the corner — a stray line coming out of the\n\t\t// shadow. `▏` under `▏` is the same eighth of a column carried down one\n\t\t// more row, which is what closes the corner at the width it is drawn at.\n\t\tif (shadowFn && bandWidth > 1) {\n\t\t\tconst run = \"▔\".repeat(bandWidth - 1);\n\t\t\tresult.push(` ${shadowFn(hasColumn ? `${run}▏` : run)}`);\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = {\n\t\t\tchildLines,\n\t\t\twidth,\n\t\t\tbgSample,\n\t\t\tshadowSample,\n\t\t\tinset,\n\t\t\tlines: result,\n\t\t};\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"box.js","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from \"../tui.js\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.js\";\n\ntype RenderCache = {\n\tchildLines: string[];\n\twidth: number;\n\tbgSample: string | undefined;\n\tshadowSample: string | undefined;\n\tinset: number;\n\tlines: string[];\n};\n\n/**\n * The paper treatment a box asks its owner for on every frame: the ink of its\n * shadow and the gutter it holds back from the right margin. Both are optional,\n * and a sheet with neither is the plain full-width band.\n */\nexport interface PaperSheet {\n\t/** Paints the shadow's own glyphs. Omitted, the sheet casts no shadow. */\n\tshadow?: (text: string) => string;\n\t/**\n\t * Columns of page held back at the right margin.\n\t *\n\t * A block that runs the full width of the terminal has no right edge to\n\t * show: it is a band of colour between two screen edges, not a sheet on a\n\t * page. Reserving a few columns gives it one, which is what makes both the\n\t * shadow's right-hand column and a visible right edge possible at all.\n\t */\n\tinset?: number;\n}\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\tprivate paperFn?: () => PaperSheet | undefined;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\t/**\n\t * Change the horizontal padding after construction.\n\t *\n\t * For a box whose padding exists to carry a background band: with no band to\n\t * draw, the padding is just an indent that pushes the content out of line\n\t * with everything around it.\n\t */\n\tsetPaddingX(paddingX: number): void {\n\t\tif (this.paddingX === paddingX) return;\n\t\tthis.paddingX = paddingX;\n\t\tthis.invalidateCache();\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output\n\t}\n\n\t/**\n\t * Give the box the paper treatment, resolved on every frame.\n\t *\n\t * A shadow and the gutter it needs arrive together because they are one\n\t * decision, and it is a decision the *theme* makes — so the box asks for it\n\t * at render time rather than being told once. A box built under one theme\n\t * outlives it: the user switches theme with the block already on screen, and\n\t * a shadow function captured at construction would still be painting the old\n\t * theme's ink (or reaching for a colour the new theme never defined).\n\t *\n\t * The shadow itself is one extra row of `▔` — an upper *one-eighth* block,\n\t * which paints a hairline across the top of its cells and so hugs the box's\n\t * bottom edge — indented one column to give the offset a terminal cannot\n\t * give in sub-pixels. An inset box gets a matching column of `▏` down its\n\t * right edge, and the run stops where that column starts, so the two meet as\n\t * an L; with no inset the band already owns the last cell, so the bottom\n\t * edge is all there is room for.\n\t *\n\t * Eighths, not halves. `▀` and `▌` are half a cell of solid ink, which at a\n\t * terminal's resolution is not a shadow but a second band of colour wrapped\n\t * around two sides of every message — heavy enough to pull the eye off the\n\t * text it is supposed to sit behind. A shadow's whole job is to be noticed\n\t * without being looked at, and an eighth is as close to that as a cell grid\n\t * gets.\n\t *\n\t * Passing no provider, or one that returns nothing, draws the full-width\n\t * band the box has always drawn.\n\t */\n\tsetPaper(paperFn?: () => PaperSheet | undefined): void {\n\t\tthis.paperFn = paperFn;\n\t\tthis.invalidateCache();\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t}\n\n\tprivate matchCache(\n\t\twidth: number,\n\t\tchildLines: string[],\n\t\tbgSample: string | undefined,\n\t\tshadowSample: string | undefined,\n\t\tinset: number,\n\t): boolean {\n\t\tconst cache = this.cache;\n\t\treturn (\n\t\t\t!!cache &&\n\t\t\tcache.width === width &&\n\t\t\tcache.bgSample === bgSample &&\n\t\t\tcache.shadowSample === shadowSample &&\n\t\t\tcache.inset === inset &&\n\t\t\tcache.childLines.length === childLines.length &&\n\t\t\tcache.childLines.every((line, i) => line === childLines[i])\n\t\t);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// The paper treatment is asked for per frame, so a box already on screen\n\t\t// follows a theme switch instead of holding the treatment it was built\n\t\t// with.\n\t\tconst paper = this.paperFn?.();\n\t\t// A sheet needs a band wide enough to carry its own bottom run. Narrower\n\t\t// than that the gutter has nowhere to go: the treatment degenerated into\n\t\t// a one-column band with a shadow beside it and no run under it. Below\n\t\t// the threshold the box falls back to the plain full-width band a theme\n\t\t// without paper draws, which is the honest answer at that size.\n\t\tconst wide = width - Math.max(0, paper?.inset ?? 0) > 1;\n\t\tconst shadowFn = wide ? paper?.shadow : undefined;\n\t\tconst inset = wide ? Math.max(0, paper?.inset ?? 0) : 0;\n\n\t\t// The band stops short of the right margin when inset, leaving a gutter of\n\t\t// page for the sheet's own edge and the shadow that follows it.\n\t\tconst bandWidth = Math.max(1, width - inset);\n\t\t// Padding only exists to carry the band, so it gives way rather than\n\t\t// pushing content off the end of it. Clamping `contentWidth` alone was\n\t\t// not enough: at a width where the gutter and two columns of padding\n\t\t// leave no room, the row came out wider than the band it was supposed\n\t\t// to fill, the shadow's column went with it, and the line wrapped past\n\t\t// the right margin. Both halves are derived from the band instead.\n\t\tconst paddingX = Math.min(this.paddingX, Math.max(0, Math.floor((bandWidth - 1) / 2)));\n\t\tconst contentWidth = Math.max(1, bandWidth - paddingX * 2);\n\t\tconst leftPad = \" \".repeat(paddingX);\n\n\t\t// Render all children\n\t\tconst childLines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\tif (childLines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\t\tconst shadowSample = shadowFn ? shadowFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childLines, bgSample, shadowSample, inset)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst rows: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\t\trows.push(...childLines);\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\n\t\t// The right-hand column only exists when a gutter was reserved for it.\n\t\tconst hasColumn = shadowFn !== undefined && inset > 0;\n\t\tconst result: string[] = [];\n\t\trows.forEach((line, index) => {\n\t\t\t// Every row of the sheet ends in the same column.\n\t\t\t//\n\t\t\t// The edge used to be nicked one column in on roughly every fifth\n\t\t\t// row, to read as cut by hand rather than ruled. At a terminal's\n\t\t\t// resolution it could not: a nick is a whole cell, which is a step\n\t\t\t// far too coarse to read as the wobble of a pair of scissors, and it\n\t\t\t// landed as damage instead. On the top row — the one row with no\n\t\t\t// shadow behind it, because the offset is down as well as right — it\n\t\t\t// showed as a bite taken out of the sheet's corner. Everywhere else\n\t\t\t// it was backfilled with a block of shadow ink, which put a tooth of\n\t\t\t// shadow *inside* the sheet's own outline. Both are the same mistake\n\t\t\t// seen from two sides: the fill was leaving holes and the shadow was\n\t\t\t// covering for them. A ruled edge has neither.\n\t\t\tconst band = this.applyBg(line, bandWidth);\n\t\t\t// `▏` paints the leftmost eighth of its cell, so the column reads as a\n\t\t\t// hairline hugging the sheet rather than a band beside it. The first row\n\t\t\t// has no column at all: the offset is down *and* right.\n\t\t\tconst column = hasColumn && index > 0 ? shadowFn(\"▏\") : \"\";\n\t\t\tresult.push(band + column);\n\t\t});\n\n\t\t// The shadow's bottom run, offset one column right of the band, and\n\t\t// nothing after it — the gutter cell the column occupies stays empty on\n\t\t// this row.\n\t\t//\n\t\t// The corner closes on its own. `▔` paints its cell edge to edge, so a\n\t\t// run of `bandWidth - 1` starting in column 1 ends at exactly the cell\n\t\t// boundary the column's `▏` paints its hairline on: the horizontal leg's\n\t\t// right end abuts the vertical leg's left edge. Carrying `▏` down into\n\t\t// this row to \"close\" it instead overshot the other way — `▏` fills the\n\t\t// full height of its cell while `▔` fills the top eighth of one, so the\n\t\t// glyph hung a whole row below the shadow's bottom edge and the L came\n\t\t// out as a cross with a tick poking under it.\n\t\tif (shadowFn && bandWidth > 1) {\n\t\t\tresult.push(` ${shadowFn(\"▔\".repeat(bandWidth - 1))}`);\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = {\n\t\t\tchildLines,\n\t\t\twidth,\n\t\t\tbgSample,\n\t\t\tshadowSample,\n\t\t\tinset,\n\t\t\tlines: result,\n\t\t};\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
|
package/dist/tui.d.ts
CHANGED
|
@@ -233,6 +233,20 @@ export declare class TUI extends Container {
|
|
|
233
233
|
* per-frame full-buffer scan (collectKittyImageIds) is skipped entirely —
|
|
234
234
|
* the common case for a pure-text session. */
|
|
235
235
|
private sawImageLine;
|
|
236
|
+
/**
|
|
237
|
+
* Live kitty image id -> the id the pinned window transmits its own copy
|
|
238
|
+
* under, and the copies currently placed on the alternate screen.
|
|
239
|
+
*
|
|
240
|
+
* The pinned window repaints whole, so a placement from the previous scroll
|
|
241
|
+
* position has to be deleted or it hangs over the new one. Deleting a kitty
|
|
242
|
+
* image deletes *every* placement of it, though, including the one on the
|
|
243
|
+
* normal screen — which the differential frame on the way back out has no
|
|
244
|
+
* reason to repaint, so the picture would simply be gone. The window
|
|
245
|
+
* therefore transmits a second copy under an id of its own and only ever
|
|
246
|
+
* deletes that one.
|
|
247
|
+
*/
|
|
248
|
+
private scrollImageIds;
|
|
249
|
+
private scrollPlacedImages;
|
|
236
250
|
private static readonly EMPTY_KITTY_IDS;
|
|
237
251
|
private previousWidth;
|
|
238
252
|
private previousHeight;
|
|
@@ -627,16 +641,28 @@ export declare class TUI extends Container {
|
|
|
627
641
|
* left it, because `scrollToLive` throws it away rather than resuming from it.
|
|
628
642
|
*/
|
|
629
643
|
private renderScrollView;
|
|
644
|
+
/** Take the pinned window's own copies of the images off the screen. */
|
|
645
|
+
private clearScrollImages;
|
|
646
|
+
/**
|
|
647
|
+
* On the way off the alternate screen: free the copies and the ids with
|
|
648
|
+
* them, while there is still a screen to write to. The live ids are never
|
|
649
|
+
* touched, so the normal screen comes back with its pictures intact.
|
|
650
|
+
*/
|
|
651
|
+
private releaseScrollImages;
|
|
630
652
|
/**
|
|
631
653
|
* One transcript row, ready for the pinned window.
|
|
632
654
|
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
655
|
+
* `row` is where in the window the line lands, which decides whether an image
|
|
656
|
+
* on it can be drawn at all: a picture reaches `imageRowOffset` rows *above*
|
|
657
|
+
* its line, and a terminal will not draw above the first row — it clamps,
|
|
658
|
+
* putting the picture over rows that are not its own and leaving it there.
|
|
659
|
+
* A block hanging off the top of the window is named instead of drawn, so
|
|
660
|
+
* scrolling one into view shows a placeholder until all of it is on screen.
|
|
638
661
|
*/
|
|
639
662
|
private emitScrollLine;
|
|
663
|
+
private static readonly IMAGE_PLACEHOLDER;
|
|
664
|
+
/** An image line in the pinned window: drawn if all of it fits, named if not. */
|
|
665
|
+
private emitScrollImage;
|
|
640
666
|
private doRender;
|
|
641
667
|
/**
|
|
642
668
|
* Build the escape sequence that parks the hardware cursor for this frame.
|