@cacinie/cace-timer 1.3.1 → 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 CHANGED
@@ -12,20 +12,17 @@ npm install -g @cacinie/cace-timer
12
12
  ```
13
13
 
14
14
  ```
15
- ╭──────────╮
16
- │ ● ● │
17
- │ ▽ │
18
- │ ─── │
19
- ╰──┬────┬──╯
20
- ╭──┴────┴──╮
21
- │ ╔════╗ │
22
- CACE║ │
23
- │ ╚════╝ │
24
- ╰────┬┬────╯
25
- ╭──┘└──╮
26
- │ │
27
- ╰──┬┬──╯
28
- ╭┘└╮
15
+ _,
16
+ //
17
+ .-'/`--.
18
+ / //\ \
19
+ /// \ h* \
20
+ (|:(O')-(O'):|)
21
+ |:(// //):|
22
+ \: \ v / :/
23
+ '- `._.' -'
24
+ / >o< \
25
+ '-----'
29
26
  CACE TIMER
30
27
  ```
31
28
 
@@ -210,15 +207,37 @@ Consecutive days with at least one completed task. Break a day and the streak re
210
207
 
211
208
  ## CACE Mascot
212
209
 
213
- CACE has 5 moods that change based on context:
210
+ CACE uses **MINT**, a teal bob-haired girl with delicate round glasses, a slim,
211
+ rounded jaw and 16 expressions. The dashboard and countdown blink gently;
212
+ one-shot commands print immediately. The artwork automatically steps down from
213
+ full to compact, tiny or text-only as the terminal shrinks.
214
214
 
215
- | Mood | Eyes | Mouth | When |
216
- |------|------|-------|------|
217
- | Normal | | ─── | Default display |
218
- | Happy | | ◡◡◡ | Task completed |
219
- | Focused | | ▬▬▬ | Pomodoro / active task |
220
- | Celebrating | ◉ | ▽△▽ | Points earned + arms up |
221
- | Sleepy | | ~~ | No active task |
215
+ ```bash
216
+ tk mascot preview # live preview; n/p selects an expression
217
+ tk mascot preview --all --no-tui # export all 16 expressions
218
+ tk mascot preview --size compact --ascii # ASCII art without Unicode glyphs
219
+ tk --no-animation # static mascot, live timer
220
+ tk --no-tui # plain command output
221
+ tk --color never # no foreground colours
222
+ tk --theme light # light-terminal palette
223
+ ```
224
+
225
+ `--ascii` changes mascot art; task names and translated UI text keep their original
226
+ characters. Colour modes are `auto`, `always` (truecolor for plain output), `256`
227
+ and `never`. Auto mode respects `NO_COLOR`, `TERM=dumb` and redirected output.
228
+ The blessed TUI maps colours to its supported terminal palette. No terminal
229
+ background, shell configuration or Python runtime is required by the application.
230
+ Preview with `--all` or an explicit `--size` is a static export. Add
231
+ `--expression little_smile` (or another expression key) to select one portrait.
232
+
233
+ Cancel a countdown with q/Esc/Ctrl+C. Cancellation exits 130 and retains the
234
+ active record; use `tk stop` to finish it. Rendering failures exit 1 without
235
+ adding a completed session. SIGTERM exits 143. Normal q/Esc in the dashboard or
236
+ preview exits 0. Normal timer completion rings once in a supported TTY.
237
+
238
+ For contributors: `npm test`, `npm run test:cli`, and, on POSIX with Python 3,
239
+ `npm run test:pty`. Tests use temporary timer files. See
240
+ [the design and reference](docs/design/mint.md).
222
241
 
223
242
  ## Data
224
243
 
@@ -456,15 +475,27 @@ tk stop
456
475
 
457
476
  ## CACE 吉祥物
458
477
 
459
- CACE 5 种心情,根据场景变化:
478
+ CACE 采用 **MINT 蓝绿短 bob 女孩**:细圆框、大眼、平顺内收的下颌与小圆下巴,
479
+ 无鼻子、无手部,共 16 种表情。仪表盘与番茄钟自然眨眼;开始/恢复等一次性命令即时输出。
480
+ 终端缩小时自动切换完整、紧凑、迷你或纯文字布局。
481
+
482
+ ```bash
483
+ tk mascot preview # 动态预览,n/p 切换表情
484
+ tk mascot preview --all --no-tui # 静态输出 16 种表情
485
+ tk mascot preview --size compact --ascii # 纯 ASCII 角色
486
+ tk --no-animation # 关闭动画,计时继续
487
+ tk --no-tui # 普通命令输出
488
+ tk --color never # 无颜色
489
+ tk --theme light # 浅色终端配色
490
+ ```
491
+
492
+ `--ascii` 只替换角色字符,任务名和中文文案保留原文。颜色支持 `auto|always|256|never`;
493
+ 自动模式尊重 `NO_COLOR`、`TERM=dumb` 和输出重定向。TUI 颜色适配 blessed 支持的终端调色板。
494
+ 应用不需要 Python,也不修改终端背景或 shell 配置。
460
495
 
461
- | 心情 | 眼睛 | 嘴巴 | 时机 |
462
- |------|------|------|------|
463
- | 正常 | | ─── | 默认显示 |
464
- | 开心 | ★ ★ | ◡◡◡ | 任务完成 |
465
- | 专注 | ◆ ◆ | ▬▬▬ | 番茄钟 / 活跃任务 |
466
- | 庆祝 | ◉ ◉ | ▽△▽ | 获得积分 + 举手 |
467
- | 困了 | ─ ─ | ~~ | 无活跃任务 |
496
+ 番茄钟中 q/Esc/Ctrl+C 取消并返回 130,当前记录保留,可用 `tk stop` 结束;
497
+ 渲染错误返回 1,不写入已完成历史;SIGTERM 返回 143。仪表盘和预览中 q/Esc 正常退出。
498
+ `--all` 或显式 `--size` 为静态导出,可加 `--expression <表情名称>` 指定单张。
468
499
 
469
500
  ## 数据
470
501
 
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.cmdExport = cmdExport;
37
+ const terminal_1 = require("../terminal");
37
38
  const fs = __importStar(require("fs"));
38
39
  const data_1 = require("../data");
39
40
  const mascot_1 = require("../mascot");
@@ -57,7 +58,7 @@ function cmdExport(options) {
57
58
  output = toCsv(sessions);
58
59
  }
59
60
  else {
60
- console.log(`\x1b[33m${(0, i18n_1.t)('cmd.export.unsupportedFormat')}\x1b[0m`);
61
+ console.log((0, terminal_1.paint)(`${(0, i18n_1.t)('cmd.export.unsupportedFormat')}`, 'yellow'));
61
62
  return;
62
63
  }
63
64
  if (options.output) {
@@ -73,7 +74,7 @@ function cmdExport(options) {
73
74
  function toCsv(sessions) {
74
75
  const header = 'ID,Task,Start,End,Duration,Tags,Marks\n';
75
76
  const rows = sessions
76
- .map(s => {
77
+ .map((s) => {
77
78
  const duration = s.end
78
79
  ? (0, utils_1.formatDuration)(new Date(s.end).getTime() - new Date(s.start).getTime())
79
80
  : 'running';
@@ -97,7 +98,7 @@ function toMarkdown(sessions) {
97
98
  const duration = s.end
98
99
  ? (0, utils_1.formatDuration)(new Date(s.end).getTime() - new Date(s.start).getTime())
99
100
  : (0, i18n_1.t)('common.running');
100
- const tags = s.tags.map(tg => '#' + tg).join(' ');
101
+ const tags = s.tags.map((tg) => '#' + tg).join(' ');
101
102
  md += `| ${i + 1} | ${s.task} | ${(0, utils_1.formatTime)(s.start)} | ${duration} | ${tags} |\n`;
102
103
  });
103
104
  return md;
@@ -66,6 +66,11 @@ function cmdHelp() {
66
66
  console.log(` ${(0, i18n_1.t)('cmd.help.focusDesc')}`);
67
67
  console.log();
68
68
  console.log(` ${(0, i18n_1.t)('cmd.help.lang')}`);
69
+ console.log(` ${(0, i18n_1.t)('cmd.help.display')}`);
70
+ console.log(' --no-animation --no-tui --ascii');
71
+ console.log(' --color auto|always|256|never --theme dark|light');
72
+ console.log(' tk mascot preview [--all] [--expression <name>] [--size full|compact|tiny]');
73
+ console.log(` ${(0, i18n_1.t)('mascot.help')}`);
69
74
  console.log();
70
75
  console.log(` ${(0, i18n_1.t)('cmd.help.help')}`);
71
76
  console.log(` ${(0, i18n_1.t)('cmd.help.helpDesc')}`);
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.cmdMark = cmdMark;
4
+ const terminal_1 = require("../terminal");
4
5
  const data_1 = require("../data");
5
6
  const mascot_1 = require("../mascot");
6
7
  const utils_1 = require("../utils");
@@ -8,7 +9,7 @@ const i18n_1 = require("../i18n");
8
9
  async function cmdMark(note) {
9
10
  const data = (0, data_1.loadData)();
10
11
  if (!data.current) {
11
- console.log(`\x1b[33m⚠ ${(0, i18n_1.t)('cmd.mark.noActive')}\x1b[0m`);
12
+ console.log((0, terminal_1.paint)(`⚠ ${(0, i18n_1.t)('cmd.mark.noActive')}`, 'yellow'));
12
13
  return;
13
14
  }
14
15
  const mark = {
@@ -0,0 +1,5 @@
1
+ export declare function cmdMascot(options: {
2
+ all?: boolean;
3
+ expression?: string;
4
+ size?: string;
5
+ }): Promise<void>;
@@ -0,0 +1,77 @@
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.cmdMascot = cmdMascot;
7
+ const blessed_1 = __importDefault(require("blessed"));
8
+ const mint_1 = require("../mascot/assets/mint");
9
+ const frames_1 = require("../mascot/frames");
10
+ const terminal_1 = require("../terminal");
11
+ const i18n_1 = require("../i18n");
12
+ const tui_1 = require("../tui");
13
+ const surface_1 = require("../tui/surface");
14
+ const animation_1 = require("../tui/animation");
15
+ const lifecycle_1 = require("../tui/lifecycle");
16
+ async function cmdMascot(options) {
17
+ if (options.expression && !mint_1.EXPRESSIONS.some((item) => item.key === options.expression)) {
18
+ throw new Error((0, i18n_1.t)('mascot.invalidExpression', { names: mint_1.EXPRESSIONS.map((item) => item.key).join(', ') }));
19
+ }
20
+ if (options.size && !['full', 'compact', 'tiny'].includes(options.size)) {
21
+ throw new Error((0, i18n_1.t)('mascot.invalidSize'));
22
+ }
23
+ const display = (0, terminal_1.getDisplay)();
24
+ if (options.all || options.size || !(0, tui_1.isInteractiveTerminal)()) {
25
+ const selected = options.all
26
+ ? mint_1.EXPRESSIONS
27
+ : mint_1.EXPRESSIONS.filter((item) => item.key === (options.expression ?? 'little_smile'));
28
+ for (const item of selected) {
29
+ console.log(`${item.key} / ${item[(0, i18n_1.getLocale)()]}`);
30
+ console.log((0, frames_1.renderFrame)((0, frames_1.getFrame)({
31
+ expression: item.key,
32
+ size: (options.size ?? 'full'),
33
+ ascii: display.ascii,
34
+ }), display));
35
+ console.log();
36
+ }
37
+ return;
38
+ }
39
+ await new Promise((resolve, reject) => {
40
+ const screen = blessed_1.default.screen({ smartCSR: true, title: 'CACE / MINT', fullUnicode: true });
41
+ const surface = (0, surface_1.createSurface)(screen);
42
+ let loop = undefined;
43
+ let index = Math.max(0, mint_1.EXPRESSIONS.findIndex((item) => item.key === options.expression));
44
+ let settled = false;
45
+ const disposeSignals = (0, lifecycle_1.installSignalCleanup)(screen);
46
+ screen.once('destroy', () => {
47
+ loop?.dispose();
48
+ disposeSignals();
49
+ });
50
+ const finish = (error) => {
51
+ if (settled)
52
+ return;
53
+ settled = true;
54
+ loop?.dispose();
55
+ (0, lifecycle_1.destroyScreen)(screen);
56
+ if (error !== undefined)
57
+ reject(error);
58
+ else
59
+ resolve();
60
+ };
61
+ screen.key(['escape', 'q'], () => finish());
62
+ screen.key(['C-c'], () => process.emit('SIGINT'));
63
+ screen.key(['right', 'n', 'space'], () => {
64
+ index = (index + 1) % mint_1.EXPRESSIONS.length;
65
+ loop?.refresh();
66
+ });
67
+ screen.key(['left', 'p'], () => {
68
+ index = (index + mint_1.EXPRESSIONS.length - 1) % mint_1.EXPRESSIONS.length;
69
+ loop?.refresh();
70
+ });
71
+ screen.on('resize', () => loop?.refresh());
72
+ loop = (0, animation_1.startRefreshLoop)((elapsed) => {
73
+ const item = mint_1.EXPRESSIONS[index];
74
+ surface.render(item.key, elapsed, [`C A C E / M I N T`, `${index + 1}/16 ${item[(0, i18n_1.getLocale)()]}`, item.key], (0, i18n_1.t)('mascot.controls'));
75
+ }, finish, display.animation ? 100 : 1000);
76
+ });
77
+ }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.cmdPomodoro = cmdPomodoro;
4
+ const terminal_1 = require("../terminal");
4
5
  const data_1 = require("../data");
5
6
  const mascot_1 = require("../mascot");
6
7
  const utils_1 = require("../utils");
@@ -9,19 +10,23 @@ const tui_1 = require("../tui");
9
10
  const countdown_1 = require("../tui/countdown");
10
11
  function getRandomEncouragement() {
11
12
  const keys = [
12
- 'cmd.focus.encourage1', 'cmd.focus.encourage2', 'cmd.focus.encourage3',
13
- 'cmd.focus.encourage4', 'cmd.focus.encourage5', 'cmd.focus.encourage6',
13
+ 'cmd.focus.encourage1',
14
+ 'cmd.focus.encourage2',
15
+ 'cmd.focus.encourage3',
16
+ 'cmd.focus.encourage4',
17
+ 'cmd.focus.encourage5',
18
+ 'cmd.focus.encourage6',
14
19
  ];
15
20
  return (0, i18n_1.t)(keys[Math.floor(Math.random() * keys.length)]);
16
21
  }
17
22
  function normalizeTags(rawTag) {
18
23
  if (Array.isArray(rawTag)) {
19
- return rawTag.flatMap(tg => tg.split(',').map(s => s.trim())).filter(Boolean);
24
+ return rawTag.flatMap((tg) => tg.split(',').map((s) => s.trim())).filter(Boolean);
20
25
  }
21
26
  if (typeof rawTag === 'string') {
22
27
  return rawTag
23
28
  .split(',')
24
- .map(s => s.trim())
29
+ .map((s) => s.trim())
25
30
  .filter(Boolean);
26
31
  }
27
32
  return [];
@@ -33,18 +38,19 @@ function formatMsAsClock(ms) {
33
38
  return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
34
39
  }
35
40
  function runCountdown(totalMs) {
36
- return new Promise(resolve => {
41
+ return new Promise((resolve) => {
37
42
  const startTime = Date.now();
38
43
  const barWidth = 20;
39
44
  const interval = setInterval(() => {
40
45
  const elapsed = Date.now() - startTime;
41
- const remaining = Math.max(0, totalMs - elapsed);
42
46
  const progress = Math.min(1, elapsed / totalMs);
43
47
  const filled = Math.floor(progress * barWidth);
44
48
  const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
45
49
  const elapsedStr = formatMsAsClock(elapsed);
46
50
  const totalStr = formatMsAsClock(totalMs);
47
- process.stdout.write(`\r [${bar}] ${elapsedStr} / ${totalStr} `);
51
+ if (process.stdout.isTTY && process.env.TERM !== 'dumb') {
52
+ process.stdout.write(`\r [${bar}] ${elapsedStr} / ${totalStr} `);
53
+ }
48
54
  if (elapsed >= totalMs) {
49
55
  clearInterval(interval);
50
56
  process.stdout.write('\n');
@@ -66,7 +72,7 @@ function runCountdown(totalMs) {
66
72
  async function cmdPomodoro(task, options) {
67
73
  const data = (0, data_1.loadData)();
68
74
  if (data.current) {
69
- console.log(`\x1b[33m⚠ ${(0, i18n_1.t)('common.alreadyActive')}\x1b[0m`);
75
+ console.log((0, terminal_1.paint)(`⚠ ${(0, i18n_1.t)('common.alreadyActive')}`, 'yellow'));
70
76
  return;
71
77
  }
72
78
  const workMin = options.work ?? 25;
@@ -94,14 +100,15 @@ async function cmdPomodoro(task, options) {
94
100
  console.log(` ${getRandomEncouragement()}`);
95
101
  if ((0, tui_1.isInteractiveTerminal)()) {
96
102
  // TUI countdown with mascot
97
- await new Promise(resolve => {
98
- (0, countdown_1.showCountdown)({
99
- totalMs: workMin * 60 * 1000,
100
- label: `${(0, i18n_1.t)('cmd.pomodoro.round', { round: String(round), total: String(rounds) })} [${(0, i18n_1.t)('cmd.pomodoro.workPhase')}]`,
101
- mascots: [mascot_1.CACE_FOCUSED, mascot_1.CACE_HAPPY],
102
- onDone: resolve,
103
- });
103
+ const completed = await (0, countdown_1.showCountdown)({
104
+ totalMs: workMin * 60 * 1000,
105
+ label: `${task} / ${(0, i18n_1.t)('cmd.pomodoro.round', { round: String(round), total: String(rounds) })}`,
104
106
  });
107
+ if (!completed) {
108
+ console.log((0, i18n_1.t)('tui.cancelled'));
109
+ process.exitCode = 130;
110
+ return;
111
+ }
105
112
  }
106
113
  else {
107
114
  await runCountdown(workMin * 60 * 1000);
@@ -112,7 +119,8 @@ async function cmdPomodoro(task, options) {
112
119
  data.current = null;
113
120
  totalWorkMs += workMin * 60 * 1000;
114
121
  (0, data_1.saveData)(data);
115
- process.stdout.write('\x07'); // bell
122
+ if (process.stdout.isTTY && process.env.TERM !== 'dumb')
123
+ process.stdout.write('\x07');
116
124
  (0, mascot_1.showCaceSmall)((0, i18n_1.t)('cmd.pomodoro.workDone'), 'happy');
117
125
  console.log();
118
126
  if (round < rounds) {
@@ -120,7 +128,8 @@ async function cmdPomodoro(task, options) {
120
128
  console.log(` ${(0, i18n_1.t)('cmd.pomodoro.breakTime')}`);
121
129
  await runCountdown(breakMin * 60 * 1000);
122
130
  totalBreakMs += breakMin * 60 * 1000;
123
- process.stdout.write('\x07');
131
+ if (process.stdout.isTTY && process.env.TERM !== 'dumb')
132
+ process.stdout.write('\x07');
124
133
  (0, mascot_1.showCaceSmall)((0, i18n_1.t)('cmd.pomodoro.breakDone'), 'normal');
125
134
  console.log();
126
135
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.cmdResume = cmdResume;
4
+ const terminal_1 = require("../terminal");
4
5
  const data_1 = require("../data");
5
6
  const mascot_1 = require("../mascot");
6
7
  const utils_1 = require("../utils");
@@ -8,7 +9,7 @@ const i18n_1 = require("../i18n");
8
9
  async function cmdResume(options) {
9
10
  const data = (0, data_1.loadData)();
10
11
  if (data.current) {
11
- console.log(`\x1b[33m⚠ ${(0, i18n_1.t)('common.alreadyActive')}\x1b[0m`);
12
+ console.log((0, terminal_1.paint)(`⚠ ${(0, i18n_1.t)('common.alreadyActive')}`, 'yellow'));
12
13
  return;
13
14
  }
14
15
  if (data.history.length === 0) {
@@ -22,7 +23,7 @@ async function cmdResume(options) {
22
23
  source = data.history[0];
23
24
  }
24
25
  else if (options.id) {
25
- source = data.history.find(s => s.id === options.id);
26
+ source = data.history.find((s) => s.id === options.id);
26
27
  }
27
28
  if (!source) {
28
29
  console.log();
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.cmdStart = cmdStart;
4
+ const terminal_1 = require("../terminal");
4
5
  const data_1 = require("../data");
5
6
  const mascot_1 = require("../mascot");
6
7
  const utils_1 = require("../utils");
@@ -8,19 +9,19 @@ const i18n_1 = require("../i18n");
8
9
  async function cmdStart(task, options) {
9
10
  const data = (0, data_1.loadData)();
10
11
  if (data.current) {
11
- console.log(`\x1b[33m⚠ ${(0, i18n_1.t)('common.alreadyActive')}\x1b[0m`);
12
+ console.log((0, terminal_1.paint)(`⚠ ${(0, i18n_1.t)('common.alreadyActive')}`, 'yellow'));
12
13
  return;
13
14
  }
14
15
  // Normalize tags: support --tag dev --tag api and --tag dev,api
15
16
  let tags = [];
16
17
  const rawTag = options.tag;
17
18
  if (Array.isArray(rawTag)) {
18
- tags = rawTag.flatMap(tag => tag.split(',').map(s => s.trim())).filter(Boolean);
19
+ tags = rawTag.flatMap((tag) => tag.split(',').map((s) => s.trim())).filter(Boolean);
19
20
  }
20
21
  else if (typeof rawTag === 'string') {
21
22
  tags = rawTag
22
23
  .split(',')
23
- .map(s => s.trim())
24
+ .map((s) => s.trim())
24
25
  .filter(Boolean);
25
26
  }
26
27
  const session = {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.cmdStop = cmdStop;
4
+ const terminal_1 = require("../terminal");
4
5
  const data_1 = require("../data");
5
6
  const mascot_1 = require("../mascot");
6
7
  const utils_1 = require("../utils");
@@ -10,7 +11,7 @@ const reflection_1 = require("../tui/reflection");
10
11
  async function cmdStop(options) {
11
12
  const data = (0, data_1.loadData)();
12
13
  if (!data.current) {
13
- console.log(`\x1b[33m⚠ ${(0, i18n_1.t)('cmd.stop.noActive')}\x1b[0m`);
14
+ console.log((0, terminal_1.paint)(`⚠ ${(0, i18n_1.t)('cmd.stop.noActive')}`, 'yellow'));
14
15
  return;
15
16
  }
16
17
  const session = data.current;
@@ -68,25 +69,25 @@ async function cmdStop(options) {
68
69
  // Efficiency display
69
70
  if (hasEstimate) {
70
71
  let effEmoji = '⭐';
71
- let effColor = '\x1b[32m';
72
+ let effColor = 'green';
72
73
  if (efficiency < 50) {
73
74
  effEmoji = '💀';
74
- effColor = '\x1b[31m';
75
+ effColor = 'red';
75
76
  }
76
77
  else if (efficiency < 80) {
77
78
  effEmoji = '💪';
78
- effColor = '\x1b[33m';
79
+ effColor = 'yellow';
79
80
  }
80
81
  else if (efficiency >= 100) {
81
82
  effEmoji = '🏆';
82
83
  }
83
- console.log(` ${effEmoji} ${(0, i18n_1.t)('cmd.stop.efficiencyScore')}: ${effColor}${efficiency}%\x1b[0m`);
84
+ console.log(` ${effEmoji} ${(0, i18n_1.t)('cmd.stop.efficiencyScore')}: ${(0, terminal_1.paint)(`${efficiency}%`, effColor)}`);
84
85
  }
85
86
  else {
86
87
  console.log(` ⏱ ${(0, i18n_1.t)('cmd.stop.noEstimate')}`);
87
88
  }
88
89
  // Points & Level
89
- console.log(` \x1b[33m${(0, i18n_1.t)('score.earned', { points: String(points) })}\x1b[0m`);
90
+ console.log((0, terminal_1.paint)(` ${(0, i18n_1.t)('score.earned', { points: String(points) })}`, 'yellow'));
90
91
  console.log(` ${(0, i18n_1.t)('cmd.status.level', { level: String(level), score: String(data.score) })}`);
91
92
  console.log(` ${(0, i18n_1.t)('score.progress', { current: String(levelProgress.current), needed: String(levelProgress.needed) })}`);
92
93
  // Streak
@@ -14,7 +14,7 @@ function computeDailyHours(sessions, days) {
14
14
  day.setHours(0, 0, 0, 0);
15
15
  const dayEnd = new Date(day);
16
16
  dayEnd.setDate(dayEnd.getDate() + 1);
17
- const daySessions = sessions.filter(s => s.start >= day.toISOString() && s.start < dayEnd.toISOString());
17
+ const daySessions = sessions.filter((s) => s.start >= day.toISOString() && s.start < dayEnd.toISOString());
18
18
  const totalMs = daySessions.reduce((acc, s) => acc + (new Date(s.end).getTime() - new Date(s.start).getTime()), 0);
19
19
  result.push({
20
20
  label: day.toLocaleDateString((0, i18n_2.getLocale)() === 'zh' ? 'zh-CN' : 'en-US', {
@@ -30,33 +30,33 @@ function computeDailyHours(sessions, days) {
30
30
  const i18n_2 = require("../i18n");
31
31
  function cmdSummary(options) {
32
32
  const data = (0, data_1.loadData)();
33
- const allCompleted = data.history.filter(s => s.end); // all completed, for daily chart
33
+ const allCompleted = data.history.filter((s) => s.end); // all completed, for daily chart
34
34
  let sessions = allCompleted;
35
35
  // Time filtering
36
36
  const now = new Date();
37
37
  if (options.today) {
38
38
  const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();
39
- sessions = sessions.filter(s => s.start >= todayStart);
39
+ sessions = sessions.filter((s) => s.start >= todayStart);
40
40
  }
41
41
  else if (options.week) {
42
42
  // Start from Monday
43
43
  const dayOfWeek = now.getDay(); // 0=Sun, 1=Mon...
44
44
  const mondayOffset = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
45
45
  const weekStart = new Date(now.getFullYear(), now.getMonth(), now.getDate() - mondayOffset);
46
- sessions = sessions.filter(s => s.start >= weekStart.toISOString());
46
+ sessions = sessions.filter((s) => s.start >= weekStart.toISOString());
47
47
  }
48
48
  else if (options.month) {
49
49
  const monthStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
50
- sessions = sessions.filter(s => s.start >= monthStart);
50
+ sessions = sessions.filter((s) => s.start >= monthStart);
51
51
  }
52
52
  else {
53
53
  // Default to today
54
54
  const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();
55
- sessions = sessions.filter(s => s.start >= todayStart);
55
+ sessions = sessions.filter((s) => s.start >= todayStart);
56
56
  }
57
57
  // Tag filtering
58
58
  if (options.tag) {
59
- sessions = sessions.filter(s => s.tags.includes(options.tag));
59
+ sessions = sessions.filter((s) => s.tags.includes(options.tag));
60
60
  }
61
61
  console.log();
62
62
  if (sessions.length === 0) {
@@ -66,7 +66,7 @@ function cmdSummary(options) {
66
66
  }
67
67
  // Compute stats
68
68
  const totalSessions = sessions.length;
69
- const durations = sessions.map(s => new Date(s.end).getTime() - new Date(s.start).getTime());
69
+ const durations = sessions.map((s) => new Date(s.end).getTime() - new Date(s.start).getTime());
70
70
  const totalDuration = durations.reduce((a, b) => a + b, 0);
71
71
  const avgDuration = totalDuration / totalSessions;
72
72
  // Tag distribution
@@ -111,7 +111,7 @@ function cmdSummary(options) {
111
111
  }
112
112
  // Daily bar chart
113
113
  console.log(` ${(0, i18n_1.t)('cmd.summary.dailyHours')} (${(0, i18n_1.t)('cmd.summary.dailyHoursLabel')}):`);
114
- const maxHours = Math.max(...dailyHours.map(d => d.hours), 1);
114
+ const maxHours = Math.max(...dailyHours.map((d) => d.hours), 1);
115
115
  for (const day of dailyHours) {
116
116
  const barWidth = Math.round((day.hours / maxHours) * 20);
117
117
  const bar = '█'.repeat(barWidth);
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.cmdSync = cmdSync;
37
+ const terminal_1 = require("../terminal");
37
38
  const fs = __importStar(require("fs"));
38
39
  const path = __importStar(require("path"));
39
40
  const data_1 = require("../data");
@@ -41,14 +42,14 @@ const mascot_1 = require("../mascot");
41
42
  const i18n_1 = require("../i18n");
42
43
  function cmdSync(filePath) {
43
44
  if (!filePath || filePath.trim() === '') {
44
- console.log(`\x1b[33m${(0, i18n_1.t)('cmd.sync.emptyPath')}\x1b[0m`);
45
+ console.log((0, terminal_1.paint)(`${(0, i18n_1.t)('cmd.sync.emptyPath')}`, 'yellow'));
45
46
  return;
46
47
  }
47
48
  const data = (0, data_1.loadData)();
48
49
  const absPath = path.resolve(filePath);
49
50
  const parentDir = path.dirname(absPath);
50
51
  if (!fs.existsSync(parentDir)) {
51
- console.log(`\x1b[33m${(0, i18n_1.t)('cmd.sync.dirNotExist', { dir: parentDir })}\x1b[0m`);
52
+ console.log((0, terminal_1.paint)(`${(0, i18n_1.t)('cmd.sync.dirNotExist', { dir: parentDir })}`, 'yellow'));
52
53
  return;
53
54
  }
54
55
  data.syncPath = absPath;
package/dist/data.js CHANGED
@@ -128,7 +128,6 @@ function pointsToNextLevel(score) {
128
128
  for (let i = 1; i < level; i++) {
129
129
  totalForCurrent += i * 20;
130
130
  }
131
- const totalForNext = totalForCurrent + level * 20;
132
131
  return {
133
132
  current: score - totalForCurrent,
134
133
  needed: level * 20,
package/dist/i18n.js CHANGED
@@ -29,6 +29,17 @@ function resolveLocale(cliFlag, storedLang) {
29
29
  // ---- String maps ----
30
30
  const strings = {
31
31
  zh: {
32
+ 'tui.ready': '准备好开始了。',
33
+ 'tui.streak': '连续 {days} 天',
34
+ 'tui.quit': 'q/Esc 退出 · Ctrl+C 中断',
35
+ 'tui.cancel': 'q/Esc/Ctrl+C 取消',
36
+ 'tui.oneThing': '一次专注一件事。',
37
+ 'tui.cancelled': '计时已取消;当前记录保留,可用 tk stop 结束。',
38
+ 'mascot.controls': 'q 退出 · n/p 切换表情',
39
+ 'mascot.help': '查看 MINT 的 16 种表情与眨眼动画',
40
+ 'mascot.invalidExpression': '表情名称可选:{names}',
41
+ 'mascot.invalidSize': '尺寸可选:full、compact、tiny',
42
+ 'cmd.help.display': '显示设置(仅本次运行;--ascii 只替换角色字符):',
32
43
  // Greetings
33
44
  'greeting.lateNight': '夜深了',
34
45
  'greeting.morning': '早上好',
@@ -214,6 +225,17 @@ const strings = {
214
225
  'error.useHelp': '使用 tk help 查看帮助',
215
226
  },
216
227
  en: {
228
+ 'tui.ready': 'Ready when you are.',
229
+ 'tui.streak': '{days} day streak',
230
+ 'tui.quit': 'q/Esc quit | Ctrl+C interrupt',
231
+ 'tui.cancel': 'q/Esc/Ctrl+C cancel',
232
+ 'tui.oneThing': 'One thing at a time.',
233
+ 'tui.cancelled': 'Timer cancelled; the active record is kept. Use tk stop to finish it.',
234
+ 'mascot.controls': 'q quit | n/p switch expression',
235
+ 'mascot.help': 'Preview all 16 MINT expressions and blinking',
236
+ 'mascot.invalidExpression': 'Choose an expression: {names}',
237
+ 'mascot.invalidSize': 'Choose a size: full, compact, tiny',
238
+ 'cmd.help.display': 'Display options (this run only; --ascii changes mascot art):',
217
239
  // Greetings
218
240
  'greeting.lateNight': 'Late night',
219
241
  'greeting.morning': 'Good morning',
@@ -385,7 +407,7 @@ const strings = {
385
407
  'mascot.encourage3': 'CACE: Hang in there!',
386
408
  'mascot.focused': '😤 Focused...',
387
409
  'mascot.celebrating': '🎉 Awesome!',
388
- 'mascot.disappointed': '😴 Don\'t worry, try again tomorrow!',
410
+ 'mascot.disappointed': "😴 Don't worry, try again tomorrow!",
389
411
  // Status enhancements
390
412
  'cmd.status.level': '📊 Lv.{level} | {score} pts',
391
413
  'cmd.status.streak': '{fire} {days} day streak',