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
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [3.5.0] - 2026-06-11

### Changed

- **`classic` and `bold` redesigned as 5-row shade-weight variants of `block`
(light `▒` / heavy `▓`), sharing the exact 5-row × 4-col footprint of every
other style.** They previously rendered as taller 9-row letterforms.

### Fixed

- **In a minimized, non-zen window with session metadata, `classic`/`bold` no
longer tower over the other styles, cover the session goal, or silently fall
back to the `block` style** — every style now occupies an identical footprint
and renders its own glyphs at every window size.

## [3.4.1] - 2026-06-11

### Fixed
Expand Down Expand Up @@ -370,7 +385,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.4.1...HEAD
[Unreleased]: https://github.com/aedneth/flowclock-cli/compare/v3.5.0...HEAD
[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
[3.3.1]: https://github.com/aedneth/flowclock-cli/compare/v3.3.0...v3.3.1
Expand Down
34 changes: 19 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`** — tall, solid, **terminal-style numerals** that recreate a
familiar monospace clock (light weight).
- **`bold`** — the `classic` letterforms with heavier strokes.
- **`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.

The goal sits **centered** above the counter and the focus/break metadata
centered below it. Override the standalone HUD per session with `start --big`.
Expand All @@ -147,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 @@ -255,17 +256,19 @@ 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`** — tall, solid, **terminal-style numerals** (light weight) — a
familiar monospace clock.
- **`bold`** — the `classic` letterforms with heavier strokes.
- **`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.

Press **`d`** (or run `display` from the palette) to cycle
`block → simple → outline → minimal → classic → bold`, and **`t`** to cycle the
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. (`classic`/`bold`
are taller 9-row fonts scaled to the same rendered footprint as the 5-row fonts,
so cycling styles no longer changes the counter's overall size.)
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.

### Live session controls

Expand Down Expand Up @@ -399,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` tall solid terminal numerals, or `bold` heavier — all same reserve-first scaling; 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 @@ -432,6 +435,7 @@ on-disk schema is **v3**; migrations are non-destructive.
| **v3.3.1** ✅ | `outline` redrawn as a light seven-segment font (no more garbling at large scale); `classic`/`bold` degrade gracefully (drop goal line → block font) instead of collapsing to a text clock in a minimized window with metadata |
| **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` |
| **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.4.1",
"version": "3.5.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",
Expand Down
121 changes: 32 additions & 89 deletions src/lib/bigfont.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,50 +321,34 @@ export function computeSessionScale(
}

/**
* Pick a scale that gives every style a CONSISTENT rendered footprint, so the
* counter does not jump in size when cycling display styles with `d`.
*
* The problem: the tall (9-row) `classic`/`bold` fonts, scaled independently to
* fill the area, rendered ~1.8× taller than the 5-row fonts at the same window
* size — towering over them and even crowding out the goal line. Here the 5-row
* `block` font is the reference: we scale every style toward the SAME target
* rendered height (block's), so all styles occupy roughly the same vertical
* footprint. 5-row styles are unaffected (they already match block exactly);
* the tall fonts pick the LARGEST integer scale whose height does not exceed
* the reference, so they never overshoot the line fonts or crowd the goal line.
* 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
* 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
* `computeSessionScale` kept for a stable call site and clear intent.
*/
export function uniformCounterScale(
areaCols: number,
areaRows: number,
time: string,
style: DisplayStyle,
): number {
// Reference: how tall the 5-row block font renders in this area.
const refScale = computeSessionScale(areaCols, areaRows, time, { style: "block" });
const refHeight = refScale * styleBaseRows("block");

// Largest scale that physically fits this style in the area.
const natural = computeSessionScale(areaCols, areaRows, time, { style });

// 5-row styles already match the reference; only the tall fonts need taming.
if (styleBaseRows(style) === styleBaseRows("block")) return natural;

// Scale the tall font to the LARGEST integer height that does NOT exceed the
// reference (floor, not round) — so classic/bold never tower over the line
// fonts or crowd the goal line — then clamp to what actually fits the area.
const target = Math.max(1, Math.floor(refHeight / styleBaseRows(style)));
return Math.min(natural, target);
return computeSessionScale(areaCols, areaRows, time, { style });
}

// ---------------------------------------------------------------------------
// Tall solid "classic" / "bold" fonts — terminal-style numerals
// Solid shade-weight "classic" / "bold" fonts — terminal numerals
// ---------------------------------------------------------------------------

/**
* Per-style glyph geometry. `block`/`simple`/`outline`/`minimal` share the
* original 5-row, 4-wide-digit footprint; `classic`/`bold` are taller (9 rows)
* solid letterforms. Threading these metrics through the scaling maths keeps the
* reserve-first layout exact for every style.
* 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 lighter `▒` / heavier `▓` shade
* weights, and `simple`/`outline`/`minimal` box-drawing line-art.
*/
interface StyleMetrics {
rows: number;
Expand All @@ -377,8 +361,8 @@ const STYLE_METRICS: Record<DisplayStyle, StyleMetrics> = {
simple: { rows: BIG_ROWS, digitW: 4, colonW: 1 },
outline: { rows: BIG_ROWS, digitW: 4, colonW: 1 },
minimal: { rows: BIG_ROWS, digitW: 4, colonW: 1 },
classic: { rows: 9, digitW: 5, colonW: 1 },
bold: { rows: 9, digitW: 6, colonW: 2 },
classic: { rows: BIG_ROWS, digitW: 4, colonW: 1 },
bold: { rows: BIG_ROWS, digitW: 4, colonW: 1 },
};

/** Number of text rows the style occupies at scale 1. */
Expand All @@ -398,71 +382,30 @@ export function styleWidth(style: DisplayStyle, time: string, scale = 1): number
return w * scale;
}

// 5-wide × 9-tall LIGHT letterforms (the `classic` style).
const CLASSIC_GLYPHS: Record<string, string[]> = {
"0": [" ███ ", "█ █", "█ █", "█ █", "█ █", "█ █", "█ █", "█ █", " ███ "],
"1": [" █ ", " ██ ", " █ ", " █ ", " █ ", " █ ", " █ ", " █ ", " ███ "],
"2": [" ███ ", "█ █", " █", " █", " █ ", " █ ", " █ ", "█ ", "█████"],
"3": [" ███ ", "█ █", " █", " ██ ", " █", " █", " █", "█ █", " ███ "],
"4": [" █ ", " ██ ", " █ █ ", "█ █ ", "█████", " █ ", " █ ", " █ ", " █ "],
"5": ["█████", "█ ", "█ ", "████ ", " █", " █", " █", "█ █", " ███ "],
"6": [" ███ ", "█ █", "█ ", "█ ", "████ ", "█ █", "█ █", "█ █", " ███ "],
"7": ["█████", " █", " █ ", " █ ", " █ ", " █ ", " █ ", " █ ", " █ "],
"8": [" ███ ", "█ █", "█ █", "█ █", " ███ ", "█ █", "█ █", "█ █", " ███ "],
"9": [" ███ ", "█ █", "█ █", "█ █", " ████", " █", " █", "█ █", " ███ "],
":": [" ", " ", "█", " ", " ", " ", "█", " ", " "],
};

// 6-wide × 9-tall HEAVY letterforms (the `bold` style).
const BOLD_GLYPHS: Record<string, string[]> = {
"0": [" ████ ", "██ ██", "██ ██", "██ ██", "██ ██", "██ ██", "██ ██", "██ ██", " ████ "],
"1": [" ██ ", " ███ ", " ██ ", " ██ ", " ██ ", " ██ ", " ██ ", " ██ ", " ████ "],
"2": [" ████ ", "██ ██", " ██", " ██ ", " ██ ", " ██ ", "██ ", "██ ", "██████"],
"3": [" ████ ", "██ ██", " ██", " ███ ", " ██", " ██", " ██", "██ ██", " ████ "],
"4": [" ██ ", " ███ ", " ████ ", "██ ██ ", "██████", " ██ ", " ██ ", " ██ ", " ██ "],
"5": ["██████", "██ ", "██ ", "█████ ", " ██", " ██", " ██", "██ ██", " ████ "],
"6": [" ████ ", "██ ██", "██ ", "██ ", "█████ ", "██ ██", "██ ██", "██ ██", " ████ "],
"7": ["██████", " ██", " ██ ", " ██ ", " ██ ", " ██ ", " ██ ", " ██ ", " ██ "],
"8": [" ████ ", "██ ██", "██ ██", "██ ██", " ████ ", "██ ██", "██ ██", "██ ██", " ████ "],
"9": [" ████ ", "██ ██", "██ ██", "██ ██", " █████", " ██", " ██", "██ ██", " ████ "],
":": [" ", " ", "██", "██", " ", "██", "██", " ", " "],
};
/** The shade glyph each solid weight inks its filled cells with. */
const CLASSIC_FILL = "▒"; // light/medium shade — the `classic` weight
const BOLD_FILL = "▓"; // heavy shade — the `bold` weight

/**
* Render a 9-row solid-letterform font (classic/bold) by integer cell-repeat —
* crisp at every scale, no half-block artefacts. Width parity with
* `styleWidth` is guaranteed by the glyph tables matching STYLE_METRICS.
* 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
* `renderBigLines` guarantees the footprint can never drift from block's.
*/
function renderTallSolid(
time: string,
scale: number,
glyphs: Record<string, string[]>,
rows: number,
digitW: number,
): string[] {
const H = rows * scale;
const out = Array.from({ length: H }, () => "");
const chars = [...time];
const blank = Array.from({ length: rows }, () => " ".repeat(digitW));
chars.forEach((ch, idx) => {
const g = glyphs[ch] ?? blank;
const sep = idx < chars.length - 1 ? " ".repeat(scale) : "";
for (let r = 0; r < rows; r++) {
const line = [...(g[r] ?? "")].map((c) => c.repeat(scale)).join("");
for (let s = 0; s < scale; s++) out[r * scale + s] += line + sep;
}
});
return out;
function renderShaded(time: string, scale: number, fill: string): string[] {
return renderBigLines(time, scale).map((line) => line.replaceAll("█", fill));
}

/** Render the `classic` style — tall LIGHT solid terminal numerals. */
/** Render the `classic` style — solid LIGHT shade (`▒`) terminal numerals. */
export function renderClassicLines(time: string, scale = 1): string[] {
return renderTallSolid(time, scale, CLASSIC_GLYPHS, STYLE_METRICS.classic.rows, STYLE_METRICS.classic.digitW);
return renderShaded(time, scale, CLASSIC_FILL);
}

/** Render the `bold` style — tall HEAVY solid terminal numerals. */
/** Render the `bold` style — solid HEAVY shade (`▓`) terminal numerals. */
export function renderBoldLines(time: string, scale = 1): string[] {
return renderTallSolid(time, scale, BOLD_GLYPHS, STYLE_METRICS.bold.rows, STYLE_METRICS.bold.digitW);
return renderShaded(time, scale, BOLD_FILL);
}

/** Render `time` in the requested display style at the given scale. */
Expand Down
4 changes: 2 additions & 2 deletions src/schemas/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ 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" tall solid terminal-style numerals (light weight)
* - "bold" tall solid terminal-style numerals (heavy weight)
* - "classic" solid terminal numerals in a LIGHT shade weight (▒)
* - "bold" solid terminal numerals in a HEAVY shade weight (▓)
*/
export const DisplayStyleSchema = z.enum([
"simple",
Expand Down
Loading
Loading