@timmy6942025/cli-timer 1.1.7 → 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 +6 -5
  2. package/package.json +1 -1
  3. package/src/index.js +114 -25
package/README.md CHANGED
@@ -55,16 +55,18 @@ By default, timer and stopwatch output is centered in the terminal.
55
55
 
56
56
  You can view and set the ASCII font style used for the timer and stopwatch display.
57
57
 
58
- To list timer-compatible fonts (recommended):
58
+ To list all available fonts:
59
59
 
60
60
  ```bash
61
61
  timer style
62
62
  ```
63
63
 
64
- To list every figlet font:
64
+ This is instant and lists all figlet fonts.
65
+
66
+ To list only timer-compatible fonts (fonts that render `01:23:45` visibly):
65
67
 
66
68
  ```bash
67
- timer style --all
69
+ timer style --compatible
68
70
  ```
69
71
 
70
72
  To set your preferred font:
@@ -74,6 +76,7 @@ timer style <font>
74
76
  ```
75
77
 
76
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.
77
80
 
78
81
  ## Settings UI
79
82
 
@@ -105,8 +108,6 @@ This launches a Bubble Tea based screen where you can change:
105
108
  - Restart key
106
109
  - Exit key / exit alt key
107
110
 
108
- The settings UI font picker now shows timer-compatible fonts only.
109
-
110
111
  Controls in settings UI:
111
112
 
112
113
  - `Enter`: select/toggle
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmy6942025/cli-timer",
3
- "version": "1.1.7",
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
  }
@@ -978,7 +1072,7 @@ function runSettingsUI() {
978
1072
  const state = {
979
1073
  configPath: CONFIG_PATH,
980
1074
  config: readConfig(),
981
- fonts: getTimerCompatibleFontsSlow()
1075
+ fonts: getAllFonts()
982
1076
  };
983
1077
 
984
1078
  fs.writeFileSync(SETTINGS_STATE_PATH, JSON.stringify(state), "utf8");
@@ -1046,6 +1140,7 @@ function printUsage() {
1046
1140
  process.stdout.write("Font Styles\n");
1047
1141
  process.stdout.write(" timer style\n");
1048
1142
  process.stdout.write(" timer style --all\n");
1143
+ process.stdout.write(" timer style --compatible\n");
1049
1144
  process.stdout.write(" timer style <font>\n");
1050
1145
  }
1051
1146
 
@@ -1067,42 +1162,36 @@ function runTimer(args) {
1067
1162
  }
1068
1163
 
1069
1164
  if (args[0] === "style") {
1070
- if (args.length === 1 || (args.length === 2 && args[1] === "--compatible")) {
1071
- process.stdout.write("Checking font compatibility for timer digits...\n\n");
1165
+ if (args.length === 1 || (args.length === 2 && args[1] === "--all")) {
1072
1166
  const currentFont = getFontFromConfig();
1073
- const fonts = getTimerCompatibleFontsSlow();
1167
+ const fonts = getAllFonts();
1074
1168
  process.stdout.write(`Current font: ${currentFont}\n\n`);
1075
- process.stdout.write("Timer-compatible fonts:\n");
1169
+ process.stdout.write("Available fonts:\n");
1076
1170
  for (const font of fonts) {
1077
1171
  process.stdout.write(`${font}\n`);
1078
1172
  }
1079
- process.stdout.write("\nUse `timer style --all` to list every figlet font.\n");
1173
+ process.stdout.write("\nTip: Some fonts do not support timer digits.\n");
1174
+ process.stdout.write("Use `timer style <font>` to validate and set safely.\n");
1080
1175
  return;
1081
1176
  }
1082
1177
 
1083
- if (args.length === 2 && args[1] === "--all") {
1178
+ if (args.length === 2 && args[1] === "--compatible") {
1179
+ process.stdout.write("Checking font compatibility for timer digits...\n\n");
1084
1180
  const currentFont = getFontFromConfig();
1085
- const fonts = getAllFonts();
1181
+ const fonts = getTimerCompatibleFontsSlow();
1086
1182
  process.stdout.write(`Current font: ${currentFont}\n\n`);
1087
- process.stdout.write("All fonts:\n");
1183
+ process.stdout.write("Timer-compatible fonts:\n");
1088
1184
  for (const font of fonts) {
1089
1185
  process.stdout.write(`${font}\n`);
1090
1186
  }
1091
- process.stdout.write("\nSome fonts do not support timer digits.\n");
1092
- process.stdout.write("Use `timer style` for a safe compatible list.\n");
1093
1187
  return;
1094
1188
  }
1095
1189
 
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
  }