@ggterm/core 0.3.0 → 0.3.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/dist/cli-plot.js CHANGED
@@ -14601,6 +14601,8 @@ __export(exports_serve, {
14601
14601
  });
14602
14602
  import { watch, writeFileSync as writeFileSync3, unlinkSync } from "fs";
14603
14603
  import { join as join3 } from "path";
14604
+ import { createServer } from "http";
14605
+ import { spawn } from "child_process";
14604
14606
  function plotToVegaLite(plot) {
14605
14607
  const geomTypes = plot._provenance.geomTypes;
14606
14608
  const hasCompositeMark = geomTypes.some((t) => COMPOSITE_MARKS.has(t));
@@ -14617,6 +14619,11 @@ function getLatestPayload() {
14617
14619
  const { spec, provenance } = plotToVegaLite(plot);
14618
14620
  return JSON.stringify({ type: "plot", spec, provenance });
14619
14621
  }
14622
+ function jsonResponse(res, data, status = 200) {
14623
+ const body = JSON.stringify(data);
14624
+ res.writeHead(status, { "content-type": "application/json" });
14625
+ res.end(body);
14626
+ }
14620
14627
  function handleServe(port) {
14621
14628
  const p = port || 4242;
14622
14629
  ensureHistoryDirs();
@@ -14632,85 +14639,88 @@ function handleServe(port) {
14632
14639
  const payload = getLatestPayload();
14633
14640
  if (!payload)
14634
14641
  return;
14642
+ const sseData = `data: ${payload}
14643
+
14644
+ `;
14635
14645
  for (const client of clients) {
14636
14646
  try {
14637
- client.send(payload);
14647
+ client.write(sseData);
14638
14648
  } catch {
14639
14649
  clients.delete(client);
14640
14650
  }
14641
14651
  }
14642
14652
  }, 150);
14643
14653
  });
14644
- const server = Bun.serve({
14645
- port: p,
14646
- fetch(req, server2) {
14647
- const url2 = new URL(req.url);
14648
- if (url2.pathname === "/ws") {
14649
- if (server2.upgrade(req))
14650
- return;
14651
- return new Response("WebSocket upgrade failed", { status: 400 });
14652
- }
14653
- if (url2.pathname === "/api/latest") {
14654
- const payload = getLatestPayload();
14655
- if (!payload)
14656
- return Response.json({ type: "empty" });
14657
- return new Response(payload, { headers: { "content-type": "application/json" } });
14658
- }
14659
- if (url2.pathname === "/api/history") {
14660
- const entries = getHistory().slice(-50);
14661
- return Response.json(entries);
14662
- }
14663
- if (url2.pathname.startsWith("/api/plot/")) {
14664
- const id = url2.pathname.slice("/api/plot/".length);
14665
- const plot = loadPlotFromHistory(id);
14666
- if (!plot)
14667
- return Response.json({ error: "not found" }, { status: 404 });
14668
- const { spec, provenance } = plotToVegaLite(plot);
14669
- return Response.json({ type: "plot", spec, provenance });
14670
- }
14671
- return new Response(CLIENT_HTML, {
14672
- headers: { "content-type": "text/html; charset=utf-8" }
14654
+ const server = createServer((req, res) => {
14655
+ const url = new URL(req.url || "/", `http://localhost:${p}`);
14656
+ if (url.pathname === "/events") {
14657
+ res.writeHead(200, {
14658
+ "content-type": "text/event-stream",
14659
+ "cache-control": "no-cache",
14660
+ connection: "keep-alive"
14673
14661
  });
14674
- },
14675
- websocket: {
14676
- open(ws) {
14677
- clients.add(ws);
14678
- const payload = getLatestPayload();
14679
- if (payload)
14680
- ws.send(payload);
14681
- },
14682
- close(ws) {
14683
- clients.delete(ws);
14684
- },
14685
- message() {}
14662
+ clients.add(res);
14663
+ const payload = getLatestPayload();
14664
+ if (payload)
14665
+ res.write(`data: ${payload}
14666
+
14667
+ `);
14668
+ req.on("close", () => clients.delete(res));
14669
+ return;
14686
14670
  }
14671
+ if (url.pathname === "/api/latest") {
14672
+ const payload = getLatestPayload();
14673
+ if (!payload)
14674
+ return jsonResponse(res, { type: "empty" });
14675
+ res.writeHead(200, { "content-type": "application/json" });
14676
+ res.end(payload);
14677
+ return;
14678
+ }
14679
+ if (url.pathname === "/api/history") {
14680
+ const entries = getHistory().slice(-50);
14681
+ jsonResponse(res, entries);
14682
+ return;
14683
+ }
14684
+ if (url.pathname.startsWith("/api/plot/")) {
14685
+ const id = url.pathname.slice("/api/plot/".length);
14686
+ const plot = loadPlotFromHistory(id);
14687
+ if (!plot)
14688
+ return jsonResponse(res, { error: "not found" }, 404);
14689
+ const { spec, provenance } = plotToVegaLite(plot);
14690
+ jsonResponse(res, { type: "plot", spec, provenance });
14691
+ return;
14692
+ }
14693
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
14694
+ res.end(CLIENT_HTML);
14687
14695
  });
14688
- const url = `http://localhost:${server.port}`;
14689
- console.log(`ggterm live viewer running at ${url}`);
14690
- const markerPath = join3(getGGTermDir(), "serve.json");
14691
- writeFileSync3(markerPath, JSON.stringify({ port: server.port, pid: process.pid }));
14692
- const cleanup = () => {
14693
- try {
14694
- unlinkSync(markerPath);
14695
- } catch {}
14696
- };
14697
- process.on("SIGINT", () => {
14698
- cleanup();
14699
- process.exit(0);
14700
- });
14701
- process.on("SIGTERM", () => {
14702
- cleanup();
14703
- process.exit(0);
14696
+ server.listen(p, () => {
14697
+ const url = `http://localhost:${p}`;
14698
+ console.log(`ggterm live viewer running at ${url}`);
14699
+ const markerPath = join3(getGGTermDir(), "serve.json");
14700
+ writeFileSync3(markerPath, JSON.stringify({ port: p, pid: process.pid }));
14701
+ const cleanup = () => {
14702
+ try {
14703
+ unlinkSync(markerPath);
14704
+ } catch {}
14705
+ };
14706
+ process.on("SIGINT", () => {
14707
+ cleanup();
14708
+ process.exit(0);
14709
+ });
14710
+ process.on("SIGTERM", () => {
14711
+ cleanup();
14712
+ process.exit(0);
14713
+ });
14714
+ process.on("exit", cleanup);
14715
+ if (process.env.TERM_PROGRAM === "waveterm") {
14716
+ spawn("wsh", ["web", "open", url], { stdio: "ignore", detached: true }).unref();
14717
+ console.log(`Opened Wave panel`);
14718
+ } else {
14719
+ console.log(`Open in browser or Wave panel: wsh web open ${url}`);
14720
+ }
14721
+ console.log(`Watching ${plotsDir} for new plots...`);
14722
+ console.log(`Press Ctrl+C to stop`);
14704
14723
  });
14705
- process.on("exit", cleanup);
14706
- if (process.env.TERM_PROGRAM === "waveterm") {
14707
- Bun.spawn(["wsh", "web", "open", url]);
14708
- console.log(`Opened Wave panel`);
14709
- } else {
14710
- console.log(`Open in browser or Wave panel: wsh web open ${url}`);
14711
- }
14712
- console.log(`Watching ${plotsDir} for new plots...`);
14713
- console.log(`Press Ctrl+C to stop`);
14714
14724
  }
14715
14725
  var COMPOSITE_MARKS, CLIENT_HTML = `<!DOCTYPE html>
14716
14726
  <html lang="en">
@@ -14971,7 +14981,6 @@ let history = [];
14971
14981
  let historyIndex = {};
14972
14982
  let currentIdx = -1;
14973
14983
  let view = null;
14974
- let ws = null;
14975
14984
 
14976
14985
  function updateMeta(prov) {
14977
14986
  if (!prov) return;
@@ -15129,17 +15138,12 @@ function downloadPNG() {
15129
15138
  }
15130
15139
 
15131
15140
  function connect() {
15132
- const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
15133
- ws = new WebSocket(proto + '//' + location.host + '/ws');
15141
+ const es = new EventSource('/events');
15134
15142
 
15135
- ws.onopen = () => { statusEl.classList.add('connected'); };
15136
- ws.onclose = () => {
15137
- statusEl.classList.remove('connected');
15138
- setTimeout(connect, 2000);
15139
- };
15140
- ws.onerror = () => ws.close();
15143
+ es.onopen = () => { statusEl.classList.add('connected'); };
15144
+ es.onerror = () => { statusEl.classList.remove('connected'); };
15141
15145
 
15142
- ws.onmessage = (e) => {
15146
+ es.onmessage = (e) => {
15143
15147
  const data = JSON.parse(e.data);
15144
15148
  if (data.type === 'plot') {
15145
15149
  history.push(data);
@@ -16326,19 +16330,68 @@ Examples:`);
16326
16330
  Tip: Use "inspect <file>" to see available columns`);
16327
16331
  process.exit(1);
16328
16332
  }
16329
- if (!args[0].includes(".") && !fileExists(args[0])) {
16333
+ const BUILTIN_DATASETS = {
16334
+ iris: () => {
16335
+ const species = ["setosa", "versicolor", "virginica"];
16336
+ const data2 = Array.from({ length: 150 }, (_, i) => {
16337
+ const sp = species[Math.floor(i / 50)];
16338
+ const base = sp === "setosa" ? 0 : sp === "versicolor" ? 1 : 2;
16339
+ return {
16340
+ sepal_length: +(5 + base * 0.5 + Math.random()).toFixed(1),
16341
+ sepal_width: +(3 + Math.random() * 0.5).toFixed(1),
16342
+ petal_length: +(1.5 + base * 2 + Math.random()).toFixed(1),
16343
+ petal_width: +(0.2 + base * 0.8 + Math.random() * 0.3).toFixed(1),
16344
+ species: sp
16345
+ };
16346
+ });
16347
+ return { headers: ["sepal_length", "sepal_width", "petal_length", "petal_width", "species"], data: data2 };
16348
+ },
16349
+ mtcars: () => {
16350
+ const data2 = [
16351
+ { name: "Mazda RX4", mpg: 21, cyl: 6, hp: 110, wt: 2.62 },
16352
+ { name: "Mazda RX4 Wag", mpg: 21, cyl: 6, hp: 110, wt: 2.875 },
16353
+ { name: "Datsun 710", mpg: 22.8, cyl: 4, hp: 93, wt: 2.32 },
16354
+ { name: "Hornet 4 Drive", mpg: 21.4, cyl: 6, hp: 110, wt: 3.215 },
16355
+ { name: "Hornet Sportabout", mpg: 18.7, cyl: 8, hp: 175, wt: 3.44 },
16356
+ { name: "Valiant", mpg: 18.1, cyl: 6, hp: 105, wt: 3.46 },
16357
+ { name: "Duster 360", mpg: 14.3, cyl: 8, hp: 245, wt: 3.57 },
16358
+ { name: "Merc 240D", mpg: 24.4, cyl: 4, hp: 62, wt: 3.19 },
16359
+ { name: "Merc 230", mpg: 22.8, cyl: 4, hp: 95, wt: 3.15 },
16360
+ { name: "Merc 280", mpg: 19.2, cyl: 6, hp: 123, wt: 3.44 },
16361
+ { name: "Merc 280C", mpg: 17.8, cyl: 6, hp: 123, wt: 3.44 },
16362
+ { name: "Merc 450SE", mpg: 16.4, cyl: 8, hp: 180, wt: 4.07 },
16363
+ { name: "Merc 450SL", mpg: 17.3, cyl: 8, hp: 180, wt: 3.73 },
16364
+ { name: "Merc 450SLC", mpg: 15.2, cyl: 8, hp: 180, wt: 3.78 },
16365
+ { name: "Cadillac Fleetwood", mpg: 10.4, cyl: 8, hp: 205, wt: 5.25 },
16366
+ { name: "Lincoln Continental", mpg: 10.4, cyl: 8, hp: 215, wt: 5.424 }
16367
+ ];
16368
+ return { headers: ["name", "mpg", "cyl", "hp", "wt"], data: data2 };
16369
+ }
16370
+ };
16371
+ const [dataFile, x, y, color, title, geomSpec = "point", facetVar] = args;
16372
+ let headers;
16373
+ let data;
16374
+ if (BUILTIN_DATASETS[dataFile]) {
16375
+ const result = BUILTIN_DATASETS[dataFile]();
16376
+ headers = result.headers;
16377
+ data = result.data;
16378
+ } else if (!args[0].includes(".") && !fileExists(args[0])) {
16330
16379
  console.error(`
16331
16380
  Error: "${args[0]}" doesn't look like a file path`);
16332
16381
  console.error(`
16382
+ Built-in datasets: iris, mtcars`);
16383
+ console.error(`
16333
16384
  Did you mean one of these commands?`);
16334
16385
  console.error(` inspect <file> - Show column types`);
16335
16386
  console.error(` suggest <file> - Get plot suggestions`);
16336
16387
  console.error(` history - List saved plots`);
16337
16388
  console.error(` help - Show full usage`);
16338
16389
  process.exit(1);
16390
+ } else {
16391
+ const loaded = loadData(dataFile);
16392
+ headers = loaded.headers;
16393
+ data = loaded.data;
16339
16394
  }
16340
- const [dataFile, x, y, color, title, geomSpec = "point", facetVar] = args;
16341
- let { headers, data } = loadData(dataFile);
16342
16395
  const { baseGeom: geomType, refLines } = parseGeomSpec(geomSpec);
16343
16396
  if (geomType === "manhattan" && y && y !== "-") {
16344
16397
  data = data.map((row) => {
package/dist/serve.d.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Watches .ggterm/plots/ for new plots and pushes them to connected
5
5
  * browsers via WebSocket. Renders interactive Vega-Lite in a dark-themed page.
6
+ *
7
+ * Uses node:http and a minimal WebSocket implementation for Node.js compatibility.
6
8
  */
7
9
  export declare function handleServe(port?: number): void;
8
10
  //# sourceMappingURL=serve.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAweH,wBAAgB,WAAW,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAkG/C"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA0eH,wBAAgB,WAAW,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAmG/C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggterm/core",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Grammar of Graphics engine for terminals",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",