@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.
- package/README.md +1 -0
- package/package.json +1 -1
- 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
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|