From 3b3c1e67bb96bebb72c35e08f15c01489d59e8cd Mon Sep 17 00:00:00 2001 From: Eduardo Borjas Date: Fri, 12 Jun 2026 04:58:32 -0600 Subject: [PATCH] =?UTF-8?q?fix:=20v3.5.1=20=E2=80=94=20classic=20lightened?= =?UTF-8?q?=20to=20=E2=96=91=20light=20shade=20for=20distinct=20style=20cy?= =?UTF-8?q?cling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since v3.5.0, classic/bold shared block's exact 5-row × 4-col silhouette, differing only by shade (classic ▒, bold ▓). Both sat visually close to block █, so cycling `d` through the three solid styles showed no observable difference — reported while dogfooding in a minimized window. Lightens classic's fill from ▒ to ░ (U+2591) so the three solid styles read as distinct surfaces — block █ (full) / classic ░ (light) / bold ▓ (dark) — at the SAME locked footprint. At a 5-row × 4-col cell there is only one readable solid digit shape, so the distinction between solid styles must come from fill texture; ░/█/▓ is the widest three-way split. bold and block are unchanged; footprint parity, scaling, and small-window rendering are identical to v3.5.0 (texture-only refinement). Verified: footprint+ink parity and a no-fallback sweep across the window grid both pass; full gate green (typecheck, lint, build, 604 tests). Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 25 ++++++++++++++++++++++- README.md | 19 +++++++++--------- package.json | 2 +- src/lib/bigfont.ts | 8 ++++---- src/schemas/config.ts | 2 +- src/tui/views/session.ts | 2 +- src/version.ts | 2 +- test/bigfont.test.ts | 14 ++++++------- test/session-view.test.ts | 42 +++++++++++++++++++-------------------- 9 files changed, 70 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f86229..78b1bce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,28 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [3.5.1] - 2026-06-12 + +### Changed + +- **`classic`'s fill lightened from `▒` to `░` (U+2591 LIGHT SHADE), making + the three solid styles read as clearly distinct surfaces when cycling with + `d`: block `█` (full) / classic `░` (light shade) / bold `▓` (dark shade).** + At the locked 5-row × 4-col footprint there is only one readable solid digit + shape, so the distinction between solid styles is necessarily by fill texture; + `░` / `█` / `▓` is the strongest three-way split available. `bold` stays `▓` + (unchanged). `block` stays `█` (unchanged). Footprint parity, dimensions, + scaling, and small-window rendering are all identical to v3.5.0 — this is a + texture-only refinement. + +### Fixed + +- **Cycling `d` through `classic`/`bold` no longer looks like no change is + happening.** In v3.5.0, `classic` (`▒`) and `bold` (`▓`) both sat visually + close to `block` (`█`), so alternating among the three solid styles produced + no observable difference in a running session. Lightening `classic` to `░` + opens a clear perceptual gap between all three. + ## [3.5.0] - 2026-06-11 ### Changed @@ -385,7 +407,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.0...HEAD +[Unreleased]: https://github.com/aedneth/flowclock-cli/compare/v3.5.1...HEAD +[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 [3.4.0]: https://github.com/aedneth/flowclock-cli/compare/v3.3.1...v3.4.0 diff --git a/README.md b/README.md index b4e2837..930a772 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ 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** (`▒`), + - **`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** (`▓`), sharing the exact 5-row × 4-col footprint of every other style. @@ -148,11 +148,11 @@ Ratio 1:4.3 focus:rest ████ ████ ┗━━┛ ┗━━┛ ╚══╝ ╚══╝ minimal classic bold - ┌──┐ ┌──┐ ▒▒▒ ▒▒▒▒ ▓▓▓ ▓▓▓▓ - │ │ │ │ ▒ ▒ ▒ ▒ ▓ ▓ ▓ ▓ - │ │ │ │ ▒▒▒▒▒ ▒▒▒▒▒ ▓▓▓▓▓ ▓▓▓▓▓ - └──┘ └──┘ ▒ ▒ ▒ ▒ ▓ ▓ ▓ ▓ - ▒▒▒▒▒ ▒▒▒▒▒ ▓▓▓▓▓ ▓▓▓▓▓ + ┌──┐ ┌──┐ ░░░ ░░░░ ▓▓▓ ▓▓▓▓ + │ │ │ │ ░ ░ ░ ░ ▓ ▓ ▓ ▓ + │ │ │ │ ░░░░░ ░░░░░ ▓▓▓▓▓ ▓▓▓▓▓ + └──┘ └──┘ ░ ░ ░ ░ ▓ ▓ ▓ ▓ + ░░░░░ ░░░░░ ▓▓▓▓▓ ▓▓▓▓▓ ``` - **Themes** — `neon` (default), `amber`, `blue`, `mono`. Set the default with @@ -256,7 +256,7 @@ 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** (`▒`), +- **`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** (`▓`), sharing the exact 5-row × 4-col footprint of every other style. @@ -267,7 +267,7 @@ theme. Both choices are **saved to your config** as your new default — the sam 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 +`block` (`░` light / `▓` heavy), so cycling styles never changes the counter's size or covers the session goal. ### Live session controls @@ -402,7 +402,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` light-shade `░` solid numerals, `bold` heavy-shade `▓` — 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 | @@ -436,6 +436,7 @@ on-disk schema is **v3**; migrations are non-destructive. | **v3.4.0** ✅ | restored the original silhouette `outline` style (hollow double-wall nested-rectangle); added `minimal` (light seven-segment line font); uniform counter scaling so `classic`/`bold` no longer tower over the 5-row fonts in minimized/zen windows | | **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 | | **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 4bd5f79..2606e73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowclock-cli", - "version": "3.5.0", + "version": "3.5.1", "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 b0f9ea3..ef0a1a3 100644 --- a/src/lib/bigfont.ts +++ b/src/lib/bigfont.ts @@ -347,7 +347,7 @@ export function uniformCounterScale( * 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 lighter `▒` / heavier `▓` shade + * `block` full `█`, `classic`/`bold` the light `░` / heavy `▓` shade * weights, and `simple`/`outline`/`minimal` box-drawing line-art. */ interface StyleMetrics { @@ -383,7 +383,7 @@ export function styleWidth(style: DisplayStyle, time: string, scale = 1): number } /** The shade glyph each solid weight inks its filled cells with. */ -const CLASSIC_FILL = "▒"; // light/medium shade — the `classic` weight +const CLASSIC_FILL = "░"; // light shade — the airy `classic` weight (clearly distinct from solid block) const BOLD_FILL = "▓"; // heavy shade — the `bold` weight /** @@ -391,14 +391,14 @@ const BOLD_FILL = "▓"; // heavy shade — the `bold` weight * `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 lighter `▒` / heavier `▓` shade instead of the full `█`. Reusing + * cell with a light `░` / heavy `▓` shade instead of the full `█`. Reusing * `renderBigLines` guarantees the footprint can never drift from block's. */ function renderShaded(time: string, scale: number, fill: string): string[] { return renderBigLines(time, scale).map((line) => line.replaceAll("█", fill)); } -/** Render the `classic` style — solid LIGHT shade (`▒`) terminal numerals. */ +/** Render the `classic` style — solid LIGHT shade (`░`) terminal numerals. */ export function renderClassicLines(time: string, scale = 1): string[] { return renderShaded(time, scale, CLASSIC_FILL); } diff --git a/src/schemas/config.ts b/src/schemas/config.ts index 9cb6500..83576b1 100644 --- a/src/schemas/config.ts +++ b/src/schemas/config.ts @@ -12,7 +12,7 @@ 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 (▒) + * - "classic" solid terminal numerals in a LIGHT shade weight (░) * - "bold" solid terminal numerals in a HEAVY shade weight (▓) */ export const DisplayStyleSchema = z.enum([ diff --git a/src/tui/views/session.ts b/src/tui/views/session.ts index 67378cd..01398b3 100644 --- a/src/tui/views/session.ts +++ b/src/tui/views/session.ts @@ -223,7 +223,7 @@ 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 + // "classic" solid LIGHT shade ░ terminal numerals // "bold" solid HEAVY shade ▓ terminal numerals const rawLines = renderCounter(plan.style, time, scale); counterLines = rawLines.map((line) => { diff --git a/src/version.ts b/src/version.ts index 273f1ea..f701277 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.0"; +export const VERSION = "3.5.1"; diff --git a/test/bigfont.test.ts b/test/bigfont.test.ts index b50c388..adab900 100644 --- a/test/bigfont.test.ts +++ b/test/bigfont.test.ts @@ -290,7 +290,7 @@ describe("renderMinimalLines (light seven-segment style)", () => { // renderClassicLines / renderBoldLines — 5-row shade-weight variants of block // // 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) +// 4-col block footprint and differ only by inking filled cells with ░ (classic) // or ▓ (bold) rather than █. // --------------------------------------------------------------------------- @@ -343,9 +343,9 @@ describe("renderClassicLines / renderBoldLines (5-row shade-weight variants of b } }); - it("classic inks with ▒ (not █), bold inks with ▓ (not █)", () => { + it("classic inks with ░ (not █), bold inks with ▓ (not █)", () => { const c = renderClassicLines("8", 2).join(""); - expect(c).toContain("▒"); + expect(c).toContain("░"); expect(c).not.toContain("█"); expect(/[─│┌┐└┘┃━]/.test(c)).toBe(false); @@ -354,21 +354,21 @@ describe("renderClassicLines / renderBoldLines (5-row shade-weight variants of b expect(b).not.toContain("█"); }); - it("classic line[i] == block line[i] with █→▒ at scale 1 (exact geometry reuse)", () => { + it("classic line[i] == block line[i] with █→░ at scale 1 (exact geometry reuse)", () => { 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("█", "▒")); + expect(classicLines[i]).toBe(blockLine.replaceAll("█", "░")); }); }); - it("classic line[i] == block line[i] with █→▒ at scale 2 (geometry reuse at scale 2)", () => { + 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("█", "▒")); + expect(classicLines[i]).toBe(blockLine.replaceAll("█", "░")); }); }); diff --git a/test/session-view.test.ts b/test/session-view.test.ts index bc3a64d..73f1a7d 100644 --- a/test/session-view.test.ts +++ b/test/session-view.test.ts @@ -54,7 +54,7 @@ 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 (█ ▒ ▓) + * box-drawing), making it reliable for block/classic/bold whose inks (█ ░ ▓) * never appear in panel borders. * * The panel border renders as │...content...│ on each inner row, so we strip @@ -466,7 +466,7 @@ 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 +// ink (░ / ▓) in a minimized window with metadata — never falling back to // block's █ glyphs and never collapsing to a bare text line. // --------------------------------------------------------------------------- @@ -480,22 +480,22 @@ 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 ░ glyphs (its own ink), not █ or 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 its own shade ink ░, NOT block's █. + expect(joined).toContain("░"); // The counter is NOT the collapsed single text line. expect(joined).not.toContain("00:12:34"); }); - it("classic: H12 (tighter) still shows ▒ glyphs + metadata (no fallback to block)", () => { + it("classic: H12 (tighter) still shows ░ glyphs + metadata (no fallback to block)", () => { const joined = mini("classic", 12).join("\n"); expect(joined).toContain("focus"); - expect(joined).toContain("▒"); + expect(joined).toContain("░"); expect(joined).not.toContain("00:12:34"); }); @@ -576,7 +576,7 @@ 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); @@ -585,11 +585,11 @@ describe("v3.5 — classic/bold share block's 5-row footprint (uniform counter)" 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 block). expect(classicCount).toBeGreaterThanOrEqual(1); }); @@ -638,12 +638,12 @@ 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 ── + // ── 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 + // 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. - 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 ░ (own ink, not █) at the same glyph-row footprint as block in a roomy 92×28 window, goal + % present", () => { const w = 92, h = 28; const r = rect(w, h); @@ -654,15 +654,15 @@ 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. + 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 inks with ░ in its glyph rows — must have at least 1. 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"); @@ -671,7 +671,7 @@ describe("v3.5 — classic/bold share block's 5-row footprint (uniform counter)" // ── 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 ░ and NOT █ in glyph rows in a tight 84×17 window, goal present", () => { const r = rect(84, 17); const st = activeState({ displayStyle: "classic", @@ -683,8 +683,8 @@ describe("v3.5 — classic/bold share block's 5-row footprint (uniform counter)" const rows = renderSession(st, r, "neon", false).map(strip); const joined = rows.join("\n"); - // Classic must use its own ▒ ink. - expect(joined).toContain("▒"); + // Classic must use its own ░ ink. + expect(joined).toContain("░"); // Goal must survive. expect(joined).toContain("Guard test");