pacatui 0.1.13 → 0.1.14
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/package.json +2 -1
- package/src/index.tsx +8 -0
- package/src/menubar/action.ts +80 -0
- package/src/menubar/db-lite.ts +95 -0
- package/src/menubar/index.ts +212 -0
- package/src/menubar/plugin.ts +88 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pacatui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "A simple tui app for task, timer and invoicing for projects.",
|
|
5
5
|
"module": "src/index.tsx",
|
|
6
6
|
"type": "module",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"@prisma/adapter-libsql": "^7.2.0",
|
|
67
67
|
"@prisma/client": "^7.2.0",
|
|
68
68
|
"better-sqlite3": "^12.6.0",
|
|
69
|
+
"paca4": ".",
|
|
69
70
|
"prisma": "^7.2.0",
|
|
70
71
|
"react": "^19.2.3",
|
|
71
72
|
"stripe": "^20.2.0"
|
package/src/index.tsx
CHANGED
|
@@ -138,6 +138,14 @@ function showSplash(theme: Theme): boolean {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
async function main() {
|
|
141
|
+
// Handle menubar subcommand before TUI setup
|
|
142
|
+
const args = process.argv.slice(2);
|
|
143
|
+
if (args[0] === "menubar") {
|
|
144
|
+
const { menubarCommand } = await import("./menubar/index.ts");
|
|
145
|
+
await menubarCommand(args.slice(1));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
141
149
|
// Get stored theme and show splash with ASCII paca
|
|
142
150
|
const theme = getStoredTheme();
|
|
143
151
|
const splashShown = showSplash(theme);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the content of the action handler script.
|
|
6
|
+
* This script is invoked by SwiftBar when the user clicks a menu item.
|
|
7
|
+
*/
|
|
8
|
+
export function getActionScript(bunPath: string): string {
|
|
9
|
+
const dbPath = join(homedir(), ".paca", "paca.db");
|
|
10
|
+
|
|
11
|
+
return `#!${bunPath}
|
|
12
|
+
// Paca SwiftBar Action Handler — auto-generated, do not edit
|
|
13
|
+
|
|
14
|
+
import Database from "bun:sqlite";
|
|
15
|
+
|
|
16
|
+
const DB_PATH = ${JSON.stringify(dbPath)};
|
|
17
|
+
|
|
18
|
+
function openDb() {
|
|
19
|
+
const db = new Database(DB_PATH);
|
|
20
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
21
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
22
|
+
return db;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function askDescription(): Promise<string> {
|
|
26
|
+
try {
|
|
27
|
+
const proc = Bun.spawnSync([
|
|
28
|
+
"osascript", "-e",
|
|
29
|
+
'display dialog "What did you work on?" default answer "" with title "Paca — Stop Timer" buttons {"Cancel", "Save"} default button "Save"',
|
|
30
|
+
]);
|
|
31
|
+
const output = proc.stdout.toString().trim();
|
|
32
|
+
// osascript returns: "button returned:Save, text returned:my description"
|
|
33
|
+
const match = output.match(/text returned:(.*)/);
|
|
34
|
+
return match?.[1]?.trim() ?? "";
|
|
35
|
+
} catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function main() {
|
|
41
|
+
const action = process.argv[2];
|
|
42
|
+
const id = process.argv[3];
|
|
43
|
+
|
|
44
|
+
if (!action || !id) {
|
|
45
|
+
console.error("Usage: action.ts <start|stop> <id>");
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const db = openDb();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
if (action === "stop") {
|
|
53
|
+
const description = await askDescription();
|
|
54
|
+
db.query(
|
|
55
|
+
\`UPDATE TimeEntry SET endTime = datetime('now'), description = ?, updatedAt = datetime('now') WHERE id = ?\`
|
|
56
|
+
).run(description || null, id);
|
|
57
|
+
} else if (action === "start") {
|
|
58
|
+
// Stop any running timer first
|
|
59
|
+
const running = db.query("SELECT id FROM TimeEntry WHERE endTime IS NULL LIMIT 1").get();
|
|
60
|
+
if (running) {
|
|
61
|
+
db.query(
|
|
62
|
+
\`UPDATE TimeEntry SET endTime = datetime('now'), updatedAt = datetime('now') WHERE id = ?\`
|
|
63
|
+
).run(running.id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Start new timer
|
|
67
|
+
const newId = crypto.randomUUID();
|
|
68
|
+
db.query(
|
|
69
|
+
\`INSERT INTO TimeEntry (id, projectId, startTime, createdAt, updatedAt)
|
|
70
|
+
VALUES (?, ?, datetime('now'), datetime('now'), datetime('now'))\`
|
|
71
|
+
).run(newId, id);
|
|
72
|
+
}
|
|
73
|
+
} finally {
|
|
74
|
+
db.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
main();
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import Database from "bun:sqlite";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
|
|
5
|
+
const DB_PATH = join(homedir(), ".paca", "paca.db");
|
|
6
|
+
|
|
7
|
+
function openDb(): Database {
|
|
8
|
+
const db = new Database(DB_PATH);
|
|
9
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
10
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
11
|
+
return db;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RunningTimerRow {
|
|
15
|
+
id: string;
|
|
16
|
+
startTime: string;
|
|
17
|
+
projectId: string;
|
|
18
|
+
projectName: string;
|
|
19
|
+
projectColor: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProjectRow {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
color: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getRunningTimer(): RunningTimerRow | null {
|
|
29
|
+
const db = openDb();
|
|
30
|
+
try {
|
|
31
|
+
const row = db
|
|
32
|
+
.query<RunningTimerRow, []>(
|
|
33
|
+
`SELECT te.id, te.startTime, te.projectId,
|
|
34
|
+
p.name AS projectName, p.color AS projectColor
|
|
35
|
+
FROM TimeEntry te
|
|
36
|
+
JOIN Project p ON p.id = te.projectId
|
|
37
|
+
WHERE te.endTime IS NULL
|
|
38
|
+
LIMIT 1`,
|
|
39
|
+
)
|
|
40
|
+
.get();
|
|
41
|
+
return row ?? null;
|
|
42
|
+
} finally {
|
|
43
|
+
db.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getProjects(): ProjectRow[] {
|
|
48
|
+
const db = openDb();
|
|
49
|
+
try {
|
|
50
|
+
return db
|
|
51
|
+
.query<ProjectRow, []>(
|
|
52
|
+
`SELECT id, name, color FROM Project WHERE archived = 0 ORDER BY name ASC`,
|
|
53
|
+
)
|
|
54
|
+
.all();
|
|
55
|
+
} finally {
|
|
56
|
+
db.close();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function startTimer(projectId: string): void {
|
|
61
|
+
const db = openDb();
|
|
62
|
+
try {
|
|
63
|
+
// Stop any running timer first
|
|
64
|
+
const running = db
|
|
65
|
+
.query<{ id: string }, []>(
|
|
66
|
+
`SELECT id FROM TimeEntry WHERE endTime IS NULL LIMIT 1`,
|
|
67
|
+
)
|
|
68
|
+
.get();
|
|
69
|
+
if (running) {
|
|
70
|
+
db.query(
|
|
71
|
+
`UPDATE TimeEntry SET endTime = datetime('now'), updatedAt = datetime('now') WHERE id = ?`,
|
|
72
|
+
).run(running.id);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Start new timer
|
|
76
|
+
const id = crypto.randomUUID();
|
|
77
|
+
db.query(
|
|
78
|
+
`INSERT INTO TimeEntry (id, projectId, startTime, createdAt, updatedAt)
|
|
79
|
+
VALUES (?, ?, datetime('now'), datetime('now'), datetime('now'))`,
|
|
80
|
+
).run(id, projectId);
|
|
81
|
+
} finally {
|
|
82
|
+
db.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function stopTimer(entryId: string, description?: string): void {
|
|
87
|
+
const db = openDb();
|
|
88
|
+
try {
|
|
89
|
+
db.query(
|
|
90
|
+
`UPDATE TimeEntry SET endTime = datetime('now'), description = ?, updatedAt = datetime('now') WHERE id = ?`,
|
|
91
|
+
).run(description ?? null, entryId);
|
|
92
|
+
} finally {
|
|
93
|
+
db.close();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { existsSync, writeFileSync, unlinkSync, chmodSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { getPluginScript } from "./plugin.ts";
|
|
6
|
+
import { getActionScript } from "./action.ts";
|
|
7
|
+
import { getRunningTimer, getProjects } from "./db-lite.ts";
|
|
8
|
+
|
|
9
|
+
const PLUGIN_FILENAME = "paca-timer.3s.ts";
|
|
10
|
+
const ACTION_FILENAME = "menubar-action.ts";
|
|
11
|
+
const PACA_DIR = join(homedir(), ".paca");
|
|
12
|
+
const ACTION_PATH = join(PACA_DIR, ACTION_FILENAME);
|
|
13
|
+
|
|
14
|
+
function resolveBunPath(): string {
|
|
15
|
+
try {
|
|
16
|
+
return execSync("which bun", { encoding: "utf-8" }).trim();
|
|
17
|
+
} catch {
|
|
18
|
+
throw new Error("Could not find bun in PATH. Is Bun installed?");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findSwiftBarPluginDir(): string | null {
|
|
23
|
+
// Try reading from SwiftBar defaults
|
|
24
|
+
try {
|
|
25
|
+
const dir = execSync(
|
|
26
|
+
"defaults read com.ameba.SwiftBar PluginDirectory 2>/dev/null",
|
|
27
|
+
{ encoding: "utf-8" },
|
|
28
|
+
).trim();
|
|
29
|
+
if (dir && existsSync(dir)) return dir;
|
|
30
|
+
} catch {
|
|
31
|
+
// ignore
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check common locations
|
|
35
|
+
const candidates = [
|
|
36
|
+
join(homedir(), "Library", "Application Support", "SwiftBar", "Plugins"),
|
|
37
|
+
join(homedir(), ".swiftbar"),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
for (const dir of candidates) {
|
|
41
|
+
if (existsSync(dir)) return dir;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isSwiftBarInstalled(): boolean {
|
|
48
|
+
return (
|
|
49
|
+
existsSync("/Applications/SwiftBar.app") ||
|
|
50
|
+
existsSync(join(homedir(), "Applications", "SwiftBar.app"))
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatElapsed(startTimeStr: string): string {
|
|
55
|
+
const startMs = new Date(startTimeStr + "Z").getTime();
|
|
56
|
+
const elapsedMs = Date.now() - startMs;
|
|
57
|
+
const totalSec = Math.floor(elapsedMs / 1000);
|
|
58
|
+
const h = Math.floor(totalSec / 3600);
|
|
59
|
+
const m = Math.floor((totalSec % 3600) / 60);
|
|
60
|
+
const s = totalSec % 60;
|
|
61
|
+
return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function install() {
|
|
65
|
+
if (!isSwiftBarInstalled()) {
|
|
66
|
+
console.log("SwiftBar is not installed.");
|
|
67
|
+
console.log("");
|
|
68
|
+
console.log("Install it with:");
|
|
69
|
+
console.log(" brew install --cask swiftbar");
|
|
70
|
+
console.log("");
|
|
71
|
+
console.log("Then run this command again:");
|
|
72
|
+
console.log(" paca menubar install");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const pluginDir = findSwiftBarPluginDir();
|
|
77
|
+
if (!pluginDir) {
|
|
78
|
+
console.log("Could not find SwiftBar plugin directory.");
|
|
79
|
+
console.log("");
|
|
80
|
+
console.log("Make sure SwiftBar has been launched at least once,");
|
|
81
|
+
console.log("or set a plugin directory in SwiftBar preferences.");
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const bunPath = resolveBunPath();
|
|
86
|
+
const pluginPath = join(pluginDir, PLUGIN_FILENAME);
|
|
87
|
+
|
|
88
|
+
// Write plugin script
|
|
89
|
+
writeFileSync(pluginPath, getPluginScript(bunPath), "utf-8");
|
|
90
|
+
chmodSync(pluginPath, 0o755);
|
|
91
|
+
|
|
92
|
+
// Write action script
|
|
93
|
+
writeFileSync(ACTION_PATH, getActionScript(bunPath), "utf-8");
|
|
94
|
+
chmodSync(ACTION_PATH, 0o755);
|
|
95
|
+
|
|
96
|
+
console.log("Paca menu bar plugin installed!");
|
|
97
|
+
console.log("");
|
|
98
|
+
console.log(` Plugin: ${pluginPath}`);
|
|
99
|
+
console.log(` Action: ${ACTION_PATH}`);
|
|
100
|
+
console.log("");
|
|
101
|
+
console.log("The timer should appear in your menu bar shortly.");
|
|
102
|
+
console.log("If not, open SwiftBar and refresh plugins.");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function uninstall() {
|
|
106
|
+
let removed = false;
|
|
107
|
+
|
|
108
|
+
// Remove plugin
|
|
109
|
+
const pluginDir = findSwiftBarPluginDir();
|
|
110
|
+
if (pluginDir) {
|
|
111
|
+
const pluginPath = join(pluginDir, PLUGIN_FILENAME);
|
|
112
|
+
if (existsSync(pluginPath)) {
|
|
113
|
+
unlinkSync(pluginPath);
|
|
114
|
+
console.log(`Removed plugin: ${pluginPath}`);
|
|
115
|
+
removed = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Remove action script
|
|
120
|
+
if (existsSync(ACTION_PATH)) {
|
|
121
|
+
unlinkSync(ACTION_PATH);
|
|
122
|
+
console.log(`Removed action script: ${ACTION_PATH}`);
|
|
123
|
+
removed = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (removed) {
|
|
127
|
+
console.log("");
|
|
128
|
+
console.log("Paca menu bar plugin uninstalled.");
|
|
129
|
+
} else {
|
|
130
|
+
console.log("No menu bar plugin files found to remove.");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function status() {
|
|
135
|
+
const DB_PATH = join(PACA_DIR, "paca.db");
|
|
136
|
+
if (!existsSync(DB_PATH)) {
|
|
137
|
+
console.log("No database found. Run paca to initialize.");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const running = getRunningTimer();
|
|
142
|
+
if (running) {
|
|
143
|
+
const elapsed = formatElapsed(running.startTime);
|
|
144
|
+
console.log(`Timer running: ${running.projectName} (${elapsed})`);
|
|
145
|
+
} else {
|
|
146
|
+
console.log("No timer running.");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const projects = getProjects();
|
|
150
|
+
if (projects.length > 0) {
|
|
151
|
+
console.log("");
|
|
152
|
+
console.log("Projects:");
|
|
153
|
+
for (const p of projects) {
|
|
154
|
+
console.log(` - ${p.name}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function showHelp() {
|
|
160
|
+
const running = getRunningTimer();
|
|
161
|
+
if (running) {
|
|
162
|
+
const elapsed = formatElapsed(running.startTime);
|
|
163
|
+
console.log(`Timer: ${running.projectName} (${elapsed})`);
|
|
164
|
+
} else {
|
|
165
|
+
console.log("No timer running.");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log("");
|
|
169
|
+
console.log("Menu bar commands:");
|
|
170
|
+
console.log(" paca menubar install Install SwiftBar plugin");
|
|
171
|
+
console.log(" paca menubar uninstall Remove SwiftBar plugin");
|
|
172
|
+
console.log(" paca menubar status Show current timer status");
|
|
173
|
+
console.log("");
|
|
174
|
+
|
|
175
|
+
if (!isSwiftBarInstalled()) {
|
|
176
|
+
console.log("SwiftBar is not installed. Install it with:");
|
|
177
|
+
console.log(" brew install --cask swiftbar");
|
|
178
|
+
} else {
|
|
179
|
+
const pluginDir = findSwiftBarPluginDir();
|
|
180
|
+
if (pluginDir) {
|
|
181
|
+
const pluginPath = join(pluginDir, PLUGIN_FILENAME);
|
|
182
|
+
if (existsSync(pluginPath)) {
|
|
183
|
+
console.log("Status: Plugin installed");
|
|
184
|
+
} else {
|
|
185
|
+
console.log("Status: SwiftBar found, plugin not installed");
|
|
186
|
+
console.log(" Run: paca menubar install");
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
console.log("Status: SwiftBar found, but no plugin directory set");
|
|
190
|
+
console.log(" Launch SwiftBar first, then run: paca menubar install");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function menubarCommand(args: string[]) {
|
|
196
|
+
const subcommand = args[0];
|
|
197
|
+
|
|
198
|
+
switch (subcommand) {
|
|
199
|
+
case "install":
|
|
200
|
+
await install();
|
|
201
|
+
break;
|
|
202
|
+
case "uninstall":
|
|
203
|
+
await uninstall();
|
|
204
|
+
break;
|
|
205
|
+
case "status":
|
|
206
|
+
status();
|
|
207
|
+
break;
|
|
208
|
+
default:
|
|
209
|
+
showHelp();
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the content of the SwiftBar plugin script.
|
|
6
|
+
* The script is a self-contained Bun script that reads the Paca DB
|
|
7
|
+
* and outputs SwiftBar-formatted text.
|
|
8
|
+
*/
|
|
9
|
+
export function getPluginScript(bunPath: string): string {
|
|
10
|
+
const dbPath = join(homedir(), ".paca", "paca.db");
|
|
11
|
+
const actionPath = join(homedir(), ".paca", "menubar-action.ts");
|
|
12
|
+
|
|
13
|
+
return `#!${bunPath}
|
|
14
|
+
// Paca SwiftBar Plugin — auto-generated, do not edit
|
|
15
|
+
// Refresh: every 3 seconds (from filename)
|
|
16
|
+
|
|
17
|
+
import Database from "bun:sqlite";
|
|
18
|
+
|
|
19
|
+
const DB_PATH = ${JSON.stringify(dbPath)};
|
|
20
|
+
const ACTION = ${JSON.stringify(actionPath)};
|
|
21
|
+
const BUN = ${JSON.stringify(bunPath)};
|
|
22
|
+
|
|
23
|
+
function main() {
|
|
24
|
+
let db;
|
|
25
|
+
try {
|
|
26
|
+
db = new Database(DB_PATH, { readonly: true });
|
|
27
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
28
|
+
} catch {
|
|
29
|
+
console.log("⏸ Paca | color=#6c7086");
|
|
30
|
+
console.log("---");
|
|
31
|
+
console.log("Database not found | color=#f38ba8");
|
|
32
|
+
console.log("Run paca to initialize | color=#6c7086");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Check for running timer
|
|
38
|
+
const running = db
|
|
39
|
+
.query(
|
|
40
|
+
\`SELECT te.id, te.startTime, p.name AS projectName, p.color AS projectColor
|
|
41
|
+
FROM TimeEntry te
|
|
42
|
+
JOIN Project p ON p.id = te.projectId
|
|
43
|
+
WHERE te.endTime IS NULL
|
|
44
|
+
LIMIT 1\`
|
|
45
|
+
)
|
|
46
|
+
.get();
|
|
47
|
+
|
|
48
|
+
if (running) {
|
|
49
|
+
// Calculate elapsed time
|
|
50
|
+
const startMs = new Date(running.startTime + "Z").getTime();
|
|
51
|
+
const elapsedMs = Date.now() - startMs;
|
|
52
|
+
const totalSec = Math.floor(elapsedMs / 1000);
|
|
53
|
+
const h = Math.floor(totalSec / 3600);
|
|
54
|
+
const m = Math.floor((totalSec % 3600) / 60);
|
|
55
|
+
const s = totalSec % 60;
|
|
56
|
+
const elapsed = \`\${h}:\${String(m).padStart(2, "0")}:\${String(s).padStart(2, "0")}\`;
|
|
57
|
+
|
|
58
|
+
console.log(\`▶ \${running.projectName} \${elapsed} | color=#a6e3a1 sfimage=timer\`);
|
|
59
|
+
console.log("---");
|
|
60
|
+
console.log(\`Stop Timer | bash=\${BUN} param1=\${ACTION} param2=stop param3=\${running.id} terminal=false refresh=true\`);
|
|
61
|
+
} else {
|
|
62
|
+
console.log("⏸ Paca | color=#6c7086 sfimage=timer");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log("---");
|
|
66
|
+
|
|
67
|
+
// List projects
|
|
68
|
+
const projects = db
|
|
69
|
+
.query("SELECT id, name, color FROM Project WHERE archived = 0 ORDER BY name ASC")
|
|
70
|
+
.all();
|
|
71
|
+
|
|
72
|
+
if (projects.length === 0) {
|
|
73
|
+
console.log("No projects yet | color=#6c7086");
|
|
74
|
+
console.log("Open paca to create one | color=#6c7086");
|
|
75
|
+
} else {
|
|
76
|
+
console.log("Start Timer For... | disabled=true color=#6c7086");
|
|
77
|
+
for (const p of projects) {
|
|
78
|
+
console.log(\`\${p.name} | bash=\${BUN} param1=\${ACTION} param2=start param3=\${p.id} terminal=false refresh=true color=\${p.color}\`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
db.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
main();
|
|
87
|
+
`;
|
|
88
|
+
}
|