@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.
@@ -6,130 +6,84 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.showDashboard = showDashboard;
7
7
  const blessed_1 = __importDefault(require("blessed"));
8
8
  const data_1 = require("../data");
9
- const mascot_1 = require("../mascot");
10
9
  const utils_1 = require("../utils");
11
10
  const i18n_1 = require("../i18n");
11
+ const terminal_1 = require("../terminal");
12
12
  const lifecycle_1 = require("./lifecycle");
13
+ const animation_1 = require("./animation");
14
+ const surface_1 = require("./surface");
13
15
  function showDashboard() {
14
- return new Promise(resolve => {
16
+ return new Promise((resolve, reject) => {
15
17
  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) })}`,
18
+ const screen = blessed_1.default.screen({ smartCSR: true, title: 'CACE TIMER', fullUnicode: true });
19
+ const surface = (0, surface_1.createSurface)(screen);
20
+ let loop = undefined;
21
+ let settled = false;
22
+ const disposeSignals = (0, lifecycle_1.installSignalCleanup)(screen);
23
+ screen.once('destroy', () => {
24
+ loop?.dispose();
25
+ disposeSignals();
63
26
  });
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
27
+ const finish = (action) => {
28
+ if (settled)
29
+ return;
30
+ settled = true;
31
+ loop?.dispose();
32
+ (0, lifecycle_1.destroyScreen)(screen);
33
+ resolve(action);
34
+ };
35
+ const fail = (error) => {
36
+ if (settled)
37
+ return;
38
+ settled = true;
39
+ loop?.dispose();
40
+ (0, lifecycle_1.destroyScreen)(screen);
41
+ reject(error);
42
+ };
43
+ const menu = data.current
84
44
  ? [
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' },
45
+ { key: 'm', label: (0, i18n_1.t)('cmd.mark.markPoint'), action: 'mark' },
46
+ { key: 's', label: (0, i18n_1.t)('cmd.help.stopDesc'), action: 'stop' },
90
47
  ]
91
48
  : [
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' },
49
+ { key: 's', label: (0, i18n_1.t)('cmd.help.startDesc'), action: 'start' },
50
+ { key: 'f', label: (0, i18n_1.t)('cmd.help.focusDesc'), action: 'focus' },
97
51
  ];
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,
52
+ menu.push({ key: 'b', label: (0, i18n_1.t)('cmd.help.summaryDesc'), action: 'summary' }, { key: 'l', label: (0, i18n_1.t)('cmd.help.listDesc'), action: 'list' }, { key: '?', label: (0, i18n_1.t)('cmd.help.helpDesc'), action: 'help' });
53
+ screen.key(menu.map((item) => item.key), (key) => {
54
+ const item = menu.find((item) => item.key === key);
55
+ if (item)
56
+ finish(item.action);
108
57
  });
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();
58
+ screen.key(['escape', 'q'], () => finish('quit'));
59
+ screen.key(['C-c'], () => process.emit('SIGINT'));
60
+ screen.on('resize', () => loop?.refresh());
61
+ const today = (0, data_1.getTodayStr)();
62
+ let streak = data.streak || 0;
63
+ if (data.lastActiveDate && Date.parse(today) - Date.parse(data.lastActiveDate) > 86400000)
64
+ streak = 0;
65
+ loop = (0, animation_1.startRefreshLoop)((elapsedMs) => {
66
+ const task = data.current;
67
+ const primary = task
68
+ ? [
69
+ `${(0, i18n_1.t)('common.task')}: ${task.task}`,
70
+ `${(0, i18n_1.t)('cmd.mark.elapsed')}: ${(0, utils_1.formatDuration)(Date.now() - Date.parse(task.start))}`,
71
+ ]
72
+ : [(0, i18n_1.t)('tui.ready')];
73
+ const menuLines = menu.map((item) => `[${item.key}] ${item.label}`);
74
+ const height = Number(screen.height);
75
+ const lines = height < 12
76
+ ? [...primary, ...menuLines]
77
+ : [
78
+ 'C A C E / T I M E R',
79
+ '',
80
+ ...primary,
81
+ '',
82
+ ...menuLines,
83
+ '',
84
+ `Lv.${(0, data_1.scoreToLevel)(data.score || 0)} | ${data.score || 0} pts | ${(0, i18n_1.t)('tui.streak', { days: String(streak) })}`,
85
+ ];
86
+ surface.render('little_smile', elapsedMs, lines, (0, i18n_1.t)('tui.quit'));
87
+ }, fail, (0, terminal_1.getDisplay)().animation ? 100 : 1000);
134
88
  });
135
89
  }
package/dist/tui/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  "use strict";
2
- // ============ TUI Helpers ============
3
2
  Object.defineProperty(exports, "__esModule", { value: true });
4
3
  exports.isInteractiveTerminal = isInteractiveTerminal;
4
+ const terminal_1 = require("../terminal");
5
5
  /** Check if current terminal supports interactive TUI */
6
6
  function isInteractiveTerminal() {
7
- return process.stdin.isTTY === true && process.stdout.isTTY === true;
7
+ return ((0, terminal_1.getDisplay)().tui &&
8
+ process.env.TERM !== 'dumb' &&
9
+ process.stdin.isTTY === true &&
10
+ process.stdout.isTTY === true);
8
11
  }
@@ -0,0 +1,15 @@
1
+ import { MascotSize } from '../mascot/assets/mint';
2
+ export interface Rect {
3
+ top: number;
4
+ left: number;
5
+ width: number;
6
+ height: number;
7
+ }
8
+ export interface Layout {
9
+ mascot?: Rect & {
10
+ size: MascotSize;
11
+ };
12
+ body: Rect;
13
+ footer: Rect;
14
+ }
15
+ export declare function calculateLayout(columns: number, rows: number, bodyRows: number): Layout;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateLayout = calculateLayout;
4
+ const frames_1 = require("../mascot/frames");
5
+ function calculateLayout(columns, rows, bodyRows) {
6
+ const width = Math.max(1, Math.floor(columns));
7
+ const height = Math.max(1, Math.floor(rows));
8
+ const usableHeight = Math.max(0, height - 1);
9
+ const footer = { top: height - 1, left: 0, width, height: 1 };
10
+ const full = (0, frames_1.getFrame)();
11
+ if (width >= full.width + 42 && usableHeight >= Math.max(full.height, bodyRows)) {
12
+ return {
13
+ mascot: { size: 'full', top: 0, left: 0, width: full.width, height: full.height },
14
+ body: { top: 0, left: full.width + 3, width: width - full.width - 3, height: usableHeight },
15
+ footer,
16
+ };
17
+ }
18
+ for (const size of ['full', 'compact', 'tiny']) {
19
+ const frame = (0, frames_1.getFrame)({ size });
20
+ if (frame.width <= width && frame.height + 1 + bodyRows <= usableHeight) {
21
+ const top = frame.height + 1;
22
+ return {
23
+ mascot: {
24
+ size,
25
+ top: 0,
26
+ left: Math.floor((width - frame.width) / 2),
27
+ width: frame.width,
28
+ height: frame.height,
29
+ },
30
+ body: { top, left: 0, width, height: usableHeight - top },
31
+ footer,
32
+ };
33
+ }
34
+ }
35
+ return { body: { top: 0, left: 0, width, height: usableHeight }, footer };
36
+ }
@@ -7,13 +7,16 @@ exports.showReflectionInput = showReflectionInput;
7
7
  const blessed_1 = __importDefault(require("blessed"));
8
8
  const i18n_1 = require("../i18n");
9
9
  const lifecycle_1 = require("./lifecycle");
10
+ const terminal_1 = require("../terminal");
10
11
  function showReflectionInput() {
11
- return new Promise(resolve => {
12
+ return new Promise((resolve) => {
12
13
  const screen = blessed_1.default.screen({
13
14
  smartCSR: true,
14
15
  title: 'CACE TIMER',
15
16
  fullUnicode: true,
16
17
  });
18
+ const disposeSignals = (0, lifecycle_1.installSignalCleanup)(screen);
19
+ screen.once('destroy', disposeSignals);
17
20
  // Title
18
21
  blessed_1.default.box({
19
22
  parent: screen,
@@ -54,7 +57,7 @@ function showReflectionInput() {
54
57
  });
55
58
  input.focus();
56
59
  input.key('enter', () => {
57
- const text = input.getValue().trim();
60
+ const text = (0, terminal_1.safeText)(input.getValue()).trim();
58
61
  (0, lifecycle_1.destroyScreen)(screen);
59
62
  resolve({ text });
60
63
  });
@@ -63,8 +66,7 @@ function showReflectionInput() {
63
66
  resolve({ text: '' });
64
67
  });
65
68
  screen.key(['C-c'], () => {
66
- (0, lifecycle_1.destroyScreen)(screen);
67
- resolve({ text: '' });
69
+ process.emit('SIGINT');
68
70
  });
69
71
  screen.render();
70
72
  });
@@ -0,0 +1,5 @@
1
+ import blessed from 'blessed';
2
+ import { Expression } from '../mascot/assets/mint';
3
+ export declare function createSurface(screen: blessed.Widgets.Screen): {
4
+ render(expression: Expression, elapsedMs: number, lines: string[], footer: string, bodyRows?: number): void;
5
+ };
@@ -0,0 +1,64 @@
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.createSurface = createSurface;
7
+ const blessed_1 = __importDefault(require("blessed"));
8
+ const frames_1 = require("../mascot/frames");
9
+ const terminal_1 = require("../terminal");
10
+ const layout_1 = require("./layout");
11
+ // Artwork is tagged, user text is not. This prevents task names such as
12
+ // "{red-bg}" from being interpreted as blessed markup.
13
+ function createSurface(screen) {
14
+ const mascot = blessed_1.default.box({ parent: screen, tags: true, wrap: false, align: 'left' });
15
+ const body = blessed_1.default.box({ parent: screen, tags: false, wrap: false, align: 'left' });
16
+ const hint = blessed_1.default.box({ parent: screen, tags: false, wrap: false });
17
+ let previous = '';
18
+ return {
19
+ render(expression, elapsedMs, lines, footer, bodyRows = lines.length) {
20
+ const columns = Number(screen.width);
21
+ const rows = Number(screen.height);
22
+ const layout = (0, layout_1.calculateLayout)(columns, rows, bodyRows);
23
+ const display = (0, terminal_1.getDisplay)();
24
+ const plain = (0, terminal_1.resolveColor)() === 'never';
25
+ body.style.fg = plain ? 'default' : display.theme === 'light' ? '#526764' : '#d5e7e3';
26
+ hint.style.fg = plain ? 'default' : display.theme === 'light' ? '#687776' : '#82989e';
27
+ const frame = layout.mascot
28
+ ? (0, frames_1.getFrame)({
29
+ expression,
30
+ size: layout.mascot.size,
31
+ elapsedMs,
32
+ motion: display.animation,
33
+ ascii: display.ascii,
34
+ })
35
+ : undefined;
36
+ const picture = frame ? (0, frames_1.renderFrame)(frame, { ...display, format: 'blessed' }) : '';
37
+ const content = lines
38
+ .slice(0, layout.body.height)
39
+ .map((line) => (0, terminal_1.fitText)(line, layout.body.width))
40
+ .join('\n');
41
+ const footerText = (0, terminal_1.fitText)(footer, columns);
42
+ const signature = JSON.stringify([columns, rows, layout, picture, content, footerText]);
43
+ if (signature === previous)
44
+ return;
45
+ previous = signature;
46
+ if (layout.mascot) {
47
+ Object.assign(mascot, layout.mascot);
48
+ mascot.show();
49
+ mascot.setContent(picture);
50
+ }
51
+ else
52
+ mascot.hide();
53
+ Object.assign(body, layout.body);
54
+ if (layout.body.height)
55
+ body.show();
56
+ else
57
+ body.hide();
58
+ body.setContent(content);
59
+ Object.assign(hint, layout.footer);
60
+ hint.setContent(footerText);
61
+ screen.render();
62
+ },
63
+ };
64
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cacinie/cace-timer",
3
- "version": "1.3.2",
4
- "description": "A minimal time tracking CLI with cute anime girl mascot",
3
+ "version": "1.4.2",
4
+ "description": "Local-first time tracking CLI with a responsive MINT character dashboard",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "tk": "dist/index.js",
@@ -16,7 +16,9 @@
16
16
  "lint": "eslint src/",
17
17
  "format": "prettier --write src/",
18
18
  "prepublishOnly": "npm run build",
19
- "prepare": "npm run build"
19
+ "prepare": "npm run build",
20
+ "test:cli": "npm run build && node --test scripts/cli-smoke.cjs",
21
+ "test:pty": "npm run build && python3 scripts/pty-smoke.py"
20
22
  },
21
23
  "files": [
22
24
  "dist",