@timmy6942025/cli-timer 1.1.9 → 1.1.11

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.
package/README.md CHANGED
@@ -69,6 +69,12 @@ To list only timer-compatible fonts (fonts that render `01:23:45` visibly):
69
69
  timer style --compatible
70
70
  ```
71
71
 
72
+ To set a random font:
73
+
74
+ ```bash
75
+ timer style random
76
+ ```
77
+
72
78
  To set your preferred font:
73
79
 
74
80
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmy6942025/cli-timer",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
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,9 @@ 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 PRINTABLE_ASCII_CANDIDATES = Object.freeze(
16
+ Array.from({ length: 94 }, (_, index) => String.fromCharCode(33 + index))
17
+ );
15
18
  const TIME_CHAR_FALLBACKS = Object.freeze({
16
19
  "0": Object.freeze(["0", "O", "o", "Q", "D", "U", "X"]),
17
20
  "1": Object.freeze(["1", "I", "l", "|", "!", "T", "X"]),
@@ -25,6 +28,7 @@ const TIME_CHAR_FALLBACKS = Object.freeze({
25
28
  "9": Object.freeze(["9", "g", "q", "P", "p", "X"]),
26
29
  ":": Object.freeze([":", "|", "!", "i", "I", ".", ";", "X"])
27
30
  });
31
+ const SYNTHETIC_FILL_CHARS = Object.freeze(["#", "@", "%", "&", "*", "+", "=", "~", "^", "$", "?"]);
28
32
 
29
33
  const MIN_TICK_RATE_MS = 50;
30
34
  const MAX_TICK_RATE_MS = 1000;
@@ -62,6 +66,8 @@ const DEFAULT_CONFIG = Object.freeze({
62
66
  let allFontsCache = null;
63
67
  let compatibleFontsSlowCache = null;
64
68
  const glyphCache = new Map();
69
+ const tokenCandidatesCache = new Map();
70
+ const syntheticFillCache = new Map();
65
71
 
66
72
  function clearScreen() {
67
73
  process.stdout.write("\x1b[2J\x1b[H");
@@ -137,19 +143,62 @@ function glyphCacheKey(fontName, token) {
137
143
  return `${fontName}\u0000${token}`;
138
144
  }
139
145
 
146
+ function tokenCandidates(token) {
147
+ if (tokenCandidatesCache.has(token)) {
148
+ return tokenCandidatesCache.get(token);
149
+ }
150
+ const seeded = TIME_CHAR_FALLBACKS[token] || [token];
151
+ const all = [...new Set([...seeded, ...PRINTABLE_ASCII_CANDIDATES])];
152
+ tokenCandidatesCache.set(token, all);
153
+ return all;
154
+ }
155
+
156
+ function glyphFromRendered(rendered) {
157
+ const lines = toDisplayLines(rendered);
158
+ const width = lines.reduce((max, line) => Math.max(max, line.length), 0);
159
+ return { lines, width };
160
+ }
161
+
162
+ function hashString(value) {
163
+ let hash = 2166136261;
164
+ for (const ch of String(value)) {
165
+ hash ^= ch.charCodeAt(0);
166
+ hash = Math.imul(hash, 16777619);
167
+ }
168
+ return hash >>> 0;
169
+ }
170
+
171
+ function syntheticFillCharForFont(fontName) {
172
+ if (syntheticFillCache.has(fontName)) {
173
+ return syntheticFillCache.get(fontName);
174
+ }
175
+ const fill = SYNTHETIC_FILL_CHARS[hashString(fontName) % SYNTHETIC_FILL_CHARS.length];
176
+ syntheticFillCache.set(fontName, fill);
177
+ return fill;
178
+ }
179
+
180
+ function synthesizeGlyph(fontName, token) {
181
+ const baseline = getRenderableGlyph(DEFAULT_FONT, token);
182
+ if (!baseline) {
183
+ return null;
184
+ }
185
+ const fill = syntheticFillCharForFont(fontName);
186
+ const lines = baseline.lines.map((line) => line.replace(/[^\s]/g, fill));
187
+ const width = lines.reduce((max, line) => Math.max(max, line.length), 0);
188
+ return { lines, width };
189
+ }
190
+
140
191
  function getRenderableGlyph(fontName, token) {
141
192
  const cacheKey = glyphCacheKey(fontName, token);
142
193
  if (glyphCache.has(cacheKey)) {
143
194
  return glyphCache.get(cacheKey);
144
195
  }
145
196
 
146
- const options = TIME_CHAR_FALLBACKS[token] || [token];
197
+ const options = tokenCandidates(token);
147
198
  for (const candidate of options) {
148
199
  const rendered = renderWithFont(candidate, fontName);
149
200
  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 };
201
+ const glyph = glyphFromRendered(rendered);
153
202
  glyphCache.set(cacheKey, glyph);
154
203
  return glyph;
155
204
  }
@@ -168,6 +217,14 @@ function renderTimeByGlyphs(timeText, fontName) {
168
217
  continue;
169
218
  }
170
219
 
220
+ if (fontName !== DEFAULT_FONT) {
221
+ const synthesized = synthesizeGlyph(fontName, token);
222
+ if (synthesized) {
223
+ glyphs.push(synthesized);
224
+ continue;
225
+ }
226
+ }
227
+
171
228
  const fallback = getRenderableGlyph(DEFAULT_FONT, token);
172
229
  if (!fallback) {
173
230
  return "";
@@ -1141,6 +1198,7 @@ function printUsage() {
1141
1198
  process.stdout.write(" timer style\n");
1142
1199
  process.stdout.write(" timer style --all\n");
1143
1200
  process.stdout.write(" timer style --compatible\n");
1201
+ process.stdout.write(" timer style random\n");
1144
1202
  process.stdout.write(" timer style <font>\n");
1145
1203
  }
1146
1204
 
@@ -1187,6 +1245,24 @@ function runTimer(args) {
1187
1245
  return;
1188
1246
  }
1189
1247
 
1248
+ if (args.length === 2 && args[1].toLowerCase() === "random") {
1249
+ const fonts = getAllFonts();
1250
+ if (fonts.length === 0) {
1251
+ process.stderr.write("No fonts are available.\n");
1252
+ process.exitCode = 1;
1253
+ return;
1254
+ }
1255
+ const randomFont = fonts[Math.floor(Math.random() * fonts.length)];
1256
+ const result = setFontInConfig(randomFont);
1257
+ if (!result.ok) {
1258
+ process.stderr.write(`Failed to set random font: ${randomFont}\n`);
1259
+ process.exitCode = 1;
1260
+ return;
1261
+ }
1262
+ process.stdout.write(`Random font set to: ${result.font}\n`);
1263
+ return;
1264
+ }
1265
+
1190
1266
  const requestedFont = args.slice(1).join(" ");
1191
1267
  const result = setFontInConfig(requestedFont);
1192
1268
  if (!result.ok) {