Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -148,11 +148,11 @@ Ratio 1:4.3 focus:rest
████ ████ ┗━━┛ ┗━━┛ ╚══╝ ╚══╝

minimal classic bold
┌──┐ ┌──┐ ▒▒▒ ▒▒▒▒ ▓▓▓ ▓▓▓▓
│ │ │ │ ▒ ▒ ▓ ▓ ▓ ▓
│ │ │ │ ▒▒▒▒▒ ▒▒▒▒▒ ▓▓▓▓▓ ▓▓▓▓▓
└──┘ └──┘ ▒ ▒ ▓ ▓ ▓ ▓
▒▒▒▒▒ ▒▒▒▒▒ ▓▓▓▓▓ ▓▓▓▓▓
┌──┐ ┌──┐ ░░░ ░░░░ ▓▓▓ ▓▓▓▓
│ │ │ │ ░ ░ ▓ ▓ ▓ ▓
│ │ │ │ ░░░░░ ░░░░░ ▓▓▓▓▓ ▓▓▓▓▓
└──┘ └──┘ ░ ░ ▓ ▓ ▓ ▓
░░░░░ ░░░░░ ▓▓▓▓▓ ▓▓▓▓▓
```

- **Themes** — `neon` (default), `amber`, `blue`, `mono`. Set the default with
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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 |

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
8 changes: 4 additions & 4 deletions src/lib/bigfont.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -383,22 +383,22 @@ 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

/**
* 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 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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/schemas/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type ThemeName = z.infer<typeof ThemeNameSchema>;
* - "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([
Expand Down
2 changes: 1 addition & 1 deletion src/tui/views/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -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";
14 changes: 7 additions & 7 deletions test/bigfont.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 █.
// ---------------------------------------------------------------------------

Expand Down Expand Up @@ -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);

Expand All @@ -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("█", ""));
});
});

Expand Down
42 changes: 21 additions & 21 deletions test/session-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
// ---------------------------------------------------------------------------

Expand All @@ -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");
});

Expand Down Expand Up @@ -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);

Expand All @@ -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);
});

Expand Down Expand Up @@ -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);

Expand All @@ -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");
Expand All @@ -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",
Expand All @@ -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");

Expand Down
Loading