bmweb-cli 0.1.3 → 0.1.6

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.
Files changed (3) hide show
  1. package/README.md +9 -1
  2. package/dist/bmweb.js +158 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -315,7 +315,10 @@ cancels. The home starts with or without a cable.
315
315
 
316
316
  The TUI runs on the terminal's alternate screen (the buffer vim and htop
317
317
  use), so the shell's scrollback is never touched and quitting restores it;
318
- a screen redraws in place, only the lines that changed.
318
+ a screen redraws in place, only the lines that changed. A viewer longer
319
+ than the terminal (a fault protocol, a report) scrolls: Up/Down a line,
320
+ PgUp/PgDn a page, Home/End to either end, with a line under it saying
321
+ which rows are shown.
319
322
 
320
323
  Every dialog INPA opens is a prompt: a message waits for Enter, an input
321
324
  asks for the number (or hex, or text) within the declared range, the
@@ -350,6 +353,11 @@ tests drive the app's runtime against a fake car and a scripted terminal,
350
353
  with a module script written for the tests in INPA's language. The
351
354
  repository's `tools/check.sh` runs all of it.
352
355
 
356
+ Set `BMWEB_VERBOSE=1` to see what the app's runtime logs: the wire trace
357
+ the bus dumps after an error, each variant probe's verdict, the cable
358
+ events. It goes to stderr; without it the runtime is silent and a command's
359
+ output is only its own.
360
+
353
361
  ## License
354
362
 
355
363
  GPL-3.0, as the repository is. See `LICENSE`.
package/dist/bmweb.js CHANGED
@@ -111,6 +111,7 @@ function formatCount(n) {
111
111
  import { readFileSync as readFileSync2 } from "node:fs";
112
112
  import { join as join3 } from "node:path";
113
113
  import { fileURLToPath } from "node:url";
114
+ import { format, inspect } from "node:util";
114
115
  import { createContext, runInContext } from "node:vm";
115
116
 
116
117
  // src/runtime-files.json
@@ -284,14 +285,49 @@ function runtimeGlobals() {
284
285
  loadRuntime();
285
286
  return sandboxRef;
286
287
  }
288
+ function runtimeConsole() {
289
+ const on = !!process.env.BMWEB_VERBOSE;
290
+ const say = (...a) => {
291
+ if (on) process.stderr.write(`${format(...a)}
292
+ `);
293
+ };
294
+ const table = (rows) => {
295
+ if (on)
296
+ process.stderr.write(
297
+ `${inspect(rows, { depth: 3, maxArrayLength: 200, breakLength: 160 })}
298
+ `
299
+ );
300
+ };
301
+ const noop = () => {
302
+ };
303
+ return {
304
+ log: say,
305
+ info: say,
306
+ warn: say,
307
+ error: say,
308
+ debug: say,
309
+ trace: say,
310
+ table,
311
+ group: say,
312
+ groupCollapsed: say,
313
+ groupEnd: noop,
314
+ time: noop,
315
+ timeEnd: noop,
316
+ assert: noop,
317
+ dir: table
318
+ };
319
+ }
287
320
  function hostGlobals() {
288
321
  const noop = () => {
289
322
  };
290
323
  const sandbox = {
291
- // the scripts test `typeof window` and read window.<x>; the context's own
292
- // global stands in, as it does in the page (self: fflate's UMD attaches
293
- // its global to `self` when there is no CommonJS `exports`)
294
- console,
324
+ // the app's scripts talk to the browser console: the bus dumps its wire
325
+ // trace after an IFH error (console.table in a collapsed group), the
326
+ // variant resolver notes every probe's verdict, the bus reports the
327
+ // cable. In a terminal those are noise between a command's own lines
328
+ // (a scan's table had the trace printed through it), so they go to
329
+ // stderr only when BMWEB_VERBOSE is set, and never to stdout
330
+ console: runtimeConsole(),
295
331
  // timers.js's bmwSleep falls back to setTimeout where there is no
296
332
  // Worker; program.js schedules screen cycles and drains key presses
297
333
  // through setTimeout; activations.js defers a session end a microtask
@@ -814,6 +850,7 @@ import { createInterface } from "node:readline";
814
850
 
815
851
  // src/serial.ts
816
852
  import { readdirSync as readdirSync2 } from "node:fs";
853
+ var RX_KICK_MS = 4;
817
854
  var PORT_PATTERNS = [
818
855
  /^cu\.usbserial/i,
819
856
  /^cu\.SLAB/i,
@@ -827,8 +864,12 @@ var NodeSerialPort = class {
827
864
  binding = null;
828
865
  /** chunks heard and not yet read */
829
866
  chunks = [];
830
- /** the one read waiting for bytes, when the queue is empty */
831
- waiter = null;
867
+ /** reads waiting for bytes, oldest first, when the queue is empty */
868
+ waiters = [];
869
+ /** the receive kick, running while a read waits (see startKick) */
870
+ kick = null;
871
+ /** a kick ioctl in flight, so they never pile up */
872
+ kicking = false;
832
873
  /** the lines as last set, so a partial setSignals keeps the others */
833
874
  lines = { dtr: false, rts: false, brk: false };
834
875
  /** the wire trace sink, when the CLI wants one */
@@ -864,23 +905,25 @@ var NodeSerialPort = class {
864
905
  async close() {
865
906
  const b = this.binding;
866
907
  this.binding = null;
867
- if (this.waiter) {
868
- const w = this.waiter;
869
- this.waiter = null;
870
- w({ value: void 0, done: true });
871
- }
908
+ this.stopKick();
909
+ this.wakeAll();
872
910
  this.chunks = [];
873
911
  if (b) await b.close();
874
912
  }
913
+ /** Tell every waiting read the port is done, and forget them. */
914
+ wakeAll() {
915
+ const ws = this.waiters;
916
+ this.waiters = [];
917
+ for (const w of ws) w({ value: void 0, done: true });
918
+ }
875
919
  /**
876
920
  * Bytes the binding heard: to the waiting read, else queued.
877
921
  * @param chunk - the bytes
878
922
  */
879
923
  push(chunk) {
880
924
  if (!chunk.length) return;
881
- if (this.waiter) {
882
- const w = this.waiter;
883
- this.waiter = null;
925
+ const w = this.waiters.shift();
926
+ if (w) {
884
927
  w({ value: chunk, done: false });
885
928
  return;
886
929
  }
@@ -916,19 +959,54 @@ var NodeSerialPort = class {
916
959
  if (next) return Promise.resolve({ value: next, done: false });
917
960
  if (!this.binding) return Promise.resolve({ value: void 0, done: true });
918
961
  return new Promise((resolve2) => {
919
- this.waiter = resolve2;
962
+ this.waiters.push(resolve2);
963
+ this.startKick();
920
964
  });
921
965
  }
966
+ /**
967
+ * Make the driver deliver what it has heard.
968
+ *
969
+ * THE BUG THIS FIXES. On macOS the built-in FTDI driver does not wake the
970
+ * reader when bytes arrive: with a read armed and the process simply
971
+ * waiting, an ECU's answer sat in the driver until some OTHER call touched
972
+ * the device (the next write, a modem-line change, close), and only then
973
+ * came out -- measured on a real car as 0 bytes for the first exchange
974
+ * after open and every later exchange delivering the PREVIOUS one's bytes
975
+ * at its start. Polling the modem lines (a TIOCMGET, no wire traffic)
976
+ * every few milliseconds while a read waits makes each answer arrive
977
+ * within the poll interval, first exchange included. The interval never
978
+ * holds the process open and stops itself once no read is waiting.
979
+ */
980
+ startKick() {
981
+ if (this.kick) return;
982
+ const tick = () => {
983
+ const b = this.binding;
984
+ if (!b || !this.waiters.length) {
985
+ this.stopKick();
986
+ return;
987
+ }
988
+ if (this.kicking) return;
989
+ this.kicking = true;
990
+ b.get().catch(() => null).then(() => {
991
+ this.kicking = false;
992
+ });
993
+ };
994
+ this.kick = setInterval(tick, RX_KICK_MS);
995
+ if (typeof this.kick === "object" && "unref" in this.kick)
996
+ this.kick.unref();
997
+ }
998
+ /** Stop the receive kick. */
999
+ stopKick() {
1000
+ if (!this.kick) return;
1001
+ clearInterval(this.kick);
1002
+ this.kick = null;
1003
+ }
922
1004
  /**
923
1005
  * Cancel the reader: a waiting read is told `done`, buffered bytes go.
924
1006
  */
925
1007
  cancel() {
926
1008
  this.chunks = [];
927
- if (this.waiter) {
928
- const w = this.waiter;
929
- this.waiter = null;
930
- w({ value: void 0, done: true });
931
- }
1009
+ this.wakeAll();
932
1010
  }
933
1011
  /**
934
1012
  * Write bytes. Resolves when the OS has them, not when they have left the
@@ -1966,6 +2044,12 @@ var TuiUi = class {
1966
2044
  frame = [];
1967
2045
  /** a picker owns the keyboard: the program's key handler must stand back */
1968
2046
  modal = false;
2047
+ /** the first line of the viewer shown (INPA's viewer scrolls; so does this) */
2048
+ viewTop = 0;
2049
+ /** the view the scroll position belongs to; a new view starts at the top */
2050
+ viewShown = null;
2051
+ /** how many viewer lines the last frame had room for */
2052
+ viewRoom = 0;
1969
2053
  /** the terminal size the frame was drawn for; a change draws fresh */
1970
2054
  frameSize = "";
1971
2055
  leftResolve = null;
@@ -2380,6 +2464,32 @@ Save as [fault-memory.txt]: `);
2380
2464
  paint(p) {
2381
2465
  this.flush(this.frameLines(p));
2382
2466
  }
2467
+ /**
2468
+ * Scroll the viewer by a key: a line, a page, or to an end. Nothing
2469
+ * happens without a viewer on screen.
2470
+ * @param how - 'up' | 'down' | 'pageup' | 'pagedown' | 'home' | 'end'
2471
+ * @returns whether the key was for the viewer
2472
+ */
2473
+ scrollView(how) {
2474
+ const p = this.program;
2475
+ if (!p || !p.view) return false;
2476
+ const page = Math.max(1, this.viewRoom - 1);
2477
+ const n = (p.view.lines || []).length;
2478
+ const max = Math.max(0, n - this.viewRoom);
2479
+ const jump = {
2480
+ up: -1,
2481
+ down: 1,
2482
+ pageup: -page,
2483
+ pagedown: page,
2484
+ home: -n,
2485
+ end: n
2486
+ };
2487
+ const by = jump[how];
2488
+ if (by === void 0) return false;
2489
+ this.viewTop = Math.max(0, Math.min(max, this.viewTop + by));
2490
+ this.paint(p);
2491
+ return true;
2492
+ }
2383
2493
  /**
2384
2494
  * The frame as lines: title, rule, the view or the grid, a blank, the
2385
2495
  * key bar, then the status and progress lines. Cut to the terminal's
@@ -2392,16 +2502,34 @@ Save as [fault-memory.txt]: `);
2392
2502
  const w = this.term.columns;
2393
2503
  const title = `${p.ecu.label || p.ecu.sgbd} ${p.ecu.sgbd}.prg ${p.title || ""}`.trim();
2394
2504
  const head = [title, "-".repeat(Math.min(w, 78))];
2395
- let body = p.view ? [...p.view.lines || []] : this.gridLines(p);
2396
2505
  const tail = ["", ...this.keyLines(p), this.statusText, this.progressText];
2397
2506
  const room = Math.max(1, this.term.rows - 1 - head.length - tail.length);
2398
- if (body.length > room) body = body.filter((l, i) => l || body[i - 1]);
2399
- if (body.length > room) {
2400
- const hidden = body.length - (room - 1);
2401
- body = [
2402
- ...body.slice(0, room - 1),
2403
- `(${hidden} more rows: enlarge the terminal)`
2404
- ];
2507
+ let body;
2508
+ if (p.view) {
2509
+ const lines = [...p.view.lines || []];
2510
+ if (p.view !== this.viewShown) {
2511
+ this.viewShown = p.view;
2512
+ this.viewTop = 0;
2513
+ }
2514
+ const fits = lines.length <= room;
2515
+ const window = fits ? room : room - 1;
2516
+ this.viewRoom = window;
2517
+ this.viewTop = Math.max(0, Math.min(this.viewTop, lines.length - window));
2518
+ body = lines.slice(this.viewTop, this.viewTop + window);
2519
+ if (!fits)
2520
+ body.push(
2521
+ `\x1B[2mrows ${this.viewTop + 1}-${this.viewTop + body.length} of ${lines.length} \u2191\u2193 PgUp PgDn Home End scroll\x1B[0m`
2522
+ );
2523
+ } else {
2524
+ body = this.gridLines(p);
2525
+ if (body.length > room) body = body.filter((l, i) => l || body[i - 1]);
2526
+ if (body.length > room) {
2527
+ const hidden = body.length - (room - 1);
2528
+ body = [
2529
+ ...body.slice(0, room - 1),
2530
+ `(${hidden} more rows: enlarge the terminal)`
2531
+ ];
2532
+ }
2405
2533
  }
2406
2534
  return [...head, ...body, ...tail].map(
2407
2535
  (l) => String(l).replace(/[\r\n]/g, " ").slice(0, w)
@@ -2581,6 +2709,7 @@ async function tuiCommand(chassis, sgbd, opts = {}) {
2581
2709
  let leaving = false;
2582
2710
  const unsubscribe = term.onKey((k) => {
2583
2711
  if (ui.modal) return;
2712
+ if (ui.scrollView(k.name)) return;
2584
2713
  const what = keyToPress(k);
2585
2714
  if (what === null) return;
2586
2715
  if (what === "quit") {
@@ -2620,7 +2749,7 @@ async function tuiCommand(chassis, sgbd, opts = {}) {
2620
2749
  }
2621
2750
 
2622
2751
  // src/bmweb.ts
2623
- var VERSION = true ? "0.1.3" : "0.0.0-dev";
2752
+ var VERSION = true ? "0.1.6" : "0.0.0-dev";
2624
2753
  var INCLUDE = {
2625
2754
  include: {
2626
2755
  kind: "list",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bmweb-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "description": "BMWeb's tools as a command line: read INPA .IPO scripts, compile .IPS sources, search the corpus job index, decode and diff shared Garage reports, and, over a K+DCAN cable, run jobs, whole-car scans and INPA screens in the terminal.",
5
5
  "license": "GPL-3.0-only",
6
6
  "type": "module",