diff --git a/CHANGELOG.md b/CHANGELOG.md index 78b1bce..4f4c867 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,29 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [3.6.0] - 2026-06-12 + +### Changed + +- **`classic` and `bold` redesigned as NATIVE 5-row fonts with distinct glyph + shapes — no longer the `block` font re-inked with shade characters.** `classic` + now draws cornered, open "rounded-terminal" numerals; `bold` draws heavy-slab + numerals with filled corners. Both are inked with solid `█` and distinguished + from `block` (and from each other) by GLYPH SHAPE and stroke weight, so cycling + the three solid styles with `d` shows a clear, unambiguous change. They keep the + exact 5-row × 4-col `block` footprint, so cycling never shifts the layout and + both render their own glyphs at every window size — including a minimized window + with session metadata — with no fallback to `block` and no towering. + +### Fixed + +- **`classic`/`bold` no longer look almost identical to `block` when cycling.** In + v3.5.0–v3.5.1 the two styles were `block` re-inked with `░` / `▓` shades, which + read too close to the full `█` block (bold especially), so alternating among the + three solid styles produced little observable change. Distinct glyph shapes + replace the shade approach, which was geometrically incapable of differentiating + three solid styles at the locked 5×4 footprint. + ## [3.5.1] - 2026-06-12 ### Changed @@ -407,7 +430,8 @@ ANSI, zero new runtime dependencies, instant cold start. `FLOWCLOCK_CONFIG_DIR` / `FLOWCLOCK_DATA_DIR` overrides. - Color themes: `neon` (default), `amber`, `blue`, `mono`. -[Unreleased]: https://github.com/aedneth/flowclock-cli/compare/v3.5.1...HEAD +[Unreleased]: https://github.com/aedneth/flowclock-cli/compare/v3.6.0...HEAD +[3.6.0]: https://github.com/aedneth/flowclock-cli/compare/v3.5.1...v3.6.0 [3.5.1]: https://github.com/aedneth/flowclock-cli/compare/v3.5.0...v3.5.1 [3.5.0]: https://github.com/aedneth/flowclock-cli/compare/v3.4.1...v3.5.0 [3.4.1]: https://github.com/aedneth/flowclock-cli/compare/v3.4.0...v3.4.1 diff --git a/README.md b/README.md index 930a772..b4356e4 100644 --- a/README.md +++ b/README.md @@ -132,9 +132,10 @@ Ratio 1:4.3 focus:rest `minimal` at every scale, including scale 1 (minimized windows). - **`minimal`** — clean **light** line digits in box-drawing strokes (`┌─┐ │ └─┘`, airy `○` colon). Scales crisply at every size. - - **`classic`** — solid terminal numerals in a **light shade weight** (`░`), - sharing the exact 5-row × 4-col footprint of every other style. - - **`bold`** — solid terminal numerals in a **heavy shade weight** (`▓`), + - **`classic`** — solid **cornered "rounded-terminal" numerals** (a distinct, + stylized glyph shape), sharing the exact 5-row × 4-col footprint of every + other style. + - **`bold`** — solid **heavy-slab numerals** (a distinct, heavier glyph shape), sharing the exact 5-row × 4-col footprint of every other style. The goal sits **centered** above the counter and the focus/break metadata @@ -147,12 +148,12 @@ Ratio 1:4.3 focus:rest █ █ █ █ ┃ ┃ ┃ ┃ ║ ║ ║ ║ ████ ████ ┗━━┛ ┗━━┛ ╚══╝ ╚══╝ - minimal classic bold - ┌──┐ ┌──┐ ░░░ ░░░░ ▓▓▓ ▓▓▓▓ - │ │ │ │ ░ ░ ░ ░ ▓ ▓ ▓ ▓ - │ │ │ │ ░░░░░ ░░░░░ ▓▓▓▓▓ ▓▓▓▓▓ - └──┘ └──┘ ░ ░ ░ ░ ▓ ▓ ▓ ▓ - ░░░░░ ░░░░░ ▓▓▓▓▓ ▓▓▓▓▓ + minimal classic bold + ┌──┐ ┌──┐ ██ ██ ████ ████ + │ │ │ │ █ █ █ ██ █ ██ + │ │ │ │ █ █ █ █ █ ███ + └──┘ └──┘ █ █ █ █ ██ ██ + ██ ███ ████ ████ ``` - **Themes** — `neon` (default), `amber`, `blue`, `mono`. Set the default with @@ -256,9 +257,9 @@ so they stay prominent without overshadowing the metadata: at every scale (including scale 1 / minimized windows). - **`minimal`** — clean **light** line digits in box-drawing strokes (`┌─┐ │`), airy `○` colon. Scales crisply at every size; lighter feel than `simple`. -- **`classic`** — solid terminal numerals in a **light shade weight** (`░`), - sharing the exact 5-row × 4-col footprint of every other style. -- **`bold`** — solid terminal numerals in a **heavy shade weight** (`▓`), +- **`classic`** — solid **cornered "rounded-terminal" numerals** (a distinct, + stylized glyph shape), sharing the exact 5-row × 4-col footprint of every other style. +- **`bold`** — solid **heavy-slab numerals** (a distinct, heavier glyph shape), sharing the exact 5-row × 4-col footprint of every other style. Press **`d`** (or run `display` from the palette) to cycle @@ -266,9 +267,9 @@ Press **`d`** (or run `display` from the palette) to cycle theme. Both choices are **saved to your config** as your new default — the same as running `config set displayStyle classic` / `config set theme neon`. The public default stays `block`; the rest are opt-in alternates. All six styles -share the same 5-row footprint — `classic`/`bold` are shade-weight variants of -`block` (`░` light / `▓` heavy), so cycling styles never changes the counter's -size or covers the session goal. +share the same 5-row footprint — `classic`/`bold` are native distinct-shape +5-row fonts (cornered "rounded-terminal" classic / heavy-slab bold), so cycling +styles never changes the counter's size or covers the session goal. ### Live session controls @@ -402,7 +403,7 @@ Stored at `config.json` in your config dir (`flowclock config path`). Keys: | Key | Default | Meaning | | --- | --- | --- | | `theme` | `neon` | `neon` · `amber` · `blue` · `mono` | -| `displayStyle` | `block` | Counter look: `block` solid (default), `simple` heavy line digits, `outline` hollow silhouette in double-line chars (`╔═╗`) distinct from `minimal`, `minimal` light line digits, `classic` light-shade `░` solid numerals, `bold` heavy-shade `▓` — all share the exact 5-row footprint; cycle live with `d` | +| `displayStyle` | `block` | Counter look: `block` solid (default), `simple` heavy line digits, `outline` hollow silhouette in double-line chars (`╔═╗`) distinct from `minimal`, `minimal` light line digits, `classic` cornered/rounded solid numerals (distinct shape), `bold` heavy-slab solid numerals (distinct shape) — all share the exact 5-row footprint; cycle live with `d` | | `showControls` | `true` | show the controls footer (`--zen` overrides) | | `dailyFocusGoalS` | `14400` | daily focus goal in seconds (drives maximization %) | | `keybindings.{pause,break,category,reset,quit}` | `p` `b` `c` `r` `q` | in-session keys | @@ -437,6 +438,7 @@ on-disk schema is **v3**; migrations are non-destructive. | **v3.4.1** ✅ | `outline` double-line box-drawing chars (`╔═╗`) — distinct from `minimal` at every scale; uniform `classic`/`bold` footprint — height capped to 5-row reference, degrades to `block` in short windows | | **v3.5.0** ✅ | `classic`/`bold` redesigned as 5-row shade weights (`▒`/`▓`) for exact footprint parity with all styles — no more towering, goal covering, or silent fallback to `block` | | **v3.5.1** ✅ | `classic` lightened to `░` shade so block `█` / classic `░` / bold `▓` read as three distinct surfaces while keeping exact 5-row footprint parity | +| **v3.6.0** ✅ | `classic`/`bold` redesigned as native 5-row fonts with distinct glyph shapes (cornered `classic` / heavy-slab `bold`, both solid) — replaces the v3.5.x shade-fill approach so the three solid styles read as clearly distinct while keeping exact footprint parity | | **next** | `flowclock sync` — push `sessions.json` to a self-hosted/cloud endpoint; recurring goals; dashboard filters | | **later** | Per-goal analytics deep-dives, calendar heatmap, Homebrew tap | diff --git a/package.json b/package.json index 2606e73..c9789bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowclock-cli", - "version": "3.5.1", + "version": "3.6.0", "description": "Flowtime count-up terminal timer (HUD, not Pomodoro) with silent session logging — agent-native, like gh and vercel.", "type": "module", "license": "AGPL-3.0-or-later", diff --git a/src/lib/bigfont.ts b/src/lib/bigfont.ts index ef0a1a3..d7b7012 100644 --- a/src/lib/bigfont.ts +++ b/src/lib/bigfont.ts @@ -323,7 +323,7 @@ export function computeSessionScale( /** * Pick the counter scale for `style`. Every display style now shares the exact * 5-row × 4-col `block` footprint (the tall 9-row `classic`/`bold` letterforms - * were retired in favour of 5-row shade weights — see STYLE_METRICS), so a + * were retired in favour of native 5-row letterforms — see STYLE_METRICS), so a * single scale gives ALL styles an identical rendered size. Cycling styles with * `d` therefore never makes the counter jump in size, and no style-specific * "taming" is needed — this is now a thin, style-aware wrapper over @@ -339,16 +339,17 @@ export function uniformCounterScale( } // --------------------------------------------------------------------------- -// Solid shade-weight "classic" / "bold" fonts — terminal numerals +// Native distinct-shape "classic" / "bold" fonts — terminal numerals // --------------------------------------------------------------------------- /** * Per-style glyph geometry. EVERY style shares the original 5-row, 4-wide-digit, * 1-wide-colon footprint, so the reserve-first scaling maths and small-panel - * fallback thresholds are byte-for-byte identical across all six styles. The - * styles differ only in how each filled cell is INKED (see `renderCounter`): - * `block` full `█`, `classic`/`bold` the light `░` / heavy `▓` shade - * weights, and `simple`/`outline`/`minimal` box-drawing line-art. + * fallback thresholds are byte-for-byte identical across all six styles. + * `block`, `classic`, and `bold` are three solid fonts with distinct glyph shapes + * (all inked with `█`): block uses blunt filled rectangles, classic uses cornered/ + * open terminal numerals, and bold uses heavy-slab numerals. `simple`, `outline`, + * and `minimal` use box-drawing line-art and differ by stroke weight and style. */ interface StyleMetrics { rows: number; @@ -382,30 +383,77 @@ export function styleWidth(style: DisplayStyle, time: string, scale = 1): number return w * scale; } -/** The shade glyph each solid weight inks its filled cells with. */ -const CLASSIC_FILL = "░"; // light shade — the airy `classic` weight (clearly distinct from solid block) -const BOLD_FILL = "▓"; // heavy shade — the `bold` weight +// 4-wide × 5-tall CORNERED/OPEN terminal numerals (the `classic` style). A +// stylized, rounded-terminal letterform: notched corners and open interiors give +// it a softer silhouette clearly distinct from block's blunt rectangles, while +// keeping block's exact 5-row × 4-col footprint. Uses only █ so it cell-repeat +// scales as cleanly as the block font. +const CLASSIC_GLYPHS: Record = { + "0": [" ██ ", "█ █", "█ █", "█ █", " ██ "], + "1": [" ██ ", " █ ", " █ ", " █ ", " ███"], + "2": [" ██ ", "█ █", " █ ", " █ ", "████"], + "3": ["███ ", " █", " ██ ", " █", "███ "], + "4": ["█ █", "█ █", " ███", " █", " ██"], + "5": ["████", "█ ", "███ ", " █", "███ "], + "6": [" ██ ", "█ ", "███ ", "█ █", " ██ "], + "7": ["███ ", " █ ", " █ ", " █ ", "█ "], + "8": [" ██ ", "█ █", " ██ ", "█ █", " ██ "], + "9": [" ██ ", "█ █", " ███", " █", " ██ "], + ":": [" ", "█", " ", "█", " "], +}; + +// 4-wide × 5-tall HEAVY-SLAB terminal numerals (the `bold` style). The same +// terminal-numeral family as `classic` but with filled corners and extra interior +// ink, so it reads as a visibly heavier weight than both block and classic — yet +// keeps block's exact 5-row × 4-col footprint and scales cleanly (█ only). +const BOLD_GLYPHS: Record = { + "0": ["████", "██ █", "█ █", "█ ██", "████"], + "1": [" ██ ", "███ ", " ██ ", " ██ ", "████"], + "2": ["████", " ██", "████", "██ ", "████"], + "3": ["████", " ██", " ███", " ██", "████"], + "4": ["█ ██", "█ ██", "████", " ██", " ██"], + "5": ["████", "██ ", "████", " ██", "████"], + "6": ["████", "██ ", "████", "█ ██", "████"], + "7": ["████", " ██", " ██ ", " ██ ", " ██ "], + "8": ["████", "█ ██", "████", "█ ██", "████"], + "9": ["████", "█ ██", "████", " ██", "████"], + ":": [" ", "█", " ", "█", " "], +}; /** - * Render the solid `block` geometry with a different fill glyph. `classic` and - * `bold` are weight variants of the default `block` font: they share its EXACT - * 5-row × 4-col cell grid (so every dimension, scale step and layout reservation - * is byte-for-byte identical to block), and differ only by inking each filled - * cell with a light `░` / heavy `▓` shade instead of the full `█`. Reusing - * `renderBigLines` guarantees the footprint can never drift from block's. + * Render a 5-row solid terminal-numeral font (classic/bold) by integer + * cell-repeat — crisp at every scale, no half-block artefacts and no footprint + * drift. Each digit is 4 cols, the colon 1 col, the gap 1 col (× scale), exactly + * matching block's geometry / `styleWidth`, so the reserve-first scaling maths and + * small-window thresholds are byte-for-byte identical across all six styles. These + * letterforms have DISTINCT shapes from block (cornered `classic`, heavy `bold`), + * so cycling styles with `d` shows a clear change without altering the footprint. */ -function renderShaded(time: string, scale: number, fill: string): string[] { - return renderBigLines(time, scale).map((line) => line.replaceAll("█", fill)); +function renderSolidGlyphs(time: string, scale: number, glyphs: Record): string[] { + const totalRows = BIG_ROWS * scale; + const rows = Array.from({ length: totalRows }, () => ""); + const chars = [...time]; + chars.forEach((ch, idx) => { + const glyph = glyphs[ch] ?? [" ", " ", " ", " ", " "]; + const sep = idx < chars.length - 1 ? " ".repeat(scale) : ""; + for (let r = 0; r < BIG_ROWS; r++) { + const scaledRow = [...(glyph[r] ?? "")].map((c) => c.repeat(scale)).join(""); + for (let s = 0; s < scale; s++) { + rows[r * scale + s] = (rows[r * scale + s] ?? "") + scaledRow + sep; + } + } + }); + return rows; } -/** Render the `classic` style — solid LIGHT shade (`░`) terminal numerals. */ +/** Render the `classic` style — solid cornered/open terminal numerals (distinct shape from block). */ export function renderClassicLines(time: string, scale = 1): string[] { - return renderShaded(time, scale, CLASSIC_FILL); + return renderSolidGlyphs(time, scale, CLASSIC_GLYPHS); } -/** Render the `bold` style — solid HEAVY shade (`▓`) terminal numerals. */ +/** Render the `bold` style — solid heavy-slab terminal numerals (distinct shape from block). */ export function renderBoldLines(time: string, scale = 1): string[] { - return renderShaded(time, scale, BOLD_FILL); + return renderSolidGlyphs(time, scale, BOLD_GLYPHS); } /** Render `time` in the requested display style at the given scale. */ diff --git a/src/schemas/config.ts b/src/schemas/config.ts index 83576b1..f1ce252 100644 --- a/src/schemas/config.ts +++ b/src/schemas/config.ts @@ -12,8 +12,8 @@ export type ThemeName = z.infer; * - "simple" clean heavy box-drawing seven-segment digits * - "outline" hollow DOUBLE-LINE silhouette glyphs (╔═╗ ║ ╚╝ — distinct at every scale) * - "minimal" clean light box-drawing seven-segment digits (airy line font) - * - "classic" solid terminal numerals in a LIGHT shade weight (░) - * - "bold" solid terminal numerals in a HEAVY shade weight (▓) + * - "classic" solid cornered/rounded terminal numerals (distinct shape, native 5-row) + * - "bold" solid heavy-slab terminal numerals (distinct shape, heavier weight) */ export const DisplayStyleSchema = z.enum([ "simple", diff --git a/src/tui/views/session.ts b/src/tui/views/session.ts index 01398b3..39b1612 100644 --- a/src/tui/views/session.ts +++ b/src/tui/views/session.ts @@ -80,7 +80,8 @@ interface CounterPlan { * centered text line. * * EVERY style now shares the exact 5-row × 4-col `block` footprint (`classic`/ - * `bold` are shade-weight variants of block, not the old taller letterforms), so + * `bold` are native 5-row letterforms with distinct glyph shapes — cornered + * `classic` / heavy-slab `bold` — not the old taller 9-row letterforms), so * the requested style renders at every window size the others do. There is no * tall-font case and no fall-back to a different style: cycling styles never * changes the counter's footprint, and classic/bold are never silently swapped @@ -223,8 +224,8 @@ export function renderSession( // "simple" clean heavy box-drawing seven-segment line digits // "outline" double-line box-drawing silhouette digits // "minimal" light box-drawing seven-segment line digits - // "classic" solid LIGHT shade ░ terminal numerals - // "bold" solid HEAVY shade ▓ terminal numerals + // "classic" solid cornered/rounded terminal numerals (distinct shape, █) + // "bold" solid heavy-slab terminal numerals (distinct shape, █) const rawLines = renderCounter(plan.style, time, scale); counterLines = rawLines.map((line) => { const padded = padTo(line, innerW, "center"); diff --git a/src/version.ts b/src/version.ts index f701277..8800a57 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ /** Kept in sync with package.json "version" (asserted by a test). */ -export const VERSION = "3.5.1"; +export const VERSION = "3.6.0"; diff --git a/test/bigfont.test.ts b/test/bigfont.test.ts index adab900..4becdd7 100644 --- a/test/bigfont.test.ts +++ b/test/bigfont.test.ts @@ -287,14 +287,14 @@ describe("renderMinimalLines (light seven-segment style)", () => { }); // --------------------------------------------------------------------------- -// renderClassicLines / renderBoldLines — 5-row shade-weight variants of block +// renderClassicLines / renderBoldLines — native 5-row distinct-shape fonts // -// These are NO LONGER tall 9-row terminal fonts. They share the exact 5-row × -// 4-col block footprint and differ only by inking filled cells with ░ (classic) -// or ▓ (bold) rather than █. +// These are native 5-row fonts with distinct glyph shapes from block (cornered +// classic, heavy-slab bold), both inked solid █, sharing block's exact 5-row × +// 4-col footprint. No shade chars (░ ▒ ▓) — every filled cell is █. // --------------------------------------------------------------------------- -describe("renderClassicLines / renderBoldLines (5-row shade-weight variants of block)", () => { +describe("renderClassicLines / renderBoldLines (native 5-row distinct-shape fonts)", () => { it("classic is 5 rows tall at scale 1 — same as block (styleBaseRows == BIG_ROWS)", () => { expect(renderClassicLines("12:34:56", 1)).toHaveLength(BIG_ROWS); expect(renderClassicLines("0", 3)).toHaveLength(BIG_ROWS * 3); @@ -343,51 +343,36 @@ describe("renderClassicLines / renderBoldLines (5-row shade-weight variants of b } }); - it("classic inks with ░ (not █), bold inks with ▓ (not █)", () => { + it("classic and bold ink with solid █ (no shade chars, no box-drawing)", () => { const c = renderClassicLines("8", 2).join(""); - expect(c).toContain("░"); - expect(c).not.toContain("█"); - expect(/[─│┌┐└┘┃━]/.test(c)).toBe(false); + expect(c).toContain("█"); + expect(c).not.toContain("░"); + expect(c).not.toContain("▒"); + expect(c).not.toContain("▓"); + expect(/[─│┌┐└┘┃━╔╗╚╝║═]/.test(c)).toBe(false); const b = renderBoldLines("8", 2).join(""); - expect(b).toContain("▓"); - expect(b).not.toContain("█"); + expect(b).toContain("█"); + expect(b).not.toContain("░"); + expect(b).not.toContain("▒"); + expect(b).not.toContain("▓"); + expect(/[─│┌┐└┘┃━╔╗╚╝║═]/.test(b)).toBe(false); }); - it("classic line[i] == block line[i] with █→░ at scale 1 (exact geometry reuse)", () => { + it("classic glyphs differ from block glyphs (distinct shape, not a re-inked block)", () => { const t = "12:34:56"; - const blockLines = renderBigLines(t, 1); - const classicLines = renderClassicLines(t, 1); - blockLines.forEach((blockLine, i) => { - expect(classicLines[i]).toBe(blockLine.replaceAll("█", "░")); - }); - }); - - it("classic line[i] == block line[i] with █→░ at scale 2 (geometry reuse at scale 2)", () => { - const t = "12:34:56"; - const blockLines = renderBigLines(t, 2); - const classicLines = renderClassicLines(t, 2); - blockLines.forEach((blockLine, i) => { - expect(classicLines[i]).toBe(blockLine.replaceAll("█", "░")); - }); + // Both use █, so plain inequality proves distinct shape (not just a re-ink). + expect(renderClassicLines(t, 1)).not.toEqual(renderBigLines(t, 1)); + // Spot-check individual digit shapes differ. + expect(renderClassicLines("4", 1)).not.toEqual(renderBigLines("4", 1)); }); - it("bold line[i] == block line[i] with █→▓ at scale 1 (exact geometry reuse)", () => { + it("bold glyphs differ from block glyphs (distinct shape, heavier weight)", () => { const t = "12:34:56"; - const blockLines = renderBigLines(t, 1); - const boldLines = renderBoldLines(t, 1); - blockLines.forEach((blockLine, i) => { - expect(boldLines[i]).toBe(blockLine.replaceAll("█", "▓")); - }); - }); - - it("bold line[i] == block line[i] with █→▓ at scale 2 (geometry reuse at scale 2)", () => { - const t = "12:34:56"; - const blockLines = renderBigLines(t, 2); - const boldLines = renderBoldLines(t, 2); - blockLines.forEach((blockLine, i) => { - expect(boldLines[i]).toBe(blockLine.replaceAll("█", "▓")); - }); + // Both use █, so plain inequality proves distinct shape. + expect(renderBoldLines(t, 1)).not.toEqual(renderBigLines(t, 1)); + // Bold and classic must also differ from each other. + expect(renderBoldLines("1", 1)).not.toEqual(renderClassicLines("1", 1)); }); it("classic and bold are visually distinct from block and from each other", () => { diff --git a/test/session-view.test.ts b/test/session-view.test.ts index 73f1a7d..b3e51c8 100644 --- a/test/session-view.test.ts +++ b/test/session-view.test.ts @@ -54,8 +54,8 @@ function strip(s: string): string { * Count "pure glyph rows": lines where every non-space character (ignoring the * panel border │ that wraps each row) belongs to the given ink set. This correctly * ignores the progress bar (▕██████████▏) and panel borders (single-line - * box-drawing), making it reliable for block/classic/bold whose inks (█ ░ ▓) - * never appear in panel borders. + * box-drawing), making it reliable for block/classic/bold (all ink with solid █) + * whose glyph ink never appears in panel borders. * * The panel border renders as │...content...│ on each inner row, so we strip * the leading and trailing │ before checking the ink constraint. @@ -466,8 +466,9 @@ describe("renderSession — classic & bold display styles", () => { // Minimized window + metadata + NON-zen — the collapse bug (v3.3.1) // // Classic/bold now share block's 5-row footprint, so they render their own -// ink (░ / ▓) in a minimized window with metadata — never falling back to -// block's █ glyphs and never collapsing to a bare text line. +// distinct glyphs (solid █, distinct shapes) in a minimized window with +// metadata — never falling back to block's glyphs and never collapsing to a +// bare text line. // --------------------------------------------------------------------------- describe("renderSession — classic/bold render own ink in a minimized window (no fallback)", () => { @@ -480,39 +481,47 @@ describe("renderSession — classic/bold render own ink in a minimized window (n false, ).map(strip); - it("classic: H14 renders ░ glyphs (its own ink), not █ or bare text", () => { + it("classic: H14 renders its own classic glyphs (distinct from block), not bare text", () => { const rows = mini("classic", 14); const joined = rows.join("\n"); // Metadata still present (the whole point of NON-zen). expect(joined).toContain("focus"); expect(joined).toContain("break"); - // Classic renders its own shade ink ░, NOT block's █. - expect(joined).toContain("░"); + // Classic renders solid █ glyph rows (multi-row big font, not bare text). + expect(glyphRows(rows, ["█"])).toBeGreaterThanOrEqual(1); // The counter is NOT the collapsed single text line. expect(joined).not.toContain("00:12:34"); + // Classic renders its own distinct shape — must differ from block render. + const blockRows = mini("block", 14); + expect(joined).not.toBe(blockRows.join("\n")); }); - it("classic: H12 (tighter) still shows ░ glyphs + metadata (no fallback to block)", () => { - const joined = mini("classic", 12).join("\n"); + it("classic: H12 (tighter) still shows multi-row glyphs + metadata (no fallback to block)", () => { + const rows = mini("classic", 12); + const joined = rows.join("\n"); expect(joined).toContain("focus"); - expect(joined).toContain("░"); + expect(glyphRows(rows, ["█"])).toBeGreaterThanOrEqual(1); expect(joined).not.toContain("00:12:34"); }); - it("bold: H14 renders ▓ glyphs (its own ink), not █ or bare text", () => { + it("bold: H14 renders its own bold glyphs (distinct from block), not bare text", () => { const rows = mini("bold", 14); const joined = rows.join("\n"); expect(joined).toContain("focus"); expect(joined).toContain("break"); - // Bold renders its own shade ink ▓, NOT block's █. - expect(joined).toContain("▓"); + // Bold renders solid █ glyph rows (multi-row big font, not bare text). + expect(glyphRows(rows, ["█"])).toBeGreaterThanOrEqual(1); expect(joined).not.toContain("00:12:34"); + // Bold renders its own distinct shape — must differ from block render. + const blockRows = mini("block", 14); + expect(joined).not.toBe(blockRows.join("\n")); }); - it("bold: H12 (tighter) still shows ▓ glyphs + metadata (no fallback to block)", () => { - const joined = mini("bold", 12).join("\n"); + it("bold: H12 (tighter) still shows multi-row glyphs + metadata (no fallback to block)", () => { + const rows = mini("bold", 12); + const joined = rows.join("\n"); expect(joined).toContain("focus"); - expect(joined).toContain("▓"); + expect(glyphRows(rows, ["█"])).toBeGreaterThanOrEqual(1); expect(joined).not.toContain("00:12:34"); }); @@ -558,7 +567,7 @@ describe("renderSession — minimal display style", () => { // --------------------------------------------------------------------------- // v3.5 — classic/bold share block's footprint (Bug 1 + Bug 2 regression guards -// updated to reflect the new shade-weight design) +// updated to reflect native distinct-shape fonts, both solid █) // --------------------------------------------------------------------------- describe("v3.5 — classic/bold share block's 5-row footprint (uniform counter)", () => { @@ -576,24 +585,26 @@ describe("v3.5 — classic/bold share block's 5-row footprint (uniform counter)" // ── Test A: classic/bold glyph rows EXACTLY EQUAL block's — not just "close" ── - it("Test A — classic: ░ glyph rows EXACTLY equal block's █ glyph rows in a tight 92×17 window", () => { + it("Test A — classic: █ glyph rows EXACTLY equal block's █ glyph rows in a tight 92×17 window", () => { const w = 92, h = 17; const r = rect(w, h); - // Pure glyph rows: every non-space char must be the style's ink. + // Pure glyph rows: every non-space char must be █ (classic now inks solid █). const blockRows = renderSession(state34("block"), r, "neon", false).map(strip); const classicRows = renderSession(state34("classic"), r, "neon", false).map(strip); const blockCount = glyphRows(blockRows, ["█"]); - const classicCount = glyphRows(classicRows, ["░"]); + const classicCount = glyphRows(classicRows, ["█"]); // Exact footprint parity — delta must be 0. expect(classicCount).toBe(blockCount); - // Classic must render at least 1 row of its own ░ ink (no fallback to block). + // Classic must render at least 1 row of its own █ ink (no fallback to bare text). expect(classicCount).toBeGreaterThanOrEqual(1); + // Classic renders a distinct shape — its output must differ from block's. + expect(classicRows.join("\n")).not.toBe(blockRows.join("\n")); }); - it("Test A — bold: ▓ glyph rows EXACTLY equal block's █ glyph rows in a tight 92×17 window", () => { + it("Test A — bold: █ glyph rows EXACTLY equal block's █ glyph rows in a tight 92×17 window", () => { const w = 92, h = 17; const r = rect(w, h); @@ -601,10 +612,12 @@ describe("v3.5 — classic/bold share block's 5-row footprint (uniform counter)" const boldRows = renderSession(state34("bold"), r, "neon", false).map(strip); const blockCount = glyphRows(blockRows, ["█"]); - const boldCount = glyphRows(boldRows, ["▓"]); + const boldCount = glyphRows(boldRows, ["█"]); expect(boldCount).toBe(blockCount); expect(boldCount).toBeGreaterThanOrEqual(1); + // Bold renders a distinct shape — its output must differ from block's. + expect(boldRows.join("\n")).not.toBe(blockRows.join("\n")); }); // ── Test B: goal shown consistently across ALL six styles ── @@ -638,12 +651,13 @@ describe("v3.5 — classic/bold share block's 5-row footprint (uniform counter)" expect(/[╔║╚╗╝═]/.test(minimalJoined)).toBe(false); }); - // ── Test D: roomy window — classic renders ░ (own ink) at same footprint as block ── - // REPLACED: classic is now a 5-row shade font, never "tall" (> 5 glyph rows). - // This test verifies classic renders its own ░ ink (not █) at the same glyph-row - // footprint as block in a roomy 92×28 window, with goal + % visible. + // ── Test D: roomy window — classic renders own distinct glyphs at same footprint as block ── + // Classic is now a native 5-row distinct-shape font inked with solid █ (no shade chars). + // This test verifies classic renders its own solid-█ glyphs at the same glyph-row + // footprint as block in a roomy 92×28 window, with goal + % visible, and that + // classic's output is DISTINCT from block's (proving it's not literally block). - it("Test D — classic renders ░ (own ink, not █) at the same glyph-row footprint as block in a roomy 92×28 window, goal + % present", () => { + it("Test D — classic renders solid █ glyphs (distinct from block) at the same glyph-row footprint in a roomy 92×28 window, goal + % present", () => { const w = 92, h = 28; const r = rect(w, h); @@ -654,24 +668,27 @@ describe("v3.5 — classic/bold share block's 5-row footprint (uniform counter)" const blockStripped = blockRendered.map(strip); const classicJoined = classicStripped.join("\n"); - // Glyph row parity: classic's ░ rows == block's █ rows. - const classicGlyphCount = glyphRows(classicStripped, ["░"]); + // Glyph row parity: classic's █ rows == block's █ rows (same footprint). + const classicGlyphCount = glyphRows(classicStripped, ["█"]); const blockGlyphCount = glyphRows(blockStripped, ["█"]); expect(classicGlyphCount).toBe(blockGlyphCount); - // Classic inks with ░ in its glyph rows — must have at least 1. + // Classic must render at least 1 solid-█ glyph row. expect(classicGlyphCount).toBeGreaterThanOrEqual(1); - // Joined content of classic's output must contain ░. - expect(classicJoined).toContain("░"); + // Joined content of classic's output must contain █. + expect(classicJoined).toContain("█"); // Goal and focus-% metadata must both survive alongside the counter. expect(classicJoined).toContain("Deep work"); expect(classicJoined).toContain("%"); + + // Classic renders its own distinct shape — output must differ from block. + expect(classicJoined).not.toBe(blockStripped.join("\n")); }); // ── NEW guard: tight minimized window — classic/bold use their own ink, never fall back to block ── - it("NEW — classic renders ░ and NOT █ in glyph rows in a tight 84×17 window, goal present", () => { + it("NEW — classic renders its own distinct solid-█ glyphs (not block) in a tight 84×17 window, goal present", () => { const r = rect(84, 17); const st = activeState({ displayStyle: "classic", @@ -680,23 +697,19 @@ describe("v3.5 — classic/bold share block's 5-row footprint (uniform counter)" breakBudgetS: 300, zen: false, }); - const rows = renderSession(st, r, "neon", false).map(strip); - const joined = rows.join("\n"); + const classicRows = renderSession(st, r, "neon", false).map(strip); + const blockRows = renderSession({ ...st, displayStyle: "block" }, r, "neon", false).map(strip); + const joined = classicRows.join("\n"); - // Classic must use its own ░ ink. - expect(joined).toContain("░"); + // Classic must render solid █ glyph rows (multi-row big font, not bare text). + expect(glyphRows(classicRows, ["█"])).toBeGreaterThanOrEqual(1); // Goal must survive. expect(joined).toContain("Guard test"); - - // No pure glyph row should contain █ — classic never falls back to block ink. - const blockGlyphRows = rows.filter((l) => { - const t = l.trim(); - return t && [...t].every((ch) => ch === " " || ch === "█"); - }); - expect(blockGlyphRows).toHaveLength(0); + // Classic renders its own distinct shape — must differ from block render. + expect(joined).not.toBe(blockRows.join("\n")); }); - it("NEW — bold renders ▓ and NOT █ in glyph rows in a tight 84×17 window, goal present", () => { + it("NEW — bold renders its own distinct solid-█ glyphs (not block) in a tight 84×17 window, goal present", () => { const r = rect(84, 17); const st = activeState({ displayStyle: "bold", @@ -705,19 +718,15 @@ describe("v3.5 — classic/bold share block's 5-row footprint (uniform counter)" breakBudgetS: 300, zen: false, }); - const rows = renderSession(st, r, "neon", false).map(strip); - const joined = rows.join("\n"); + const boldRows = renderSession(st, r, "neon", false).map(strip); + const blockRows = renderSession({ ...st, displayStyle: "block" }, r, "neon", false).map(strip); + const joined = boldRows.join("\n"); - // Bold must use its own ▓ ink. - expect(joined).toContain("▓"); + // Bold must render solid █ glyph rows (multi-row big font, not bare text). + expect(glyphRows(boldRows, ["█"])).toBeGreaterThanOrEqual(1); // Goal must survive. expect(joined).toContain("Guard test"); - - // No pure glyph row should contain █ — bold never falls back to block ink. - const blockGlyphRows = rows.filter((l) => { - const t = l.trim(); - return t && [...t].every((ch) => ch === " " || ch === "█"); - }); - expect(blockGlyphRows).toHaveLength(0); + // Bold renders its own distinct shape — must differ from block render. + expect(joined).not.toBe(blockRows.join("\n")); }); });