@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/README.md +61 -36
- package/dist/commands/export.js +4 -3
- package/dist/commands/help.js +5 -0
- package/dist/commands/mark.js +2 -1
- package/dist/commands/mascot.d.ts +5 -0
- package/dist/commands/mascot.js +77 -0
- package/dist/commands/pomodoro.js +26 -17
- package/dist/commands/resume.js +3 -2
- package/dist/commands/start.js +4 -3
- package/dist/commands/stop.js +7 -6
- package/dist/commands/summary.js +9 -9
- package/dist/commands/sync.js +3 -2
- package/dist/data.js +0 -1
- package/dist/i18n.js +23 -1
- package/dist/index.js +34 -4
- package/dist/mascot/assets/mint.d.ts +171 -0
- package/dist/mascot/assets/mint.js +218 -0
- package/dist/mascot/frames.d.ts +25 -0
- package/dist/mascot/frames.js +114 -0
- package/dist/mascot/state.d.ts +3 -0
- package/dist/mascot/state.js +25 -0
- package/dist/mascot.d.ts +5 -5
- package/dist/mascot.js +23 -234
- package/dist/parser.js +12 -1
- package/dist/terminal.d.ts +15 -0
- package/dist/terminal.js +68 -0
- package/dist/tui/animation.d.ts +5 -0
- package/dist/tui/animation.js +29 -0
- package/dist/tui/countdown.d.ts +2 -5
- package/dist/tui/countdown.js +54 -123
- package/dist/tui/dashboard.js +68 -114
- package/dist/tui/index.js +5 -2
- package/dist/tui/layout.d.ts +15 -0
- package/dist/tui/layout.js +36 -0
- package/dist/tui/reflection.js +6 -4
- package/dist/tui/surface.d.ts +5 -0
- package/dist/tui/surface.js +64 -0
- package/package.json +5 -3
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
|
-
|
|
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:
|
|
213
|
-
happy:
|
|
214
|
-
sleepy:
|
|
215
|
-
focused:
|
|
216
|
-
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
|
-
|
|
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
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
console.log(
|
|
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;
|
package/dist/terminal.js
ADDED
|
@@ -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,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
|
+
}
|
package/dist/tui/countdown.d.ts
CHANGED
|
@@ -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):
|
|
8
|
-
export {};
|
|
5
|
+
export declare function showCountdown(options: CountdownOptions): Promise<boolean>;
|
package/dist/tui/countdown.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
smartCSR: true,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
}
|