@kolisachint/hoocode-tui 0.5.61 → 0.5.62

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 +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,CA4F9B;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\tconst shadowFn = paper?.shadow;\n\t\tconst inset = Math.max(0, paper?.inset ?? 0);\n\t\tconst cutEdge = 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\tconst contentWidth = Math.max(1, bandWidth - this.paddingX * 2);\n\t\tconst leftPad = \" \".repeat(this.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. A cut row\n\t\t\t// shows its nick as a column of page between sheet and shadow —\n\t\t\t// which is what a nick is — and the shadow stays a single line.\n\t\t\t// The first row has none: the offset is down *and* right.\n\t\t\tconst column = hasColumn && index > 0 ? \" \".repeat(cut) + 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\tif (shadowFn && bandWidth > 1) {\n\t\t\tresult.push(` ${shadowFn(\"▀\".repeat(hasColumn ? bandWidth : 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\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;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"]}
@@ -117,14 +117,27 @@ export class Box {
117
117
  // follows a theme switch instead of holding the treatment it was built
118
118
  // with.
119
119
  const paper = this.paperFn?.();
120
- const shadowFn = paper?.shadow;
121
- const inset = Math.max(0, paper?.inset ?? 0);
122
- const cutEdge = paper?.cutEdge === true;
120
+ // A sheet needs a band wide enough to carry its own bottom run. Narrower
121
+ // than that the gutter has nowhere to go: the treatment degenerated into
122
+ // a one-column band with a shadow beside it and no run under it. Below
123
+ // the threshold the box falls back to the plain full-width band a theme
124
+ // without paper draws, which is the honest answer at that size.
125
+ const wide = width - Math.max(0, paper?.inset ?? 0) > 1;
126
+ const shadowFn = wide ? paper?.shadow : undefined;
127
+ const inset = wide ? Math.max(0, paper?.inset ?? 0) : 0;
128
+ const cutEdge = wide && paper?.cutEdge === true;
123
129
  // The band stops short of the right margin when inset, leaving a gutter of
124
130
  // page for the sheet's own edge and the shadow that follows it.
125
131
  const bandWidth = Math.max(1, width - inset);
126
- const contentWidth = Math.max(1, bandWidth - this.paddingX * 2);
127
- const leftPad = " ".repeat(this.paddingX);
132
+ // Padding only exists to carry the band, so it gives way rather than
133
+ // pushing content off the end of it. Clamping `contentWidth` alone was
134
+ // not enough: at a width where the gutter and two columns of padding
135
+ // leave no room, the row came out wider than the band it was supposed
136
+ // to fill, the shadow's column went with it, and the line wrapped past
137
+ // the right margin. Both halves are derived from the band instead.
138
+ const paddingX = Math.min(this.paddingX, Math.max(0, Math.floor((bandWidth - 1) / 2)));
139
+ const contentWidth = Math.max(1, bandWidth - paddingX * 2);
140
+ const leftPad = " ".repeat(paddingX);
128
141
  // Render all children
129
142
  const childLines = [];
130
143
  for (const child of this.children) {
@@ -167,19 +180,33 @@ export class Box {
167
180
  // front of it. Following the cut was the first attempt and it broke
168
181
  // the shadow: `▌` paints half a cell, so a one-column step leaves no
169
182
  // overlap at all between one row's mark and the next, and what the
170
- // eye gets is a dashed staircase rather than an edge. A cut row
171
- // shows its nick as a column of page between sheet and shadow —
172
- // which is what a nick is and the shadow stays a single line.
173
- // The first row has none: the offset is down *and* right.
174
- const column = hasColumn && index > 0 ? " ".repeat(cut) + shadowFn("▌") : "";
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.
192
+ //
193
+ // The first row has no column at all: the offset is down *and* right.
194
+ const column = hasColumn && index > 0 ? shadowFn(`${"█".repeat(cut)}▌`) : "";
175
195
  result.push(band + column);
176
196
  });
177
197
  // The shadow's bottom run, offset one column right of the band. It ends
178
198
  // under the right-hand column so the two close the corner; with no
179
199
  // gutter there is no such column, and the run stops one cell short of
180
200
  // the margin instead of wrapping past it.
201
+ //
202
+ // That last cell is `▘`, not `▀`. The run is a full-width glyph and the
203
+ // column above it is a half-width one, so a run that ended on `▀`
204
+ // overshot the column by half a cell and left a tip poking out past the
205
+ // corner — a stray line coming out of the shadow. `▘` is the same top
206
+ // half narrowed to the column's own width, so the two edges close flush.
181
207
  if (shadowFn && bandWidth > 1) {
182
- result.push(` ${shadowFn("▀".repeat(hasColumn ? bandWidth : bandWidth - 1))}`);
208
+ const run = "▀".repeat(bandWidth - 1);
209
+ result.push(` ${shadowFn(hasColumn ? `${run}▘` : run)}`);
183
210
  }
184
211
  // Update cache
185
212
  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\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\tconst shadowFn = paper?.shadow;\n\t\tconst inset = Math.max(0, paper?.inset ?? 0);\n\t\tconst cutEdge = 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\tconst contentWidth = Math.max(1, bandWidth - this.paddingX * 2);\n\t\tconst leftPad = \" \".repeat(this.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. A cut row\n\t\t\t// shows its nick as a column of page between sheet and shadow —\n\t\t\t// which is what a nick is — and the shadow stays a single line.\n\t\t\t// The first row has none: the offset is down *and* right.\n\t\t\tconst column = hasColumn && index > 0 ? \" \".repeat(cut) + 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\tif (shadowFn && bandWidth > 1) {\n\t\t\tresult.push(` ${shadowFn(\"▀\".repeat(hasColumn ? bandWidth : 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\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\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-tui",
3
- "version": "0.5.61",
3
+ "version": "0.5.62",
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",