@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.
- package/README.md +6 -5
- package/package.json +1 -1
- 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
|
|
58
|
+
To list all available fonts:
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
61
|
timer style
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
|
|
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 --
|
|
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
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
|
}
|
|
@@ -978,7 +1072,7 @@ function runSettingsUI() {
|
|
|
978
1072
|
const state = {
|
|
979
1073
|
configPath: CONFIG_PATH,
|
|
980
1074
|
config: readConfig(),
|
|
981
|
-
fonts:
|
|
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] === "--
|
|
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 =
|
|
1167
|
+
const fonts = getAllFonts();
|
|
1074
1168
|
process.stdout.write(`Current font: ${currentFont}\n\n`);
|
|
1075
|
-
process.stdout.write("
|
|
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("\
|
|
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] === "--
|
|
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 =
|
|
1181
|
+
const fonts = getTimerCompatibleFontsSlow();
|
|
1086
1182
|
process.stdout.write(`Current font: ${currentFont}\n\n`);
|
|
1087
|
-
process.stdout.write("
|
|
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
|
-
|
|
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
|
}
|