@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/tui/dashboard.js
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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:
|
|
86
|
-
{ key: 's', label:
|
|
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:
|
|
93
|
-
{ key: 'f', label:
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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
|
+
}
|
package/dist/tui/reflection.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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",
|