@cacinie/cace-timer 1.3.2 → 1.4.2

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/dist/mascot.js CHANGED
@@ -4,217 +4,21 @@ exports.CACE_CELEBRATING = exports.CACE_FOCUSED = exports.CACE_SLEEPY = exports.
4
4
  exports.getGreeting = getGreeting;
5
5
  exports.showCaceAnimation = showCaceAnimation;
6
6
  exports.showCaceSmall = showCaceSmall;
7
- const utils_1 = require("./utils");
8
7
  const i18n_1 = require("./i18n");
9
- // ============ CACE TIMER Mascot ============
10
- // 可爱短发小女孩 完整身体设计
11
- // 人设参考: 语码 Cace — 译者 + Vibe Coder + 电音/VOCALOID 宅
12
- // 特征: 呆毛 + 齐耳 Bob 短发 + 尖下巴 + A 字裙(CACE 绣在裙面)
13
- // 动画帧用于开场动画,各 mood 用于内联显示
14
- // 对齐约定: 中心线在 13/14 列之间,所有 ┬┴┘└ 连接点逐列对齐
15
- const CACE_FRAMES = [
16
- // Frame 1: 正常站立
17
- `
18
- ╭─╮ ╭─╮
19
- ╰─╯ ╰─╯
20
- ╭────────────╮
21
- ╭┴────────────┴╮
22
- │ ╭╮╭╮╭╮╭╮ │
23
- │ ● ● │
24
- │ ▽ │
25
- │ ─── │
26
- ╰──────┬┬──────╯
27
- ││
28
- ╭─────┴┴─────╮
29
- ╭╯ ╔══════╗ ╰╮
30
- o│ ║ CACE ║ │o
31
- │ ╚══════╝ │
32
- ╰────┬────┬────╯
33
- │ │
34
- │ │
35
- │ │
36
- ╭┴╮ ╭┴╮
37
- ╰─╯ ╰─╯`,
38
- // Frame 2: 眨眼
39
- `
40
- ╭─╮ ╭─╮
41
- ╰─╯ ╰─╯
42
- ╭────────────╮
43
- ╭┴────────────┴╮
44
- │ ╭╮╭╮╭╮╭╮ │
45
- │ ─ ─ │
46
- │ ▽ │
47
- │ ─── │
48
- ╰──────┬┬──────╯
49
- ││
50
- ╭─────┴┴─────╮
51
- ╭╯ ╔══════╗ ╰╮
52
- o│ ║ CACE ║ │o
53
- │ ╚══════╝ │
54
- ╰────┬────┬────╯
55
- │ │
56
- │ │
57
- │ │
58
- ╭┴╮ ╭┴╮
59
- ╰─╯ ╰─╯`,
60
- // Frame 3: 微笑蹦跳
61
- `
62
- ╭─╮ ╭─╮
63
- ╰─╯ ╰─╯
64
- ╭────────────╮
65
- ╭┴────────────┴╮
66
- │ ╭╮╭╮╭╮╭╮ │
67
- │ ★ ★ │
68
- │ ▽ │
69
- │ ◡◡◡ │
70
- ╰──────┬┬──────╯
71
- ││
72
- ╭─────┴┴─────╮
73
- ╭╯ ╔══════╗ ╰╮
74
- o│ ║ CACE ║ │o
75
- │ ╚══════╝ │
76
- ╰────┬────┬────╯
77
- ╭───┘ └───╮
78
- │ │
79
- │ │
80
- ╭┴╮ ╭┴╮
81
- ╰─╯ ╰─╯`,
82
- // Frame 4: 超开心 + 举手
83
- `
84
- ♪ ╭─╮ ╭─╮
85
- ╰─╯ ╰─╯
86
- ╭────────────╮
87
- ╭┴────────────┴╮
88
- │ ╭╮╭╮╭╮╭╮ │
89
- │ ◉ ◉ │
90
- │ ▽ │
91
- │ ▽△▽ │
92
- ╰──────┬┬──────╯
93
- ││
94
- ╭─────┴┴─────╮
95
- ╭╯ ╔══════╗ ╰╮
96
- o│ ║ CACE ║ │o
97
- │ ╚══════╝ │
98
- ╰────┬────┬────╯
99
- ╭───┘ └───╮
100
- │ │
101
- │ │
102
- ╭┴╮ ╭┴╮
103
- ╰─╯ ╰─╯`,
104
- ];
105
- // --- Inline mascot variants ---
106
- exports.CACE_SMALL = `
107
- ╭─╮ ╭─╮
108
- ╰─╯ ╰─╯
109
- ╭────────────╮
110
- ╭┴────────────┴╮
111
- │ ╭╮╭╮╭╮╭╮ │
112
- │ ● ● │
113
- │ ▽ │
114
- │ ─── │
115
- ╰──────┬┬──────╯
116
- ││
117
- ╭─────┴┴─────╮
118
- ╭╯ ╔══════╗ ╰╮
119
- o│ ║ CACE ║ │o
120
- │ ╚══════╝ │
121
- ╰────┬────┬────╯
122
- │ │
123
- │ │
124
- │ │
125
- ╭┴╮ ╭┴╮
126
- ╰─╯ ╰─╯`;
127
- exports.CACE_HAPPY = `
128
- ╭─╮ ╭─╮
129
- ╰─╯ ╰─╯
130
- ╭────────────╮
131
- ╭┴────────────┴╮
132
- │ ╭╮╭╮╭╮╭╮ │
133
- │ ★ ★ │
134
- │ ▽ │
135
- │ ◡◡◡ │
136
- ╰──────┬┬──────╯
137
- ││
138
- ╭─────┴┴─────╮
139
- ╭╯ ╔══════╗ ╰╮
140
- o│ ║ CACE ║ │o
141
- │ ╚══════╝ │
142
- ╰────┬────┬────╯
143
- ╭───┘ └───╮
144
- │ │
145
- │ │
146
- ╭┴╮ ╭┴╮
147
- ╰─╯ ╰─╯`;
148
- exports.CACE_SLEEPY = `
149
- ╭─╮ ╭─╮
150
- ╰─╯ ╰─╯
151
- ╭────────────╮
152
- ╭┴────────────┴╮
153
- │ ╭╮╭╮╭╮╭╮ │
154
- │ ─ ─ │
155
- │ ▽ │
156
- │ ~~ │
157
- ╰──────┬┬──────╯
158
- ││
159
- ╭─────┴┴─────╮
160
- ╭╯ ╔══════╗ ╰╮
161
- o│ ║ CACE ║ │o
162
- │ ╚══════╝ │
163
- ╰────┬────┬────╯
164
- │ │
165
- │ │
166
- │ │
167
- ╭┴╮ ╭┴╮
168
- ╰─╯ ╰─╯`;
169
- exports.CACE_FOCUSED = `
170
- ╭─╮ ╭─╮
171
- ╰─╯ ╰─╯
172
- ╭────────────╮
173
- ╭┴────────────┴╮
174
- │ ╭╮╭╮╭╮╭╮ │
175
- │ ◆ ◆ │
176
- │ ▽ │
177
- │ ▬▬▬ │
178
- ╰──────┬┬──────╯
179
- ││
180
- ╭─────┴┴─────╮
181
- ╭╯ ╔══════╗ ╰╮
182
- o│ ║ CACE ║ │o
183
- │ ╚══════╝ │
184
- ╰────┬────┬────╯
185
- │ │
186
- │ │
187
- │ │
188
- ╭┴╮ ╭┴╮
189
- ╰─╯ ╰─╯`;
190
- exports.CACE_CELEBRATING = `
191
- ♪ ╭─╮ ╭─╮
192
- ╰─╯ ╰─╯
193
- ╭────────────╮
194
- ╭┴────────────┴╮
195
- │ ╭╮╭╮╭╮╭╮ │
196
- │ ◉ ◉ │
197
- │ ▽ │
198
- │ ▽△▽ │
199
- ╰──────┬┬──────╯
200
- ││
201
- ╭─────┴┴─────╮
202
- ╭╯ ╔══════╗ ╰╮
203
- o│ ║ CACE ║ │o
204
- │ ╚══════╝ │
205
- ╰────┬────┬────╯
206
- ╭───┘ └───╮
207
- │ │
208
- │ │
209
- ╭┴╮ ╭┴╮
210
- ╰─╯ ╰─╯`;
8
+ const terminal_1 = require("./terminal");
9
+ const frames_1 = require("./mascot/frames");
211
10
  const MOOD_MAP = {
212
- normal: exports.CACE_SMALL,
213
- happy: exports.CACE_HAPPY,
214
- sleepy: exports.CACE_SLEEPY,
215
- focused: exports.CACE_FOCUSED,
216
- celebrating: exports.CACE_CELEBRATING,
11
+ normal: 'little_smile',
12
+ happy: 'happy',
13
+ sleepy: 'asleep',
14
+ focused: 'little_smile',
15
+ celebrating: 'excited',
217
16
  };
17
+ exports.CACE_SMALL = (0, frames_1.plainFrame)((0, frames_1.getFrame)({ size: 'compact' }));
18
+ exports.CACE_HAPPY = (0, frames_1.plainFrame)((0, frames_1.getFrame)({ expression: 'happy' }));
19
+ exports.CACE_SLEEPY = (0, frames_1.plainFrame)((0, frames_1.getFrame)({ expression: 'asleep' }));
20
+ exports.CACE_FOCUSED = (0, frames_1.plainFrame)((0, frames_1.getFrame)());
21
+ exports.CACE_CELEBRATING = (0, frames_1.plainFrame)((0, frames_1.getFrame)({ expression: 'excited' }));
218
22
  function getGreeting() {
219
23
  const hour = new Date().getHours();
220
24
  if (hour < 6)
@@ -225,33 +29,18 @@ function getGreeting() {
225
29
  return (0, i18n_1.t)('greeting.afternoon');
226
30
  return (0, i18n_1.t)('greeting.evening');
227
31
  }
32
+ // One-shot commands return promptly. Continuous motion belongs to a TUI screen
33
+ // with a lifecycle, not a clear/sleep loop inside task mutations.
228
34
  async function showCaceAnimation(message = '') {
229
- const cyan = '\x1b[36m';
230
- const bold = '\x1b[1m';
231
- const reset = '\x1b[0m';
232
- // Animation: 3 cycles through frames for a smooth intro
233
- for (let i = 0; i < 3; i++) {
234
- for (const frame of CACE_FRAMES) {
235
- console.clear();
236
- console.log(cyan + frame + reset);
237
- console.log();
238
- console.log(cyan + bold + ' ═══════════════════════════════════' + reset);
239
- console.log(cyan + bold + ' C A C E T I M E R' + reset);
240
- console.log(cyan + bold + ' ═══════════════════════════════════' + reset);
241
- if (message) {
242
- console.log();
243
- console.log(' ' + message);
244
- }
245
- await (0, utils_1.sleep)(200);
246
- }
247
- }
35
+ showCaceSmall(message);
248
36
  }
249
37
  function showCaceSmall(status = '', mood = 'normal') {
250
- const cyan = '\x1b[36m';
251
- const reset = '\x1b[0m';
252
- const face = MOOD_MAP[mood] || exports.CACE_SMALL;
253
- console.log(cyan + face + reset);
254
- if (status) {
255
- console.log(cyan + ' ' + status + reset);
256
- }
38
+ const display = (0, terminal_1.getDisplay)();
39
+ const columns = process.stdout.columns ?? 80;
40
+ const size = columns >= 22 && (process.stdout.rows ?? 24) >= 18 ? 'compact' : 'tiny';
41
+ const frame = (0, frames_1.getFrame)({ expression: MOOD_MAP[mood], size, ascii: display.ascii });
42
+ if (columns >= frame.width)
43
+ console.log((0, frames_1.renderFrame)(frame, display));
44
+ if (status)
45
+ console.log((0, terminal_1.fitText)(status, columns));
257
46
  }
package/dist/parser.js CHANGED
@@ -1,6 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseArgs = parseArgs;
4
+ const BOOLEAN_OPTIONS = new Set([
5
+ 'no-animation',
6
+ 'no-tui',
7
+ 'ascii',
8
+ 'all',
9
+ 'animate',
10
+ 'today',
11
+ 'week',
12
+ 'month',
13
+ 'last',
14
+ ]);
4
15
  function parseArgs(args) {
5
16
  const result = {
6
17
  command: '',
@@ -13,7 +24,7 @@ function parseArgs(args) {
13
24
  if (arg.startsWith('--')) {
14
25
  const key = arg.slice(2);
15
26
  const nextArg = args[i + 1];
16
- if (nextArg && !nextArg.startsWith('-')) {
27
+ if (!BOOLEAN_OPTIONS.has(key) && nextArg && !nextArg.startsWith('-')) {
17
28
  // Support repeated flags: accumulate into array
18
29
  const existing = result.options[key];
19
30
  if (Array.isArray(existing)) {
@@ -0,0 +1,15 @@
1
+ export type ColorMode = 'auto' | 'always' | '256' | 'never';
2
+ export interface DisplayOptions {
3
+ animation: boolean;
4
+ tui: boolean;
5
+ ascii: boolean;
6
+ color: ColorMode;
7
+ theme: 'dark' | 'light';
8
+ }
9
+ export declare function configureDisplay(options: Partial<DisplayOptions>): void;
10
+ export declare function getDisplay(): Readonly<DisplayOptions>;
11
+ export declare function resolveColor(mode?: ColorMode, isTTY?: boolean, env?: NodeJS.ProcessEnv): Exclude<ColorMode, 'auto'>;
12
+ export declare function cellWidth(text: string): number;
13
+ export declare function safeText(text: string): string;
14
+ export declare function fitText(text: string, columns: number): string;
15
+ export declare function paint(text: string, color: 'cyan' | 'yellow' | 'red' | 'green'): string;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.configureDisplay = configureDisplay;
7
+ exports.getDisplay = getDisplay;
8
+ exports.resolveColor = resolveColor;
9
+ exports.cellWidth = cellWidth;
10
+ exports.safeText = safeText;
11
+ exports.fitText = fitText;
12
+ exports.paint = paint;
13
+ const blessed_1 = __importDefault(require("blessed"));
14
+ const node_util_1 = require("node:util");
15
+ let display = {
16
+ animation: true,
17
+ tui: true,
18
+ ascii: false,
19
+ color: 'auto',
20
+ theme: 'dark',
21
+ };
22
+ function configureDisplay(options) {
23
+ display = { ...display, ...options };
24
+ }
25
+ function getDisplay() {
26
+ return display;
27
+ }
28
+ function resolveColor(mode = display.color, isTTY = process.stdout.isTTY === true, env = process.env) {
29
+ if (mode !== 'auto')
30
+ return mode;
31
+ if (!isTTY || env.TERM === 'dumb' || env.NO_COLOR !== undefined)
32
+ return 'never';
33
+ return /^(truecolor|24bit)$/.test(env.COLORTERM ?? '') ? 'always' : '256';
34
+ }
35
+ // Use the same width table as the terminal renderer, including ambiguous-width
36
+ // policy (NCURSES_CJK_WIDTH), rather than JavaScript UTF-16 string lengths.
37
+ const unicode = blessed_1.default.unicode;
38
+ function cellWidth(text) {
39
+ return unicode.strWidth(text);
40
+ }
41
+ function safeText(text) {
42
+ return (0, node_util_1.stripVTControlCharacters)(text).replace(/[\p{Cc}\p{Cf}]/gu, ' ');
43
+ }
44
+ function fitText(text, columns) {
45
+ const clean = safeText(text);
46
+ const width = Math.max(0, Math.floor(columns));
47
+ if (cellWidth(clean) <= width)
48
+ return clean;
49
+ if (!width)
50
+ return '';
51
+ let result = '';
52
+ let used = 0;
53
+ for (const char of clean) {
54
+ const size = unicode.charWidth(char.codePointAt(0));
55
+ if (used + size > width - 1)
56
+ break;
57
+ result += char;
58
+ used += size;
59
+ }
60
+ return result + '~';
61
+ }
62
+ function paint(text, color) {
63
+ const clean = safeText(text);
64
+ if (resolveColor() === 'never')
65
+ return clean;
66
+ const codes = { cyan: 36, yellow: 33, red: 31, green: 32 };
67
+ return `\x1b[${codes[color]}m${clean}\x1b[0m`;
68
+ }
@@ -0,0 +1,5 @@
1
+ export interface RefreshLoop {
2
+ refresh(): void;
3
+ dispose(): void;
4
+ }
5
+ export declare function startRefreshLoop(render: (elapsedMs: number) => void, onError: (error: unknown) => void, intervalMs?: number, now?: () => number): RefreshLoop;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startRefreshLoop = startRefreshLoop;
4
+ // A single owned timer per screen. The caller only writes changed content;
5
+ // this clock never changes task data or signals business completion.
6
+ function startRefreshLoop(render, onError, intervalMs = 100, now = () => performance.now()) {
7
+ const startedAt = now();
8
+ let disposed = false;
9
+ const dispose = () => {
10
+ if (disposed)
11
+ return;
12
+ disposed = true;
13
+ clearInterval(timer);
14
+ };
15
+ const refresh = () => {
16
+ if (disposed)
17
+ return;
18
+ try {
19
+ render(Math.max(0, now() - startedAt));
20
+ }
21
+ catch (error) {
22
+ dispose();
23
+ onError(error);
24
+ }
25
+ };
26
+ const timer = setInterval(refresh, intervalMs);
27
+ refresh();
28
+ return { refresh, dispose };
29
+ }
@@ -1,8 +1,5 @@
1
- interface CountdownOptions {
1
+ export interface CountdownOptions {
2
2
  totalMs: number;
3
3
  label: string;
4
- mascots: string[];
5
- onDone: () => void;
6
4
  }
7
- export declare function showCountdown(options: CountdownOptions): void;
8
- export {};
5
+ export declare function showCountdown(options: CountdownOptions): Promise<boolean>;
@@ -5,131 +5,62 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.showCountdown = showCountdown;
7
7
  const blessed_1 = __importDefault(require("blessed"));
8
+ const i18n_1 = require("../i18n");
9
+ const terminal_1 = require("../terminal");
8
10
  const lifecycle_1 = require("./lifecycle");
11
+ const animation_1 = require("./animation");
12
+ const surface_1 = require("./surface");
13
+ // Resolve false on cancellation; reject on render failure. Neither path is a
14
+ // completed timer, so callers must not archive a successful work session.
9
15
  function showCountdown(options) {
10
- const screen = blessed_1.default.screen({
11
- smartCSR: true,
12
- title: 'CACE TIMER',
13
- fullUnicode: true,
14
- });
15
- // Mascot display
16
- const mascotBox = blessed_1.default.box({
17
- parent: screen,
18
- top: 0,
19
- left: 'center',
20
- width: '100%',
21
- height: 16,
22
- align: 'center',
23
- valign: 'middle',
24
- style: { fg: 'cyan' },
25
- });
26
- // Label (round info) — rendered as static content, no box ref needed
27
- blessed_1.default.box({
28
- parent: screen,
29
- top: 16,
30
- left: 'center',
31
- width: '100%',
32
- height: 1,
33
- align: 'center',
34
- style: { fg: 'white', bold: true },
35
- content: options.label,
36
- });
37
- // Timer display
38
- const timerBox = blessed_1.default.box({
39
- parent: screen,
40
- top: 18,
41
- left: 'center',
42
- width: '100%',
43
- height: 3,
44
- align: 'center',
45
- valign: 'middle',
46
- style: { fg: 'cyan', bold: true },
47
- });
48
- // Progress bar container
49
- const progressLabel = blessed_1.default.box({
50
- parent: screen,
51
- top: 22,
52
- left: 'center',
53
- width: '100%',
54
- height: 1,
55
- align: 'center',
56
- style: { fg: 'white' },
57
- });
58
- // Controls hint
59
- blessed_1.default.box({
60
- parent: screen,
61
- bottom: 0,
62
- left: 'center',
63
- width: '100%',
64
- height: 1,
65
- align: 'center',
66
- style: { fg: 'gray' },
67
- content: 'Ctrl+C to stop',
68
- });
69
- // Draw mascot
70
- if (options.mascots.length > 0) {
71
- mascotBox.setContent(options.mascots[0]);
72
- }
73
- const startTime = Date.now();
74
- const barWidth = 30;
75
- // Clean up signal handlers when this screen closes normally.
76
- const disposeSignalCleanup = (0, lifecycle_1.installSignalCleanup)(screen);
77
- // Update timer every 500ms
78
- const interval = setInterval(() => {
79
- let elapsed = 0;
80
- let remaining = 0;
81
- try {
82
- elapsed = Date.now() - startTime;
83
- remaining = Math.max(0, options.totalMs - elapsed);
84
- const progress = Math.min(1, elapsed / options.totalMs);
85
- // Format time
86
- const totalSec = Math.floor(remaining / 1000);
87
- const min = Math.floor(totalSec / 60);
88
- const sec = totalSec % 60;
89
- const timeStr = `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
90
- // Progress bar
91
- const filled = Math.floor(progress * barWidth);
92
- const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
93
- const pct = Math.floor(progress * 100);
94
- timerBox.setContent(` ${timeStr} `);
95
- progressLabel.setContent(`[${bar}] ${pct}%`);
96
- // Rotate mascot expression
97
- if (options.mascots.length > 1) {
98
- const idx = Math.floor(progress * options.mascots.length) % options.mascots.length;
99
- mascotBox.setContent(options.mascots[idx]);
100
- }
101
- screen.render();
102
- }
103
- catch (err) {
104
- // Render or content error — don't leave the screen half-updated.
105
- // Tear down with cleanup, then surface the error to the caller.
106
- clearInterval(interval);
107
- disposeSignalCleanup();
108
- (0, lifecycle_1.destroyScreen)(screen);
109
- options.onDone();
110
- // Re-throw asynchronously so Node reports it instead of swallowing it.
111
- setImmediate(() => {
112
- throw err;
113
- });
114
- return;
115
- }
116
- if (elapsed >= options.totalMs) {
117
- clearInterval(interval);
118
- disposeSignalCleanup();
119
- process.stdout.write('\x07'); // bell
16
+ return new Promise((resolve, reject) => {
17
+ const screen = blessed_1.default.screen({ smartCSR: true, title: 'CACE TIMER', fullUnicode: true });
18
+ const surface = (0, surface_1.createSurface)(screen);
19
+ let loop = undefined;
20
+ let settled = false;
21
+ const disposeSignals = (0, lifecycle_1.installSignalCleanup)(screen);
22
+ screen.once('destroy', () => {
23
+ loop?.dispose();
24
+ disposeSignals();
25
+ });
26
+ const finish = (completed, error) => {
27
+ if (settled)
28
+ return;
29
+ settled = true;
30
+ loop?.dispose();
120
31
  (0, lifecycle_1.destroyScreen)(screen);
121
- options.onDone();
122
- }
123
- }, 500);
124
- // Don't keep the event loop alive just for the timer.
125
- if (typeof interval.unref === 'function')
126
- interval.unref();
127
- // Handle Ctrl+C — exit code 130, blessed cleanup included.
128
- screen.key(['C-c'], () => {
129
- clearInterval(interval);
130
- process.stdout.write('\n');
131
- (0, lifecycle_1.destroyScreen)(screen);
132
- process.kill(process.pid, 'SIGINT');
32
+ if (error !== undefined)
33
+ reject(error);
34
+ else
35
+ resolve(completed);
36
+ };
37
+ screen.key(['escape', 'q'], () => finish(false));
38
+ screen.key(['C-c'], () => process.emit('SIGINT'));
39
+ screen.on('resize', () => loop?.refresh());
40
+ loop = (0, animation_1.startRefreshLoop)((elapsed) => {
41
+ if (elapsed >= options.totalMs) {
42
+ finish(true);
43
+ return;
44
+ }
45
+ const remaining = Math.max(0, options.totalMs - elapsed);
46
+ const totalSeconds = Math.ceil(remaining / 1000);
47
+ const minutes = Math.floor(totalSeconds / 60);
48
+ const seconds = totalSeconds % 60;
49
+ const progress = Math.min(1, elapsed / options.totalMs);
50
+ const width = Math.max(1, Math.min(28, Number(screen.width) - 8));
51
+ const filled = Math.floor(width * progress);
52
+ const bar = '[' + '='.repeat(filled) + '-'.repeat(width - filled) + ']';
53
+ const lines = [
54
+ options.label,
55
+ `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`,
56
+ `${bar} ${Math.floor(progress * 100)}%`,
57
+ '',
58
+ (0, i18n_1.t)('tui.oneThing'),
59
+ ];
60
+ surface.render('little_smile', elapsed, lines, (0, i18n_1.t)('tui.cancel'));
61
+ }, (error) => finish(false, error), (0, terminal_1.getDisplay)().animation ? 100 : 200);
62
+ // A zero-duration timer can finish during the first synchronous render.
63
+ if (settled)
64
+ loop.dispose();
133
65
  });
134
- screen.render();
135
66
  }