@kolisachint/hoocode-tui 0.5.62 → 0.5.64

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,9 +1,8 @@
1
1
  import type { Component } from "../tui.js";
2
2
  /**
3
3
  * The paper treatment a box asks its owner for on every frame: the ink of its
4
- * shadow, the gutter it holds back from the right margin, and whether its right
5
- * edge is cut by hand. Each part is optional, and a sheet with none of them is
6
- * the plain full-width band.
4
+ * shadow and the gutter it holds back from the right margin. Both are optional,
5
+ * and a sheet with neither is the plain full-width band.
7
6
  */
8
7
  export interface PaperSheet {
9
8
  /** Paints the shadow's own glyphs. Omitted, the sheet casts no shadow. */
@@ -14,18 +13,9 @@ export interface PaperSheet {
14
13
  * A block that runs the full width of the terminal has no right edge to
15
14
  * show: it is a band of colour between two screen edges, not a sheet on a
16
15
  * page. Reserving a few columns gives it one, which is what makes both the
17
- * shadow's right-hand column and a cut edge possible at all.
16
+ * shadow's right-hand column and a visible right edge possible at all.
18
17
  */
19
18
  inset?: number;
20
- /**
21
- * Cut the right edge by hand rather than ruling it.
22
- *
23
- * The jitter is one column at most and it only ever eats padding, never
24
- * content, so a cut edge cannot cost a character. It is derived from the row
25
- * index and the block's own content, which keeps it identical between frames
26
- * — an edge that reshuffled on every render would read as noise, not paper.
27
- */
28
- cutEdge?: boolean;
29
19
  }
30
20
  /**
31
21
  * Box component - a container that applies padding and background to all children
@@ -53,7 +43,7 @@ export declare class Box implements Component {
53
43
  /**
54
44
  * Give the box the paper treatment, resolved on every frame.
55
45
  *
56
- * A shadow, a gutter and a cut edge arrive together because they are one
46
+ * A shadow and the gutter it needs arrive together because they are one
57
47
  * decision, and it is a decision the *theme* makes — so the box asks for it
58
48
  * at render time rather than being told once. A box built under one theme
59
49
  * outlives it: the user switches theme with the block already on screen, and
@@ -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;AAuB3C;;;;;GAKG;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;IACf;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB;AAWD;;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;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,UAAU,GAAG,SAAS,GAAG,IAAI,CAGrD;IAED,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,UAAU;IAqBlB,UAAU,IAAI,IAAI,CAKjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAuH9B;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\tcutEdge: boolean;\n\tlines: string[];\n};\n\n/**\n * Roughly one row in this many takes the cut.\n *\n * A coin flip per row is not a cut edge, it is a sawtooth: the eye reads\n * alternating columns as a pattern, which is the opposite of hand-made. Scissors\n * leave a mostly straight line with the occasional nick, so the deviation has to\n * be rare enough to read as an accident.\n */\nconst CUT_EVERY = 5;\n\n/**\n * The paper treatment a box asks its owner for on every frame: the ink of its\n * shadow, the gutter it holds back from the right margin, and whether its right\n * edge is cut by hand. Each part is optional, and a sheet with none of them is\n * 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 cut edge possible at all.\n\t */\n\tinset?: number;\n\t/**\n\t * Cut the right edge by hand rather than ruling it.\n\t *\n\t * The jitter is one column at most and it only ever eats padding, never\n\t * content, so a cut edge cannot cost a character. It is derived from the row\n\t * index and the block's own content, which keeps it identical between frames\n\t * — an edge that reshuffled on every render would read as noise, not paper.\n\t */\n\tcutEdge?: boolean;\n}\n\n/** djb2. Small, stable, and good enough to decide one column of a paper edge. */\nfunction hashString(value: string): number {\n\tlet hash = 5381;\n\tfor (let i = 0; i < value.length; i++) {\n\t\thash = ((hash << 5) + hash + value.charCodeAt(i)) >>> 0;\n\t}\n\treturn hash;\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, a gutter and a cut edge 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 half-block, which\n\t * paints solid colour across the top half 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 * 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\tcutEdge: boolean,\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.cutEdge === cutEdge &&\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\t\tconst cutEdge = wide && paper?.cutEdge === true;\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, cutEdge)) {\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// A cut edge needs a seed that is stable for this block but different\n\t\t// from its neighbours, or every sheet on screen is cut identically.\n\t\tconst seed = cutEdge ? hashString(childLines.join(\"\\n\")) : 0;\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// Never let the cut reach into the content: the jitter comes out of\n\t\t\t// the padding the row was going to draw anyway.\n\t\t\tconst slack = Math.max(0, bandWidth - visibleWidth(line));\n\t\t\tconst cut = cutEdge && slack > 0 && hashString(`${seed}:${index}`) % CUT_EVERY === 0 ? 1 : 0;\n\t\t\tconst rowWidth = bandWidth - cut;\n\t\t\tconst band = this.applyBg(line, rowWidth);\n\t\t\t// The shadow holds one column whatever the cut does to the edge in\n\t\t\t// front of it. Following the cut was the first attempt and it broke\n\t\t\t// the shadow: `▌` paints half a cell, so a one-column step leaves no\n\t\t\t// overlap at all between one row's mark and the next, and what the\n\t\t\t// eye gets is a dashed staircase rather than an edge.\n\t\t\t//\n\t\t\t// The column the cut gives back is inked too, as a full block. A nick\n\t\t\t// is a notch scissors took out of the *sheet*, not a hole punched in\n\t\t\t// the shadow behind it, so what the notch exposes is more shadow.\n\t\t\t// Leaving that column as bare page — which is what it used to be —\n\t\t\t// parked a gutter of paper between the sheet and its own shadow, and\n\t\t\t// a shadow detached from the thing casting it reads as a rendering\n\t\t\t// fault rather than as an edge.\n\t\t\t//\n\t\t\t// The first row has no column at all: the offset is down *and* right.\n\t\t\tconst column = hasColumn && index > 0 ? shadowFn(`${\"█\".repeat(cut)}▌`) : \"\";\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 `▘`, not `▀`. The run is a full-width glyph and the\n\t\t// column above it is a half-width one, so a run that ended on `▀`\n\t\t// overshot the column by half a cell and left a tip poking out past the\n\t\t// corner — a stray line coming out of the shadow. `▘` is the same top\n\t\t// half narrowed to the column's own width, so the two edges close flush.\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\tcutEdge,\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;;;;;;;;;;;;;;;;;;;;OAoBG;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,CA8G9B;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 half-block, which\n\t * paints solid colour across the top half 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 * 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 left half of its cell, so the column reads as a thin\n\t\t\t// line hugging the sheet rather than a second band beside it. The\n\t\t\t// first row 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 `▘`, not `▀`. The run is a full-width glyph and the\n\t\t// column above it is a half-width one, so a run that ended on `▀`\n\t\t// overshot the column by half a cell and left a tip poking out past the\n\t\t// corner — a stray line coming out of the shadow. `▘` is the same top\n\t\t// half narrowed to the column's own width, so the two edges close flush.\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,21 +1,4 @@
1
1
  import { applyBackgroundToLine, visibleWidth } from "../utils.js";
2
- /**
3
- * Roughly one row in this many takes the cut.
4
- *
5
- * A coin flip per row is not a cut edge, it is a sawtooth: the eye reads
6
- * alternating columns as a pattern, which is the opposite of hand-made. Scissors
7
- * leave a mostly straight line with the occasional nick, so the deviation has to
8
- * be rare enough to read as an accident.
9
- */
10
- const CUT_EVERY = 5;
11
- /** djb2. Small, stable, and good enough to decide one column of a paper edge. */
12
- function hashString(value) {
13
- let hash = 5381;
14
- for (let i = 0; i < value.length; i++) {
15
- hash = ((hash << 5) + hash + value.charCodeAt(i)) >>> 0;
16
- }
17
- return hash;
18
- }
19
2
  /**
20
3
  * Box component - a container that applies padding and background to all children
21
4
  */
@@ -67,7 +50,7 @@ export class Box {
67
50
  /**
68
51
  * Give the box the paper treatment, resolved on every frame.
69
52
  *
70
- * A shadow, a gutter and a cut edge arrive together because they are one
53
+ * A shadow and the gutter it needs arrive together because they are one
71
54
  * decision, and it is a decision the *theme* makes — so the box asks for it
72
55
  * at render time rather than being told once. A box built under one theme
73
56
  * outlives it: the user switches theme with the block already on screen, and
@@ -92,14 +75,13 @@ export class Box {
92
75
  invalidateCache() {
93
76
  this.cache = undefined;
94
77
  }
95
- matchCache(width, childLines, bgSample, shadowSample, inset, cutEdge) {
78
+ matchCache(width, childLines, bgSample, shadowSample, inset) {
96
79
  const cache = this.cache;
97
80
  return (!!cache &&
98
81
  cache.width === width &&
99
82
  cache.bgSample === bgSample &&
100
83
  cache.shadowSample === shadowSample &&
101
84
  cache.inset === inset &&
102
- cache.cutEdge === cutEdge &&
103
85
  cache.childLines.length === childLines.length &&
104
86
  cache.childLines.every((line, i) => line === childLines[i]));
105
87
  }
@@ -125,7 +107,6 @@ export class Box {
125
107
  const wide = width - Math.max(0, paper?.inset ?? 0) > 1;
126
108
  const shadowFn = wide ? paper?.shadow : undefined;
127
109
  const inset = wide ? Math.max(0, paper?.inset ?? 0) : 0;
128
- const cutEdge = wide && paper?.cutEdge === true;
129
110
  // The band stops short of the right margin when inset, leaving a gutter of
130
111
  // page for the sheet's own edge and the shadow that follows it.
131
112
  const bandWidth = Math.max(1, width - inset);
@@ -153,7 +134,7 @@ export class Box {
153
134
  const bgSample = this.bgFn ? this.bgFn("test") : undefined;
154
135
  const shadowSample = shadowFn ? shadowFn("test") : undefined;
155
136
  // Check cache validity
156
- if (this.matchCache(width, childLines, bgSample, shadowSample, inset, cutEdge)) {
137
+ if (this.matchCache(width, childLines, bgSample, shadowSample, inset)) {
157
138
  return this.cache.lines;
158
139
  }
159
140
  // Apply background and padding
@@ -163,35 +144,28 @@ export class Box {
163
144
  rows.push(...childLines);
164
145
  for (let i = 0; i < this.paddingY; i++)
165
146
  rows.push("");
166
- // A cut edge needs a seed that is stable for this block but different
167
- // from its neighbours, or every sheet on screen is cut identically.
168
- const seed = cutEdge ? hashString(childLines.join("\n")) : 0;
169
147
  // The right-hand column only exists when a gutter was reserved for it.
170
148
  const hasColumn = shadowFn !== undefined && inset > 0;
171
149
  const result = [];
172
150
  rows.forEach((line, index) => {
173
- // Never let the cut reach into the content: the jitter comes out of
174
- // the padding the row was going to draw anyway.
175
- const slack = Math.max(0, bandWidth - visibleWidth(line));
176
- const cut = cutEdge && slack > 0 && hashString(`${seed}:${index}`) % CUT_EVERY === 0 ? 1 : 0;
177
- const rowWidth = bandWidth - cut;
178
- const band = this.applyBg(line, rowWidth);
179
- // The shadow holds one column whatever the cut does to the edge in
180
- // front of it. Following the cut was the first attempt and it broke
181
- // the shadow: `▌` paints half a cell, so a one-column step leaves no
182
- // overlap at all between one row's mark and the next, and what the
183
- // eye gets is a dashed staircase rather than an edge.
184
- //
185
- // The column the cut gives back is inked too, as a full block. A nick
186
- // is a notch scissors took out of the *sheet*, not a hole punched in
187
- // the shadow behind it, so what the notch exposes is more shadow.
188
- // Leaving that column as bare page — which is what it used to be —
189
- // parked a gutter of paper between the sheet and its own shadow, and
190
- // a shadow detached from the thing casting it reads as a rendering
191
- // fault rather than as an edge.
151
+ // Every row of the sheet ends in the same column.
192
152
  //
193
- // The first row has no column at all: the offset is down *and* right.
194
- const column = hasColumn && index > 0 ? shadowFn(`${"█".repeat(cut)}▌`) : "";
153
+ // The edge used to be nicked one column in on roughly every fifth
154
+ // row, to read as cut by hand rather than ruled. At a terminal's
155
+ // resolution it could not: a nick is a whole cell, which is a step
156
+ // far too coarse to read as the wobble of a pair of scissors, and it
157
+ // landed as damage instead. On the top row — the one row with no
158
+ // shadow behind it, because the offset is down as well as right — it
159
+ // showed as a bite taken out of the sheet's corner. Everywhere else
160
+ // it was backfilled with a block of shadow ink, which put a tooth of
161
+ // shadow *inside* the sheet's own outline. Both are the same mistake
162
+ // seen from two sides: the fill was leaving holes and the shadow was
163
+ // covering for them. A ruled edge has neither.
164
+ const band = this.applyBg(line, bandWidth);
165
+ // `▌` paints the left half of its cell, so the column reads as a thin
166
+ // line hugging the sheet rather than a second band beside it. The
167
+ // first row has no column at all: the offset is down *and* right.
168
+ const column = hasColumn && index > 0 ? shadowFn("▌") : "";
195
169
  result.push(band + column);
196
170
  });
197
171
  // The shadow's bottom run, offset one column right of the band. It ends
@@ -215,7 +189,6 @@ export class Box {
215
189
  bgSample,
216
190
  shadowSample,
217
191
  inset,
218
- cutEdge,
219
192
  lines: result,
220
193
  };
221
194
  return result;
@@ -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\tcutEdge: boolean;\n\tlines: string[];\n};\n\n/**\n * Roughly one row in this many takes the cut.\n *\n * A coin flip per row is not a cut edge, it is a sawtooth: the eye reads\n * alternating columns as a pattern, which is the opposite of hand-made. Scissors\n * leave a mostly straight line with the occasional nick, so the deviation has to\n * be rare enough to read as an accident.\n */\nconst CUT_EVERY = 5;\n\n/**\n * The paper treatment a box asks its owner for on every frame: the ink of its\n * shadow, the gutter it holds back from the right margin, and whether its right\n * edge is cut by hand. Each part is optional, and a sheet with none of them is\n * 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 cut edge possible at all.\n\t */\n\tinset?: number;\n\t/**\n\t * Cut the right edge by hand rather than ruling it.\n\t *\n\t * The jitter is one column at most and it only ever eats padding, never\n\t * content, so a cut edge cannot cost a character. It is derived from the row\n\t * index and the block's own content, which keeps it identical between frames\n\t * — an edge that reshuffled on every render would read as noise, not paper.\n\t */\n\tcutEdge?: boolean;\n}\n\n/** djb2. Small, stable, and good enough to decide one column of a paper edge. */\nfunction hashString(value: string): number {\n\tlet hash = 5381;\n\tfor (let i = 0; i < value.length; i++) {\n\t\thash = ((hash << 5) + hash + value.charCodeAt(i)) >>> 0;\n\t}\n\treturn hash;\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, a gutter and a cut edge 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 half-block, which\n\t * paints solid colour across the top half 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 * 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\tcutEdge: boolean,\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.cutEdge === cutEdge &&\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\t\tconst cutEdge = wide && paper?.cutEdge === true;\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, cutEdge)) {\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// A cut edge needs a seed that is stable for this block but different\n\t\t// from its neighbours, or every sheet on screen is cut identically.\n\t\tconst seed = cutEdge ? hashString(childLines.join(\"\\n\")) : 0;\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// Never let the cut reach into the content: the jitter comes out of\n\t\t\t// the padding the row was going to draw anyway.\n\t\t\tconst slack = Math.max(0, bandWidth - visibleWidth(line));\n\t\t\tconst cut = cutEdge && slack > 0 && hashString(`${seed}:${index}`) % CUT_EVERY === 0 ? 1 : 0;\n\t\t\tconst rowWidth = bandWidth - cut;\n\t\t\tconst band = this.applyBg(line, rowWidth);\n\t\t\t// The shadow holds one column whatever the cut does to the edge in\n\t\t\t// front of it. Following the cut was the first attempt and it broke\n\t\t\t// the shadow: `▌` paints half a cell, so a one-column step leaves no\n\t\t\t// overlap at all between one row's mark and the next, and what the\n\t\t\t// eye gets is a dashed staircase rather than an edge.\n\t\t\t//\n\t\t\t// The column the cut gives back is inked too, as a full block. A nick\n\t\t\t// is a notch scissors took out of the *sheet*, not a hole punched in\n\t\t\t// the shadow behind it, so what the notch exposes is more shadow.\n\t\t\t// Leaving that column as bare page — which is what it used to be —\n\t\t\t// parked a gutter of paper between the sheet and its own shadow, and\n\t\t\t// a shadow detached from the thing casting it reads as a rendering\n\t\t\t// fault rather than as an edge.\n\t\t\t//\n\t\t\t// The first row has no column at all: the offset is down *and* right.\n\t\t\tconst column = hasColumn && index > 0 ? shadowFn(`${\"█\".repeat(cut)}▌`) : \"\";\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 `▘`, not `▀`. The run is a full-width glyph and the\n\t\t// column above it is a half-width one, so a run that ended on `▀`\n\t\t// overshot the column by half a cell and left a tip poking out past the\n\t\t// corner — a stray line coming out of the shadow. `▘` is the same top\n\t\t// half narrowed to the column's own width, so the two edges close flush.\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\tcutEdge,\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 half-block, which\n\t * paints solid colour across the top half 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 * 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 left half of its cell, so the column reads as a thin\n\t\t\t// line hugging the sheet rather than a second band beside it. The\n\t\t\t// first row 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 `▘`, not `▀`. The run is a full-width glyph and the\n\t\t// column above it is a half-width one, so a run that ended on `▀`\n\t\t// overshot the column by half a cell and left a tip poking out past the\n\t\t// corner — a stray line coming out of the shadow. `▘` is the same top\n\t\t// half narrowed to the column's own width, so the two edges close flush.\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-tui",
3
- "version": "0.5.62",
3
+ "version": "0.5.64",
4
4
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",