@timmy6942025/cli-timer 1.1.8 → 1.1.9

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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/src/index.js +103 -14
package/README.md CHANGED
@@ -76,6 +76,7 @@ timer style <font>
76
76
  ```
77
77
 
78
78
  Replace `<font>` with any font name from the list shown by `timer style`.
79
+ If a font is missing native timer digits, CLI Timer now substitutes close glyphs so the display stays stylized instead of falling back to plain text.
79
80
 
80
81
  ## Settings UI
81
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmy6942025/cli-timer",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
4
4
  "description": "Simple customizable terminal timer and stopwatch",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -12,6 +12,19 @@ const SETTINGS_STATE_PATH = path.join(CONFIG_DIR, "settings-state.json");
12
12
  const DEFAULT_FONT = "Standard";
13
13
  const TIMER_SAMPLE_TEXT = "01:23:45";
14
14
  const MIN_FIGLET_WIDTH = 120;
15
+ const TIME_CHAR_FALLBACKS = Object.freeze({
16
+ "0": Object.freeze(["0", "O", "o", "Q", "D", "U", "X"]),
17
+ "1": Object.freeze(["1", "I", "l", "|", "!", "T", "X"]),
18
+ "2": Object.freeze(["2", "Z", "z", "S", "s", "X"]),
19
+ "3": Object.freeze(["3", "E", "e", "B", "b", "X"]),
20
+ "4": Object.freeze(["4", "A", "a", "H", "h", "X"]),
21
+ "5": Object.freeze(["5", "S", "s", "$", "X"]),
22
+ "6": Object.freeze(["6", "G", "g", "b", "X"]),
23
+ "7": Object.freeze(["7", "T", "t", "Y", "y", "X"]),
24
+ "8": Object.freeze(["8", "B", "b", "X"]),
25
+ "9": Object.freeze(["9", "g", "q", "P", "p", "X"]),
26
+ ":": Object.freeze([":", "|", "!", "i", "I", ".", ";", "X"])
27
+ });
15
28
 
16
29
  const MIN_TICK_RATE_MS = 50;
17
30
  const MAX_TICK_RATE_MS = 1000;
@@ -48,6 +61,7 @@ const DEFAULT_CONFIG = Object.freeze({
48
61
 
49
62
  let allFontsCache = null;
50
63
  let compatibleFontsSlowCache = null;
64
+ const glyphCache = new Map();
51
65
 
52
66
  function clearScreen() {
53
67
  process.stdout.write("\x1b[2J\x1b[H");
@@ -86,6 +100,21 @@ function hasVisibleGlyphs(text) {
86
100
  return typeof text === "string" && /[^\s]/.test(text);
87
101
  }
88
102
 
103
+ function significantLines(text) {
104
+ const lines = toDisplayLines(text).map((line) => line.trim());
105
+ return lines.filter((line) => line.length > 0);
106
+ }
107
+
108
+ function isPlainTimerRender(rendered, timeText) {
109
+ const lines = significantLines(rendered);
110
+ return lines.length === 1 && lines[0] === String(timeText).trim();
111
+ }
112
+
113
+ function isPlainGlyphRender(rendered, token) {
114
+ const lines = significantLines(rendered);
115
+ return lines.length === 1 && lines[0] === String(token).trim();
116
+ }
117
+
89
118
  function renderWithFont(text, fontName) {
90
119
  const width = Number.isFinite(process.stdout.columns)
91
120
  ? Math.max(MIN_FIGLET_WIDTH, Math.floor(process.stdout.columns))
@@ -104,8 +133,66 @@ function renderWithFont(text, fontName) {
104
133
  }
105
134
  }
106
135
 
136
+ function glyphCacheKey(fontName, token) {
137
+ return `${fontName}\u0000${token}`;
138
+ }
139
+
140
+ function getRenderableGlyph(fontName, token) {
141
+ const cacheKey = glyphCacheKey(fontName, token);
142
+ if (glyphCache.has(cacheKey)) {
143
+ return glyphCache.get(cacheKey);
144
+ }
145
+
146
+ const options = TIME_CHAR_FALLBACKS[token] || [token];
147
+ for (const candidate of options) {
148
+ const rendered = renderWithFont(candidate, fontName);
149
+ if (hasVisibleGlyphs(rendered) && !isPlainGlyphRender(rendered, candidate)) {
150
+ const lines = toDisplayLines(rendered);
151
+ const width = lines.reduce((max, line) => Math.max(max, line.length), 0);
152
+ const glyph = { lines, width };
153
+ glyphCache.set(cacheKey, glyph);
154
+ return glyph;
155
+ }
156
+ }
157
+
158
+ glyphCache.set(cacheKey, null);
159
+ return null;
160
+ }
161
+
162
+ function renderTimeByGlyphs(timeText, fontName) {
163
+ const glyphs = [];
164
+ for (const token of String(timeText)) {
165
+ const preferred = getRenderableGlyph(fontName, token);
166
+ if (preferred) {
167
+ glyphs.push(preferred);
168
+ continue;
169
+ }
170
+
171
+ const fallback = getRenderableGlyph(DEFAULT_FONT, token);
172
+ if (!fallback) {
173
+ return "";
174
+ }
175
+ glyphs.push(fallback);
176
+ }
177
+
178
+ const maxHeight = glyphs.reduce((max, glyph) => Math.max(max, glyph.lines.length), 0);
179
+ const outputLines = [];
180
+ for (let row = 0; row < maxHeight; row += 1) {
181
+ const parts = glyphs.map((glyph) => {
182
+ const padTop = maxHeight - glyph.lines.length;
183
+ const sourceIndex = row - padTop;
184
+ const line = sourceIndex >= 0 ? glyph.lines[sourceIndex] || "" : "";
185
+ return line.padEnd(glyph.width, " ");
186
+ });
187
+ outputLines.push(parts.join(" ").replace(/\s+$/g, ""));
188
+ }
189
+
190
+ return `${outputLines.join("\n")}\n`;
191
+ }
192
+
107
193
  function isTimerCompatibleFont(fontName) {
108
- return hasVisibleGlyphs(renderWithFont(TIMER_SAMPLE_TEXT, fontName));
194
+ const rendered = renderWithFont(TIMER_SAMPLE_TEXT, fontName);
195
+ return hasVisibleGlyphs(rendered) && !isPlainTimerRender(rendered, TIMER_SAMPLE_TEXT);
109
196
  }
110
197
 
111
198
  function getTimerCompatibleFontsSlow() {
@@ -240,7 +327,7 @@ function normalizeConfig(raw) {
240
327
  next.keybindings = normalizeKeybindings(raw.keybindings);
241
328
  if (typeof raw.font === "string") {
242
329
  const normalizedFont = normalizeFontName(raw.font);
243
- if (normalizedFont && isTimerCompatibleFont(normalizedFont)) {
330
+ if (normalizedFont) {
244
331
  next.font = normalizedFont;
245
332
  }
246
333
  }
@@ -284,9 +371,6 @@ function setFontInConfig(requestedFont) {
284
371
  if (!normalized) {
285
372
  return { ok: false, reason: "unknown", font: null };
286
373
  }
287
- if (!isTimerCompatibleFont(normalized)) {
288
- return { ok: false, reason: "incompatible", font: null };
289
- }
290
374
  const updated = updateConfig({ font: normalized });
291
375
  return { ok: true, reason: null, font: updated.font };
292
376
  }
@@ -346,15 +430,25 @@ function parseDurationArgs(args) {
346
430
 
347
431
  function renderTimeAscii(timeText, fontName) {
348
432
  const preferred = renderWithFont(timeText, fontName);
349
- if (hasVisibleGlyphs(preferred)) {
433
+ if (hasVisibleGlyphs(preferred) && !isPlainTimerRender(preferred, timeText)) {
350
434
  return preferred;
351
435
  }
352
436
 
437
+ const preferredByGlyph = renderTimeByGlyphs(timeText, fontName);
438
+ if (hasVisibleGlyphs(preferredByGlyph) && !isPlainTimerRender(preferredByGlyph, timeText)) {
439
+ return preferredByGlyph;
440
+ }
441
+
353
442
  const fallback = renderWithFont(timeText, DEFAULT_FONT);
354
- if (hasVisibleGlyphs(fallback)) {
443
+ if (hasVisibleGlyphs(fallback) && !isPlainTimerRender(fallback, timeText)) {
355
444
  return fallback;
356
445
  }
357
446
 
447
+ const fallbackByGlyph = renderTimeByGlyphs(timeText, DEFAULT_FONT);
448
+ if (hasVisibleGlyphs(fallbackByGlyph)) {
449
+ return fallbackByGlyph;
450
+ }
451
+
358
452
  return `${timeText}\n`;
359
453
  }
360
454
 
@@ -704,7 +798,7 @@ function playCompletionAlarm(config) {
704
798
  return;
705
799
  }
706
800
  try {
707
- process.stderr.write("\x07\x07\x07");
801
+ process.stderr.write("\x07".repeat(5));
708
802
  } catch (_error) {
709
803
  }
710
804
  }
@@ -1096,13 +1190,8 @@ function runTimer(args) {
1096
1190
  const requestedFont = args.slice(1).join(" ");
1097
1191
  const result = setFontInConfig(requestedFont);
1098
1192
  if (!result.ok) {
1099
- if (result.reason === "incompatible") {
1100
- process.stderr.write(`Font is incompatible with timer digits: ${requestedFont}\n`);
1101
- } else {
1102
- process.stderr.write(`Unknown font: ${requestedFont}\n`);
1103
- }
1193
+ process.stderr.write(`Unknown font: ${requestedFont}\n`);
1104
1194
  process.stderr.write("Run `timer style` to list fonts.\n");
1105
- process.stderr.write("Run `timer style --compatible` to list only compatible fonts.\n");
1106
1195
  process.exitCode = 1;
1107
1196
  return;
1108
1197
  }