kittyhtml 0.1.0 → 0.2.1

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
@@ -9,12 +9,16 @@ Built for AI agents that have something nice to show you — a styled report, a
9
9
  ## Install
10
10
 
11
11
  ```sh
12
- # from this directory, until published
13
- npm install
14
- npm link # exposes the `kittyhtml` binary on your PATH
12
+ npm install -g kittyhtml
15
13
  ```
16
14
 
17
- Requires Node 20+. Pulls in [`canvas`](https://www.npmjs.com/package/canvas) (native build) and `dropflow`.
15
+ Or one-shot, no install:
16
+
17
+ ```sh
18
+ npx kittyhtml --demo
19
+ ```
20
+
21
+ Requires Node 20+. Pulls in [`@napi-rs/canvas`](https://www.npmjs.com/package/@napi-rs/canvas) (prebuilt native binary, no compile step) and `dropflow`. Two deps total, ~90 KB tarball.
18
22
 
19
23
  ## Use
20
24
 
@@ -70,15 +74,15 @@ See the [DropFlow README](https://github.com/chearon/dropflow#supported-css-rule
70
74
 
71
75
  ## Fonts
72
76
 
73
- First run fetches Noto fonts from a CDN via DropFlow's bundled `register-noto-fonts.js`. Subsequent renders reuse what was loaded. Bundled offline fonts are on the roadmap.
77
+ `Noto Sans` (regular, bold, italic, bold-italic) and `Noto Sans Mono` (regular, bold) ship inside the package as latin-subset TTFs (~160 KB total). No CDN fetch on first run; works offline. Reference them in HTML with `font-family: 'Noto Sans', sans-serif` and `font-family: 'Noto Sans Mono', monospace`.
74
78
 
75
79
  ## Claude Code skill
76
80
 
77
- A bundled skill lets Claude Code render output as a styled inline image when you ask for it as "kittyhtml" or "khtml":
81
+ A bundled skill lets Claude Code render output as a styled inline image when you ask for it as "kittyhtml" or "khtml". After a global install:
78
82
 
79
83
  ```sh
80
84
  mkdir -p ~/.claude/skills
81
- cp -r skill/kittyhtml ~/.claude/skills/kittyhtml
85
+ cp -r "$(npm root -g)/kittyhtml/skill/kittyhtml" ~/.claude/skills/
82
86
  ```
83
87
 
84
88
  Then in any Claude Code session: *"give me this report as kittyhtml"* — the agent will generate DropFlow-compatible HTML and pipe it through this CLI. The skill is narrow on purpose; it only triggers on those keywords.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kittyhtml",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Render HTML to an image and display it inline in Kitty/iTerm2-capable terminals. No browser — CSS layout via DropFlow.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,7 @@
26
26
  "demo": "node src/cli.js --demo"
27
27
  },
28
28
  "dependencies": {
29
- "canvas": "^2.11.2",
29
+ "@napi-rs/canvas": "^1.0.0",
30
30
  "dropflow": "^0.6.1"
31
31
  },
32
32
  "keywords": [
package/src/cli.js CHANGED
@@ -80,6 +80,17 @@ function loadDemoHtml() {
80
80
  return readFileSync(join(here, '..', 'examples', 'demo.html'), 'utf8');
81
81
  }
82
82
 
83
+ // readFileSync(0) crashes with EAGAIN when stdin is in non-blocking mode and
84
+ // the upstream process hasn't flushed yet (common with `claude -p ... | kittyhtml`).
85
+ // Async iteration over process.stdin yields back to the event loop and waits
86
+ // for data, so it handles any pipe pacing correctly.
87
+ async function readStdin() {
88
+ process.stdin.setEncoding('utf8');
89
+ let buf = '';
90
+ for await (const chunk of process.stdin) buf += chunk;
91
+ return buf;
92
+ }
93
+
83
94
  async function main() {
84
95
  const opts = parseArgs(process.argv.slice(2));
85
96
 
@@ -89,7 +100,7 @@ async function main() {
89
100
  } else if (opts.file) {
90
101
  html = readFileSync(opts.file, 'utf8');
91
102
  } else if (!process.stdin.isTTY) {
92
- html = readFileSync(0, 'utf8');
103
+ html = await readStdin();
93
104
  } else {
94
105
  process.stdout.write(HELP);
95
106
  process.exit(1);
package/src/render.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as flow from 'dropflow';
2
2
  import parse from 'dropflow/parse.js';
3
- import { createCanvas } from 'canvas';
3
+ import { createCanvas, GlobalFonts, loadImage } from '@napi-rs/canvas';
4
4
 
5
5
  const FONTS_DIR = new URL('../assets/fonts/', import.meta.url);
6
6
  const BUNDLED_FONTS = [
@@ -12,13 +12,26 @@ const BUNDLED_FONTS = [
12
12
  'NotoSansMono-Bold.ttf',
13
13
  ];
14
14
 
15
- let fontsReady = false;
16
- function ensureFonts() {
17
- if (fontsReady) return;
15
+ let envReady = false;
16
+ function ensureEnv() {
17
+ if (envReady) return;
18
+
19
+ // Tell DropFlow how to register a font and decode an image into the
20
+ // @napi-rs/canvas backend (default wiring in environment-node.js targets the
21
+ // legacy `canvas` package).
22
+ flow.environment.registerFont = face => {
23
+ const key = GlobalFonts.register(face.getBuffer(), face.uniqueFamily);
24
+ if (key) return () => GlobalFonts.remove(key);
25
+ };
26
+ flow.environment.createDecodedImage = async image => {
27
+ return await loadImage(Buffer.from(image.buffer));
28
+ };
29
+
18
30
  for (const file of BUNDLED_FONTS) {
19
31
  flow.fonts.add(flow.createFaceFromTablesSync(new URL(file, FONTS_DIR)));
20
32
  }
21
- fontsReady = true;
33
+
34
+ envReady = true;
22
35
  }
23
36
 
24
37
  /**
@@ -34,7 +47,7 @@ function ensureFonts() {
34
47
  */
35
48
  export async function renderHtml(html, opts = {}) {
36
49
  const { width = 800, height = null, scale = 1, background = null } = opts;
37
- ensureFonts();
50
+ ensureEnv();
38
51
 
39
52
  const root = parse(html);
40
53
  await flow.load(root);
@@ -60,5 +73,5 @@ export async function renderHtml(html, opts = {}) {
60
73
  ctx.fillRect(0, 0, pxWidth, pxHeight);
61
74
  }
62
75
  flow.paintToCanvas(layout, ctx);
63
- return canvas.toBuffer('image/png');
76
+ return await canvas.encode('png');
64
77
  }