@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.
- package/package.json +1 -1
- package/src/index.js +61 -4
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 "";
|