@drakulavich/zapara 0.6.0 → 0.7.0
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/CHANGELOG.md +11 -0
- package/README.md +8 -11
- package/package.json +1 -1
- package/src/image.ts +11 -1
- package/src/index.ts +32 -13
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to this project are documented here. The format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.7.0] - 2026-09-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- In a terminal, `zapara card` asks `open it? [Y/n]` and opens the picture in
|
|
12
|
+
the default viewer on Enter. Pipes, scripts and Windows are never asked.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- `zapara card` writes to `~/Downloads/zapara-card.png` instead of the current
|
|
16
|
+
directory, and says `wrote zapara-card.png to Downloads`. Without that
|
|
17
|
+
folder it asks for `--out`.
|
|
18
|
+
|
|
8
19
|
## [0.6.0] - 2026-09-22
|
|
9
20
|
|
|
10
21
|
### Changed
|
package/README.md
CHANGED
|
@@ -96,19 +96,16 @@ The grid is a fixed 98 columns wide, 100 with its hour header, and does not refl
|
|
|
96
96
|
`zapara card` turns your last two weeks into one picture: a character named after the kind of load that dominates your hours, the sentence behind it, the peak hour, the share of calm, warming, heating and fried hours, and three highlights. It carries no dates and no hour totals, so it does not read as a timesheet.
|
|
97
97
|
|
|
98
98
|
```bash
|
|
99
|
-
zapara card # writes zapara-card.png
|
|
99
|
+
zapara card # writes zapara-card.png to ~/Downloads
|
|
100
100
|
zapara card --out card.webp # WebP instead; --out card.html writes the page itself
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
zapara card && osascript -e 'set the clipboard to (read (POSIX file "zapara-card.png") as «class PNGf»)'
|
|
107
|
-
```
|
|
103
|
+
In a terminal it then asks `open it? [Y/n]`: Enter opens the picture in the default viewer, where ⌘C copies it for a chat. A pipe or a script is never asked.
|
|
108
104
|
|
|
109
105
|
```
|
|
110
106
|
The Marathoner: Longest streak 7h53m without a break, 68% of your hours calm.
|
|
111
|
-
wrote zapara-card.png
|
|
107
|
+
wrote zapara-card.png to Downloads
|
|
108
|
+
open it? [Y/n]
|
|
112
109
|
```
|
|
113
110
|
|
|
114
111
|
<p align="center"><img src="https://raw.githubusercontent.com/drakulavich/zapara/main/assets/card.webp" alt="zapara card: The Marathoner, longest streak 7h53m, 68% of hours calm" width="800"></p>
|
|
@@ -124,7 +121,7 @@ This one comes from the same `busy-week` fixture as the pictures above. A headle
|
|
|
124
121
|
| `zapara --from 2026-09-01 --to 2026-09-14` | Any window, both days inclusive, at most 90 days. `--to` alone is 7 days ending there, `--days 30 --to 2026-09-14` is 30 days ending there. |
|
|
125
122
|
| `zapara today` | Today, one row per hour that had activity. `yesterday` likewise. |
|
|
126
123
|
| `zapara 2026-09-14 --explain` | One day, with the six weighted components behind each index. |
|
|
127
|
-
| `zapara card` | The last 14 days as one shareable picture, `zapara-card.png` in
|
|
124
|
+
| `zapara card` | The last 14 days as one shareable picture, `zapara-card.png` in `~/Downloads`. |
|
|
128
125
|
| `zapara card --days 30 --out me.webp` | Any window from 1 to 90 days; `.png`, `.webp` or `.html` by extension. `--json` prints the card's data instead. |
|
|
129
126
|
| `zapara status` | Writes today's load to `~/.claude/zapara/status.json` for a status line to read, and prints the same line. See [Status line](#status-line). |
|
|
130
127
|
|
|
@@ -134,16 +131,16 @@ This one comes from the same `busy-week` fixture as the pictures above. A headle
|
|
|
134
131
|
| `--explain` | With a day: the six weighted parts behind each index. |
|
|
135
132
|
| `--json` | Print the whole window as one JSON document instead of a table. |
|
|
136
133
|
| `--projects <dir>` | Read this directory instead of `~/.claude/projects`. |
|
|
137
|
-
| `--out <path>` | Where `card` writes
|
|
134
|
+
| `--out <path>` | Where `card` writes instead of `~/Downloads`; the extension picks the format. |
|
|
138
135
|
| `--no-color` | Plain glyphs and peaks with no ANSI codes. `NO_COLOR` in the environment does the same. |
|
|
139
136
|
| `-h`, `--help` | Usage, exit 0. |
|
|
140
137
|
| `-V`, `--version` | The version from `package.json`, exit 0. |
|
|
141
138
|
|
|
142
139
|
Levels: calm 0–29, warming 30–59, heating 60–84, fried 85–100.
|
|
143
140
|
|
|
144
|
-
The grid and the day print a text table when stdout is a terminal and JSON otherwise, so `zapara | cat` prints JSON; no flag forces text in a pipe yet. `card` and `status` write their file and print their lines whether piped or not. `card --json` is the exception: it prints the card's data and writes no file. `--json` changes nothing for `status`, whose line is already JSON and whose file is written either way.
|
|
141
|
+
The grid and the day print a text table when stdout is a terminal and JSON otherwise, so `zapara | cat` prints JSON; no flag forces text in a pipe yet. `card` and `status` write their file and print their lines whether piped or not. `card` asks to open the picture only when stdin and stdout are both a terminal, and never on Windows. `card --json` is the exception: it prints the card's data and writes no file. `--json` changes nothing for `status`, whose line is already JSON and whose file is written either way.
|
|
145
142
|
|
|
146
|
-
A run that works exits 0, and so does a window with no data, which prints an empty grid. Exit 1 is a failure zapara can name, printed as one line to stderr that never contains a path: the projects directory missing or unreadable, `status` unable to write its file, `card` unable to write its picture or to find a browser engine, and whatever else goes wrong below the command line. Exit 2 is a usage error, such as a bad date, an unknown flag or a value flag given twice; it prints one line and a hint to `--help`.
|
|
143
|
+
A run that works exits 0, and so does a window with no data, which prints an empty grid. Exit 1 is a failure zapara can name, printed as one line to stderr that never contains a path: the projects directory missing or unreadable, `status` unable to write its file, `card` unable to write its picture or to find a browser engine, `card` without `--out` on a machine with no `~/Downloads` folder, and whatever else goes wrong below the command line. Exit 2 is a usage error, such as a bad date, an unknown flag or a value flag given twice; it prints one line and a hint to `--help`.
|
|
147
144
|
|
|
148
145
|
## Status line
|
|
149
146
|
|
package/package.json
CHANGED
package/src/image.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The only module that reads the card assets, opens a Bun.WebView or Bun.Image,
|
|
2
|
-
// and
|
|
2
|
+
// writes the card, and starts another program: the opener that shows it.
|
|
3
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { resolve } from "node:path";
|
|
4
5
|
import type { CardAssets } from "./cardhtml.ts";
|
|
5
6
|
|
|
6
7
|
const ASSETS = new URL("../assets/", import.meta.url);
|
|
@@ -72,3 +73,12 @@ async function write(out: string, data: string | Uint8Array): Promise<void> {
|
|
|
72
73
|
throw new Error(WRITE_LINE);
|
|
73
74
|
}
|
|
74
75
|
}
|
|
76
|
+
|
|
77
|
+
// Absolute, so `-card.html` is never an option. `sh … &` with SIGHUP ignored:
|
|
78
|
+
// a detached Bun.spawn child, or one in a terminal zapara leads, dies with zapara.
|
|
79
|
+
export function openCard(path: string): void {
|
|
80
|
+
const opener = process.platform === "darwin" ? "open" : "xdg-open";
|
|
81
|
+
try {
|
|
82
|
+
Bun.spawnSync(["sh", "-c", 'trap "" HUP; "$0" "$@" </dev/null >/dev/null 2>&1 &', opener, resolve(path)], { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// Argument parsing, the clock, stdout and exit codes live here; everything else is pure.
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
3
|
+
import { readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { cardData, sentenceText } from "./card.ts";
|
|
7
7
|
import { cardHtml } from "./cardhtml.ts";
|
|
8
8
|
import { localDate } from "./derive.ts";
|
|
9
|
-
import { loadAssets, renderCard } from "./image.ts";
|
|
9
|
+
import { loadAssets, openCard, renderCard } from "./image.ts";
|
|
10
10
|
import { renderDay, renderJson, renderWeek } from "./render.ts";
|
|
11
11
|
import { report } from "./report.ts";
|
|
12
12
|
import { renderStatus, statusOf } from "./status.ts";
|
|
@@ -35,7 +35,8 @@ window:
|
|
|
35
35
|
|
|
36
36
|
options:
|
|
37
37
|
--explain with a day: the six weighted parts behind each index
|
|
38
|
-
--out <path> with card: .png, .webp or .html
|
|
38
|
+
--out <path> with card: .png, .webp or .html
|
|
39
|
+
(default ~/Downloads/zapara-card.png)
|
|
39
40
|
--json the same data as JSON; a pipe gets JSON without asking
|
|
40
41
|
--projects <dir> read this directory instead of ~/.claude/projects
|
|
41
42
|
--no-color no ANSI colors; NO_COLOR does the same
|
|
@@ -44,7 +45,7 @@ options:
|
|
|
44
45
|
levels: calm 0-29 warming 30-59 heating 60-84 fried 85-100`;
|
|
45
46
|
const HINT = "run 'zapara --help' for usage";
|
|
46
47
|
|
|
47
|
-
type Args = { command: "grid" | "day" | "card" | "status"; to: string; days: number; explain: boolean; json: boolean; out: string; projects: string; color: boolean };
|
|
48
|
+
type Args = { command: "grid" | "day" | "card" | "status"; to: string; days: number; explain: boolean; json: boolean; out: string | null; projects: string; color: boolean };
|
|
48
49
|
|
|
49
50
|
class UsageError extends Error {}
|
|
50
51
|
// Thrown only at a flag position, never for a token consumed as another flag's
|
|
@@ -95,12 +96,11 @@ function windowOf(days: string | null, from: string | null, to: string | null, d
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
function parseArgs(argv: string[], now: Date, env: NodeJS.ProcessEnv, isTTY: boolean): Args {
|
|
98
|
-
const a: Args = { command: "grid", to: localDate(now), days: 7, explain: false, json: false, out:
|
|
99
|
+
const a: Args = { command: "grid", to: localDate(now), days: 7, explain: false, json: false, out: null, projects: join(homedir(), ".claude", "projects"), color: isTTY && !env.NO_COLOR };
|
|
99
100
|
let days: string | null = null;
|
|
100
101
|
let from: string | null = null;
|
|
101
102
|
let to: string | null = null;
|
|
102
103
|
let jsonFlag = false;
|
|
103
|
-
let outGiven = false;
|
|
104
104
|
const positional: string[] = [];
|
|
105
105
|
// A value flag given twice is a usage error; bare flags are idempotent and untracked.
|
|
106
106
|
const seen = new Set<string>();
|
|
@@ -136,7 +136,7 @@ function parseArgs(argv: string[], now: Date, env: NodeJS.ProcessEnv, isTTY: boo
|
|
|
136
136
|
case "--from": from = value(); break;
|
|
137
137
|
case "--to": to = value(); break;
|
|
138
138
|
case "--days": days = value(true); break;
|
|
139
|
-
case "--out": a.out = value();
|
|
139
|
+
case "--out": a.out = value(); break;
|
|
140
140
|
default:
|
|
141
141
|
if (arg.startsWith("-")) throw new UsageError(`unknown flag${named(arg)}`);
|
|
142
142
|
positional.push(arg);
|
|
@@ -158,10 +158,12 @@ function parseArgs(argv: string[], now: Date, env: NodeJS.ProcessEnv, isTTY: boo
|
|
|
158
158
|
// The card is a file either way, so only an explicit --json switches it.
|
|
159
159
|
a.json = a.command === "card" ? jsonFlag : jsonFlag || !isTTY;
|
|
160
160
|
if (a.command !== "day" && a.explain) throw new UsageError("--explain applies to a named day only");
|
|
161
|
-
if (a.command !== "card" &&
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
if (a.command !== "card" && a.out !== null) throw new UsageError("--out applies to card only");
|
|
162
|
+
if (a.out !== null) {
|
|
163
|
+
// Printed back verbatim in `wrote \u2026`, so it must be one plain line.
|
|
164
|
+
if (/[\x00-\x1f\x7f]/.test(a.out)) throw new UsageError("--out must not contain control characters");
|
|
165
|
+
if (!/\.(png|webp|html)$/i.test(a.out)) throw new UsageError("--out must end in .png, .webp or .html");
|
|
166
|
+
}
|
|
165
167
|
return a;
|
|
166
168
|
}
|
|
167
169
|
|
|
@@ -188,6 +190,16 @@ async function status(a: Args, now: Date): Promise<number> {
|
|
|
188
190
|
return 0;
|
|
189
191
|
}
|
|
190
192
|
|
|
193
|
+
// The label names the folder, not the path: the CLI never prints a derived path.
|
|
194
|
+
function cardTarget(out: string | null): { path: string; label: string } {
|
|
195
|
+
if (out !== null) return { path: out, label: out };
|
|
196
|
+
const dir = join(homedir(), "Downloads");
|
|
197
|
+
let isDir = false;
|
|
198
|
+
try { isDir = statSync(dir).isDirectory(); } catch {}
|
|
199
|
+
if (!isDir) throw new Error("no Downloads folder: pass --out <path>");
|
|
200
|
+
return { path: join(dir, "zapara-card.png"), label: "zapara-card.png to Downloads" };
|
|
201
|
+
}
|
|
202
|
+
|
|
191
203
|
async function card(a: Args): Promise<number> {
|
|
192
204
|
const days: Day[] = await report({ projects: a.projects, to: a.to, days: a.days });
|
|
193
205
|
const data = cardData(days, { days: a.days });
|
|
@@ -203,8 +215,15 @@ async function card(a: Args): Promise<number> {
|
|
|
203
215
|
console.log(JSON.stringify(json, null, 2));
|
|
204
216
|
return 0;
|
|
205
217
|
}
|
|
206
|
-
|
|
207
|
-
|
|
218
|
+
const target = cardTarget(a.out);
|
|
219
|
+
await renderCard(cardHtml(data, await loadAssets()), target.path);
|
|
220
|
+
console.log(`${data.name}: ${sentenceText(data.sentence)}\nwrote ${target.label}`);
|
|
221
|
+
if (process.stdin.isTTY && process.stdout.isTTY && process.platform !== "win32") {
|
|
222
|
+
process.stdout.write("open it? [Y/n] ");
|
|
223
|
+
let answer: string | null = null;
|
|
224
|
+
for await (const line of console) { answer = line; break; }
|
|
225
|
+
if (answer !== null && /^(y|yes)?$/i.test(answer.trim())) openCard(target.path);
|
|
226
|
+
}
|
|
208
227
|
return 0;
|
|
209
228
|
}
|
|
210
229
|
|