@cacinie/cace-timer 1.1.1 → 1.3.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/LICENSE +21 -21
- package/README.md +491 -192
- package/dist/commands/delete.d.ts +4 -0
- package/dist/commands/delete.js +48 -0
- package/dist/commands/export.d.ts +4 -0
- package/dist/commands/export.js +104 -0
- package/dist/commands/help.d.ts +1 -0
- package/dist/commands/help.js +73 -0
- package/dist/commands/list.d.ts +5 -0
- package/dist/commands/list.js +42 -0
- package/dist/commands/mark.d.ts +1 -0
- package/dist/commands/mark.js +27 -0
- package/dist/commands/pomodoro.d.ts +6 -0
- package/dist/commands/pomodoro.js +135 -0
- package/dist/commands/resume.d.ts +4 -0
- package/dist/commands/resume.js +54 -0
- package/dist/commands/search.d.ts +1 -0
- package/dist/commands/search.js +36 -0
- package/dist/commands/start.d.ts +4 -0
- package/dist/commands/start.js +47 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +48 -0
- package/dist/commands/stop.d.ts +3 -0
- package/dist/commands/stop.js +104 -0
- package/dist/commands/summary.d.ts +6 -0
- package/dist/commands/summary.js +128 -0
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +62 -0
- package/dist/data.d.ts +18 -0
- package/dist/data.js +161 -0
- package/dist/i18n.d.ts +7 -0
- package/dist/i18n.js +418 -0
- package/dist/index.js +142 -498
- package/dist/mascot.d.ts +9 -0
- package/dist/mascot.js +257 -0
- package/dist/parser.d.ts +6 -0
- package/dist/parser.js +45 -0
- package/dist/tui/countdown.d.ts +8 -0
- package/dist/tui/countdown.js +135 -0
- package/dist/tui/dashboard.d.ts +2 -0
- package/dist/tui/dashboard.js +135 -0
- package/dist/tui/index.d.ts +2 -0
- package/dist/tui/index.js +8 -0
- package/dist/tui/lifecycle.d.ts +28 -0
- package/dist/tui/lifecycle.js +82 -0
- package/dist/tui/reflection.d.ts +5 -0
- package/dist/tui/reflection.js +71 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +44 -0
- package/package.json +20 -4
package/dist/mascot.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CACE_CELEBRATING = exports.CACE_FOCUSED = exports.CACE_SLEEPY = exports.CACE_HAPPY = exports.CACE_SMALL = void 0;
|
|
4
|
+
exports.getGreeting = getGreeting;
|
|
5
|
+
exports.showCaceAnimation = showCaceAnimation;
|
|
6
|
+
exports.showCaceSmall = showCaceSmall;
|
|
7
|
+
const utils_1 = require("./utils");
|
|
8
|
+
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
|
+
╰─╯ ╰─╯`;
|
|
211
|
+
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,
|
|
217
|
+
};
|
|
218
|
+
function getGreeting() {
|
|
219
|
+
const hour = new Date().getHours();
|
|
220
|
+
if (hour < 6)
|
|
221
|
+
return (0, i18n_1.t)('greeting.lateNight');
|
|
222
|
+
if (hour < 12)
|
|
223
|
+
return (0, i18n_1.t)('greeting.morning');
|
|
224
|
+
if (hour < 18)
|
|
225
|
+
return (0, i18n_1.t)('greeting.afternoon');
|
|
226
|
+
return (0, i18n_1.t)('greeting.evening');
|
|
227
|
+
}
|
|
228
|
+
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
|
+
}
|
|
248
|
+
}
|
|
249
|
+
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
|
+
}
|
|
257
|
+
}
|
package/dist/parser.d.ts
ADDED
package/dist/parser.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseArgs = parseArgs;
|
|
4
|
+
function parseArgs(args) {
|
|
5
|
+
const result = {
|
|
6
|
+
command: '',
|
|
7
|
+
positional: [],
|
|
8
|
+
options: {},
|
|
9
|
+
};
|
|
10
|
+
let i = 0;
|
|
11
|
+
while (i < args.length) {
|
|
12
|
+
const arg = args[i];
|
|
13
|
+
if (arg.startsWith('--')) {
|
|
14
|
+
const key = arg.slice(2);
|
|
15
|
+
const nextArg = args[i + 1];
|
|
16
|
+
if (nextArg && !nextArg.startsWith('-')) {
|
|
17
|
+
// Support repeated flags: accumulate into array
|
|
18
|
+
const existing = result.options[key];
|
|
19
|
+
if (Array.isArray(existing)) {
|
|
20
|
+
existing.push(nextArg);
|
|
21
|
+
}
|
|
22
|
+
else if (typeof existing === 'string') {
|
|
23
|
+
result.options[key] = [existing, nextArg];
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
result.options[key] = nextArg;
|
|
27
|
+
}
|
|
28
|
+
i += 2;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
result.options[key] = true;
|
|
32
|
+
i++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (!result.command) {
|
|
36
|
+
result.command = arg;
|
|
37
|
+
i++;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
result.positional.push(arg);
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
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.showCountdown = showCountdown;
|
|
7
|
+
const blessed_1 = __importDefault(require("blessed"));
|
|
8
|
+
const lifecycle_1 = require("./lifecycle");
|
|
9
|
+
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
|
|
120
|
+
(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');
|
|
133
|
+
});
|
|
134
|
+
screen.render();
|
|
135
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
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.showDashboard = showDashboard;
|
|
7
|
+
const blessed_1 = __importDefault(require("blessed"));
|
|
8
|
+
const data_1 = require("../data");
|
|
9
|
+
const mascot_1 = require("../mascot");
|
|
10
|
+
const utils_1 = require("../utils");
|
|
11
|
+
const i18n_1 = require("../i18n");
|
|
12
|
+
const lifecycle_1 = require("./lifecycle");
|
|
13
|
+
function showDashboard() {
|
|
14
|
+
return new Promise(resolve => {
|
|
15
|
+
const data = (0, data_1.loadData)();
|
|
16
|
+
// Refresh streak display (don't mutate saved data here)
|
|
17
|
+
let displayStreak = data.streak || 0;
|
|
18
|
+
const today = (0, data_1.getTodayStr)();
|
|
19
|
+
if (data.lastActiveDate && data.lastActiveDate !== today) {
|
|
20
|
+
const last = new Date(data.lastActiveDate);
|
|
21
|
+
const now = new Date(today);
|
|
22
|
+
const diff = Math.floor((now.getTime() - last.getTime()) / 86400000);
|
|
23
|
+
if (diff > 1)
|
|
24
|
+
displayStreak = 0;
|
|
25
|
+
}
|
|
26
|
+
const level = (0, data_1.scoreToLevel)(data.score || 0);
|
|
27
|
+
const prog = (0, data_1.pointsToNextLevel)(data.score || 0);
|
|
28
|
+
const screen = blessed_1.default.screen({
|
|
29
|
+
smartCSR: true,
|
|
30
|
+
title: 'CACE TIMER',
|
|
31
|
+
fullUnicode: true,
|
|
32
|
+
});
|
|
33
|
+
// Mascot
|
|
34
|
+
const mascotContent = data.current ? mascot_1.CACE_FOCUSED : mascot_1.CACE_SLEEPY;
|
|
35
|
+
blessed_1.default.box({
|
|
36
|
+
parent: screen,
|
|
37
|
+
top: 0,
|
|
38
|
+
left: 'center',
|
|
39
|
+
width: 46,
|
|
40
|
+
height: 13,
|
|
41
|
+
align: 'center',
|
|
42
|
+
valign: 'top',
|
|
43
|
+
style: { fg: 'cyan' },
|
|
44
|
+
content: mascotContent,
|
|
45
|
+
});
|
|
46
|
+
// Level / Score / Streak bar
|
|
47
|
+
const streakStr = displayStreak > 1
|
|
48
|
+
? (0, i18n_1.t)('score.streakFire', { days: String(displayStreak) })
|
|
49
|
+
: displayStreak === 1
|
|
50
|
+
? (0, i18n_1.t)('score.newStreak')
|
|
51
|
+
: (0, i18n_1.t)('cmd.status.noStreak');
|
|
52
|
+
const progFilled = Math.min(20, Math.round((prog.current / Math.max(1, prog.needed)) * 20));
|
|
53
|
+
const progressBar = '█'.repeat(progFilled) + '░'.repeat(20 - progFilled);
|
|
54
|
+
blessed_1.default.box({
|
|
55
|
+
parent: screen,
|
|
56
|
+
top: 13,
|
|
57
|
+
left: 'center',
|
|
58
|
+
width: '100%',
|
|
59
|
+
height: 3,
|
|
60
|
+
align: 'center',
|
|
61
|
+
style: { fg: 'yellow', bold: true },
|
|
62
|
+
content: ` ${(0, i18n_1.t)('cmd.status.level', { level: String(level), score: String(data.score || 0) })} | ${streakStr}\n ${progressBar} ${(0, i18n_1.t)('score.progress', { current: String(prog.current), needed: String(prog.needed) })}`,
|
|
63
|
+
});
|
|
64
|
+
// Active task panel
|
|
65
|
+
const hasActive = !!data.current;
|
|
66
|
+
const menuTop = hasActive ? 18 : 17;
|
|
67
|
+
if (hasActive && data.current) {
|
|
68
|
+
const elapsed = Date.now() - new Date(data.current.start).getTime();
|
|
69
|
+
const taskInfo = data.current;
|
|
70
|
+
blessed_1.default.box({
|
|
71
|
+
parent: screen,
|
|
72
|
+
top: 16,
|
|
73
|
+
left: '10%',
|
|
74
|
+
width: '80%',
|
|
75
|
+
height: 2,
|
|
76
|
+
align: 'center',
|
|
77
|
+
border: { type: 'line' },
|
|
78
|
+
style: { fg: 'green', border: { fg: 'green' } },
|
|
79
|
+
content: ` ${(0, i18n_1.t)('cmd.status.inProgress')}: ${taskInfo.task} | ${(0, i18n_1.t)('cmd.mark.elapsed')}: ${(0, utils_1.formatDuration)(elapsed)}`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Menu items - context-dependent
|
|
83
|
+
const menuItems = hasActive
|
|
84
|
+
? [
|
|
85
|
+
{ key: 'm', label: `📍 ${(0, i18n_1.t)('cmd.mark.markPoint')}`, action: 'mark' },
|
|
86
|
+
{ key: 's', label: `⏹ ${(0, i18n_1.t)('cmd.help.stopDesc')}`, action: 'stop' },
|
|
87
|
+
{ key: 'b', label: `📊 ${(0, i18n_1.t)('cmd.help.summaryDesc')}`, action: 'summary' },
|
|
88
|
+
{ key: 'l', label: `📋 ${(0, i18n_1.t)('cmd.help.listDesc')}`, action: 'list' },
|
|
89
|
+
{ key: '?', label: `❓ ${(0, i18n_1.t)('cmd.help.helpDesc')}`, action: 'help' },
|
|
90
|
+
]
|
|
91
|
+
: [
|
|
92
|
+
{ key: 's', label: `🚀 ${(0, i18n_1.t)('cmd.help.startDesc')}`, action: 'start' },
|
|
93
|
+
{ key: 'f', label: `🍅 ${(0, i18n_1.t)('cmd.help.focusDesc')}`, action: 'focus' },
|
|
94
|
+
{ key: 'b', label: `📊 ${(0, i18n_1.t)('cmd.help.summaryDesc')}`, action: 'summary' },
|
|
95
|
+
{ key: 'l', label: `📋 ${(0, i18n_1.t)('cmd.help.listDesc')}`, action: 'list' },
|
|
96
|
+
{ key: '?', label: `❓ ${(0, i18n_1.t)('cmd.help.helpDesc')}`, action: 'help' },
|
|
97
|
+
];
|
|
98
|
+
const menuContent = menuItems.map(m => ` [${m.key}] ${m.label}`).join('\n');
|
|
99
|
+
blessed_1.default.box({
|
|
100
|
+
parent: screen,
|
|
101
|
+
top: menuTop,
|
|
102
|
+
left: 'center',
|
|
103
|
+
width: '80%',
|
|
104
|
+
height: menuItems.length + 1,
|
|
105
|
+
align: 'center',
|
|
106
|
+
style: { fg: 'white' },
|
|
107
|
+
content: menuContent,
|
|
108
|
+
});
|
|
109
|
+
// Bottom hint
|
|
110
|
+
blessed_1.default.box({
|
|
111
|
+
parent: screen,
|
|
112
|
+
bottom: 0,
|
|
113
|
+
left: 'center',
|
|
114
|
+
width: '100%',
|
|
115
|
+
height: 1,
|
|
116
|
+
align: 'center',
|
|
117
|
+
style: { fg: 'gray' },
|
|
118
|
+
content: 'q/Esc to quit',
|
|
119
|
+
});
|
|
120
|
+
// Handle keys
|
|
121
|
+
const allKeys = menuItems.map(m => m.key);
|
|
122
|
+
screen.key([...allKeys], (ch) => {
|
|
123
|
+
const item = menuItems.find(m => m.key === ch);
|
|
124
|
+
if (item) {
|
|
125
|
+
(0, lifecycle_1.destroyScreen)(screen);
|
|
126
|
+
resolve(item.action);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
screen.key(['escape', 'q', 'C-c'], () => {
|
|
130
|
+
(0, lifecycle_1.destroyScreen)(screen);
|
|
131
|
+
resolve('quit');
|
|
132
|
+
});
|
|
133
|
+
screen.render();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============ TUI Helpers ============
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.isInteractiveTerminal = isInteractiveTerminal;
|
|
5
|
+
/** Check if current terminal supports interactive TUI */
|
|
6
|
+
function isInteractiveTerminal() {
|
|
7
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
8
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type blessed from 'blessed';
|
|
2
|
+
type Screen = blessed.Widgets.Screen;
|
|
3
|
+
/**
|
|
4
|
+
* Single source of truth for tearing down a blessed screen without
|
|
5
|
+
* leaving the parent TTY in a broken state.
|
|
6
|
+
*
|
|
7
|
+
* blessed calls `process.stdin.setRawMode(true)` on entry. Depending on
|
|
8
|
+
* the blessed version and the exact exit path, `screen.destroy()` does not
|
|
9
|
+
* always restore the parent TTY — leaving the user's terminal in raw mode
|
|
10
|
+
* after Ctrl+C / quit. This helper runs the documented cleanup chain
|
|
11
|
+
* (`screen.destroy` + raw-mode off + cursor reset) so every exit path
|
|
12
|
+
* (Ctrl+C, q/Esc, timeout, callback throw) behaves the same.
|
|
13
|
+
*
|
|
14
|
+
* Every cleanup step is wrapped in try/catch — screen may already be
|
|
15
|
+
* destroyed (double-fire on race conditions), stdin may not be a TTY,
|
|
16
|
+
* and `screen.program` may have been torn down by blessed itself.
|
|
17
|
+
*/
|
|
18
|
+
export declare function destroyScreen(screen: Screen): void;
|
|
19
|
+
/**
|
|
20
|
+
* Install SIGINT/SIGTERM handlers that run the standard blessed cleanup
|
|
21
|
+
* and then exit with the conventional signal status (130 / 143).
|
|
22
|
+
*
|
|
23
|
+
* Returns a `dispose()` function so callers can remove the handlers when
|
|
24
|
+
* the screen is closed normally. Installing again while a previous
|
|
25
|
+
* instance is active disposes that instance first (idempotent replace).
|
|
26
|
+
*/
|
|
27
|
+
export declare function installSignalCleanup(screen: Screen): () => void;
|
|
28
|
+
export {};
|