@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 +6 -0
- package/package.json +1 -1
- package/src/index.js +80 -4
package/README.md
CHANGED
package/package.json
CHANGED
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 =
|
|
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
|
|
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) {
|