@timmy6942025/cli-timer 1.1.9 → 1.1.10

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +61 -4
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.10",
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 "";