bmweb-cli 0.1.1 → 0.1.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/README.md +10 -6
- package/dist/bmweb.js +158 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -309,15 +309,19 @@ status line and the script's progress window are the two bottom lines.
|
|
|
309
309
|
With no arguments it starts on the app's own home, an INPA script of the
|
|
310
310
|
project's own (`home/bmweb-home.ips`): F1 picks a chassis then a module,
|
|
311
311
|
F2 the chassis's whole-vehicle script, and `scriptchange` hands the screen
|
|
312
|
-
to that script.
|
|
313
|
-
|
|
314
|
-
cable.
|
|
312
|
+
to that script. A pick is a list the keyboard walks: Up/Down move the bar,
|
|
313
|
+
typing narrows the list to the rows containing the text, Enter picks, Esc
|
|
314
|
+
cancels. The home starts with or without a cable.
|
|
315
|
+
|
|
316
|
+
The TUI runs on the terminal's alternate screen (the buffer vim and htop
|
|
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.
|
|
315
319
|
|
|
316
320
|
Every dialog INPA opens is a prompt: a message waits for Enter, an input
|
|
317
321
|
asks for the number (or hex, or text) within the declared range, the
|
|
318
|
-
two-word box takes y/n, the component picker (togglelist)
|
|
319
|
-
|
|
320
|
-
file name. **Every write is asked first**, exactly as the app asks: a key
|
|
322
|
+
two-word box takes y/n, the component picker (togglelist) and Select are
|
|
323
|
+
the same list picker (Space marks several where several may be picked),
|
|
324
|
+
save-as asks for a file name. **Every write is asked first**, exactly as the app asks: a key
|
|
321
325
|
whose body can send a write names the jobs and waits for y; a screen that
|
|
322
326
|
sends one on every refresh asks once for as long as it is open; n or
|
|
323
327
|
Enter abandons the key. On quit the leaving menu's Back job (the script's
|
package/dist/bmweb.js
CHANGED
|
@@ -1892,6 +1892,7 @@ function nodeTerminal() {
|
|
|
1892
1892
|
};
|
|
1893
1893
|
raw(true);
|
|
1894
1894
|
input.resume();
|
|
1895
|
+
output.write("\x1B[?1049h\x1B[H");
|
|
1895
1896
|
const subs = /* @__PURE__ */ new Set();
|
|
1896
1897
|
let paused = false;
|
|
1897
1898
|
input.on(
|
|
@@ -1945,6 +1946,7 @@ function nodeTerminal() {
|
|
|
1945
1946
|
}
|
|
1946
1947
|
},
|
|
1947
1948
|
close() {
|
|
1949
|
+
output.write("\x1B[?1049l");
|
|
1948
1950
|
raw(false);
|
|
1949
1951
|
input.pause();
|
|
1950
1952
|
}
|
|
@@ -1960,6 +1962,8 @@ var TuiUi = class {
|
|
|
1960
1962
|
program = null;
|
|
1961
1963
|
/** the lines of the frame on screen, when the cursor sits right below it */
|
|
1962
1964
|
frame = [];
|
|
1965
|
+
/** a picker owns the keyboard: the program's key handler must stand back */
|
|
1966
|
+
modal = false;
|
|
1963
1967
|
/** the terminal size the frame was drawn for; a change draws fresh */
|
|
1964
1968
|
frameSize = "";
|
|
1965
1969
|
leftResolve = null;
|
|
@@ -1988,10 +1992,7 @@ var TuiUi = class {
|
|
|
1988
1992
|
* and drawn again for the new size.
|
|
1989
1993
|
*/
|
|
1990
1994
|
resized() {
|
|
1991
|
-
|
|
1992
|
-
this.term.write(`\x1B[${this.frame.length}A\r\x1B[J`);
|
|
1993
|
-
this.frame = [];
|
|
1994
|
-
}
|
|
1995
|
+
this.frame = [];
|
|
1995
1996
|
if (this.program) this.paint(this.program);
|
|
1996
1997
|
}
|
|
1997
1998
|
/**
|
|
@@ -2014,26 +2015,116 @@ var TuiUi = class {
|
|
|
2014
2015
|
return null;
|
|
2015
2016
|
}
|
|
2016
2017
|
const title = step.what === "module" ? `Modules of ${step.arg}` : "Vehicles";
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2018
|
+
const picked = await this.pickList(title, options);
|
|
2019
|
+
return picked ? picked[0] : null;
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* The picker: a list the keyboard walks. Up/Down (and PageUp/PageDown)
|
|
2023
|
+
* move the bar, typing narrows the list to the rows containing the text,
|
|
2024
|
+
* Backspace widens it again, Enter picks the row under the bar (Space
|
|
2025
|
+
* marks a row when several may be picked, Enter then takes the marked
|
|
2026
|
+
* ones, or the bar's row when none is marked), Esc cancels. Drawn as the
|
|
2027
|
+
* frame, so it repaints in place like a screen.
|
|
2028
|
+
* @param title - what is being picked
|
|
2029
|
+
* @param options - the rows
|
|
2030
|
+
* @param multiple - whether several rows may be picked
|
|
2031
|
+
* @returns the picked values (one, unless multiple), or null for cancel
|
|
2032
|
+
*/
|
|
2033
|
+
async pickList(title, options, multiple = false) {
|
|
2034
|
+
let filter = "";
|
|
2035
|
+
let cursor = 0;
|
|
2036
|
+
let top = 0;
|
|
2037
|
+
const marked = /* @__PURE__ */ new Set();
|
|
2038
|
+
const shown = () => {
|
|
2039
|
+
const q = filter.toLowerCase();
|
|
2040
|
+
return q ? options.filter(
|
|
2034
2041
|
(o) => `${o.label} ${o.meta || ""} ${o.value}`.toLowerCase().includes(q)
|
|
2042
|
+
) : options;
|
|
2043
|
+
};
|
|
2044
|
+
const draw = () => {
|
|
2045
|
+
const rows = shown();
|
|
2046
|
+
const w = this.term.columns;
|
|
2047
|
+
const window = Math.max(3, this.term.rows - 7);
|
|
2048
|
+
if (cursor >= rows.length) cursor = Math.max(0, rows.length - 1);
|
|
2049
|
+
if (cursor < top) top = cursor;
|
|
2050
|
+
if (cursor >= top + window) top = cursor - window + 1;
|
|
2051
|
+
const count = rows.length === options.length ? `${options.length}` : `${rows.length} of ${options.length}`;
|
|
2052
|
+
const lines = [
|
|
2053
|
+
`${title} \x1B[2m(${count})\x1B[0m`,
|
|
2054
|
+
`\x1B[2mFilter:\x1B[0m ${filter}\x1B[7m \x1B[0m`,
|
|
2055
|
+
""
|
|
2056
|
+
];
|
|
2057
|
+
for (const o of rows.slice(top, top + window)) {
|
|
2058
|
+
const i = rows.indexOf(o);
|
|
2059
|
+
const mark = multiple ? marked.has(o.value) ? "[x] " : "[ ] " : "";
|
|
2060
|
+
const meta = o.meta ? ` ${o.meta}` : "";
|
|
2061
|
+
const text = `${mark}${o.label}${meta}`.slice(0, w - 4);
|
|
2062
|
+
lines.push(
|
|
2063
|
+
i === cursor ? `\x1B[7m > ${text.padEnd(w - 4)} \x1B[0m` : ` ${o.meta ? `${mark}${o.label}\x1B[2m${meta}\x1B[0m` : text}`
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
if (!rows.length) lines.push(" \x1B[2m(nothing matches)\x1B[0m");
|
|
2067
|
+
if (rows.length > top + window)
|
|
2068
|
+
lines.push(` \x1B[2m... ${rows.length - top - window} more\x1B[0m`);
|
|
2069
|
+
lines.push("");
|
|
2070
|
+
lines.push(
|
|
2071
|
+
`\x1B[2m\u2191\u2193 move type to filter Enter picks${multiple ? " Space marks" : ""} Esc cancels\x1B[0m`
|
|
2035
2072
|
);
|
|
2036
|
-
|
|
2073
|
+
this.flush(lines);
|
|
2074
|
+
};
|
|
2075
|
+
this.modal = true;
|
|
2076
|
+
try {
|
|
2077
|
+
return await new Promise((resolve2) => {
|
|
2078
|
+
const off = this.term.onKey((k) => {
|
|
2079
|
+
const rows = shown();
|
|
2080
|
+
const finish = (v) => {
|
|
2081
|
+
off();
|
|
2082
|
+
resolve2(v);
|
|
2083
|
+
};
|
|
2084
|
+
if (k.name === "escape" || k.ctrl && k.name === "c") {
|
|
2085
|
+
finish(null);
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
if (k.name === "return" || k.name === "enter") {
|
|
2089
|
+
if (multiple && marked.size) {
|
|
2090
|
+
finish(
|
|
2091
|
+
options.filter((o2) => marked.has(o2.value)).map((o2) => o2.value)
|
|
2092
|
+
);
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
const o = rows[cursor];
|
|
2096
|
+
finish(o ? [o.value] : null);
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
if (k.name === "up" || k.ctrl && k.name === "p")
|
|
2100
|
+
cursor = Math.max(0, cursor - 1);
|
|
2101
|
+
else if (k.name === "down" || k.ctrl && k.name === "n")
|
|
2102
|
+
cursor = Math.min(rows.length - 1, cursor + 1);
|
|
2103
|
+
else if (k.name === "pageup")
|
|
2104
|
+
cursor = Math.max(0, cursor - (this.term.rows - 7));
|
|
2105
|
+
else if (k.name === "pagedown")
|
|
2106
|
+
cursor = Math.min(rows.length - 1, cursor + (this.term.rows - 7));
|
|
2107
|
+
else if (k.name === "home") cursor = 0;
|
|
2108
|
+
else if (k.name === "end") cursor = Math.max(0, rows.length - 1);
|
|
2109
|
+
else if (k.name === "backspace") filter = filter.slice(0, -1);
|
|
2110
|
+
else if (multiple && k.name === "space") {
|
|
2111
|
+
const o = rows[cursor];
|
|
2112
|
+
if (o) {
|
|
2113
|
+
if (marked.has(o.value)) marked.delete(o.value);
|
|
2114
|
+
else marked.add(o.value);
|
|
2115
|
+
}
|
|
2116
|
+
} else if (k.ch && !k.ctrl && k.ch >= " " && k.ch !== "\x7F") {
|
|
2117
|
+
filter += k.ch;
|
|
2118
|
+
cursor = 0;
|
|
2119
|
+
} else return;
|
|
2120
|
+
draw();
|
|
2121
|
+
});
|
|
2122
|
+
draw();
|
|
2123
|
+
});
|
|
2124
|
+
} finally {
|
|
2125
|
+
this.modal = false;
|
|
2126
|
+
this.frame = [];
|
|
2127
|
+
if (this.program) this.paint(this.program);
|
|
2037
2128
|
}
|
|
2038
2129
|
}
|
|
2039
2130
|
/** Note the program once it exists, for the key handler. */
|
|
@@ -2041,10 +2132,9 @@ Number to open, text to filter, Enter to cancel: `
|
|
|
2041
2132
|
this.program = p;
|
|
2042
2133
|
}
|
|
2043
2134
|
/**
|
|
2044
|
-
* A prompt
|
|
2045
|
-
* cursor, so the frame is forgotten and the next paint
|
|
2046
|
-
*
|
|
2047
|
-
* tool call does).
|
|
2135
|
+
* A typed prompt under the frame (readline, raw mode off). It moves the
|
|
2136
|
+
* cursor, so the frame is forgotten and the next paint clears the screen
|
|
2137
|
+
* and draws fresh.
|
|
2048
2138
|
* @param prompt - the prompt text
|
|
2049
2139
|
* @returns the answer, or null when cancelled
|
|
2050
2140
|
*/
|
|
@@ -2164,8 +2254,8 @@ Send ${job}${arg ? ` ${arg}` : ""} to the module (${ctx.label})?${every} [y/N] `
|
|
|
2164
2254
|
* flag, every picked key ';'-joined).
|
|
2165
2255
|
*/
|
|
2166
2256
|
async pickComponent(p, step) {
|
|
2167
|
-
const rows = this.R.ipoScreenComponents(p.exec, p.screen).map((l,
|
|
2168
|
-
key: step.argnum ? String(
|
|
2257
|
+
const rows = this.R.ipoScreenComponents(p.exec, p.screen).map((l, i) => ({
|
|
2258
|
+
key: step.argnum ? String(i + 1) : String(l.keys).split(";")[0] || "",
|
|
2169
2259
|
caption: l.label || String(l.keys).split(";")[0] || ""
|
|
2170
2260
|
}));
|
|
2171
2261
|
if (!rows.length) {
|
|
@@ -2175,30 +2265,27 @@ Send ${job}${arg ? ` ${arg}` : ""} to the module (${ctx.label})?${every} [y/N] `
|
|
|
2175
2265
|
);
|
|
2176
2266
|
return null;
|
|
2177
2267
|
}
|
|
2178
|
-
const
|
|
2268
|
+
const options = rows.map((r) => ({
|
|
2269
|
+
value: r.key,
|
|
2270
|
+
label: r.caption,
|
|
2271
|
+
meta: r.key !== r.caption ? r.key : ""
|
|
2272
|
+
}));
|
|
2179
2273
|
if (step.multiple) {
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
)
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
if (i == null) return null;
|
|
2196
|
-
const onOff = await this.ask(`On or off? [on/off] `);
|
|
2197
|
-
if (onOff == null || !onOff.trim()) return null;
|
|
2198
|
-
return {
|
|
2199
|
-
ort: rows[i].key,
|
|
2200
|
-
ein: /^on/i.test(onOff.trim()) ? 0 : 1
|
|
2201
|
-
};
|
|
2274
|
+
const picked2 = await this.pickList("Components", options, true);
|
|
2275
|
+
if (!picked2 || !picked2.length) return null;
|
|
2276
|
+
return { ort: picked2.join(";"), ein: 0 };
|
|
2277
|
+
}
|
|
2278
|
+
const picked = await this.pickList("Component", options);
|
|
2279
|
+
if (!picked) return null;
|
|
2280
|
+
const onOff = await this.pickList(
|
|
2281
|
+
`${options.find((o) => o.value === picked[0])?.label || picked[0]}`,
|
|
2282
|
+
[
|
|
2283
|
+
{ value: "on", label: "On" },
|
|
2284
|
+
{ value: "off", label: "Off" }
|
|
2285
|
+
]
|
|
2286
|
+
);
|
|
2287
|
+
if (!onOff) return null;
|
|
2288
|
+
return { ort: picked[0], ein: onOff[0] === "on" ? 0 : 1 };
|
|
2202
2289
|
}
|
|
2203
2290
|
/** INPA's Select: which named logical lines to keep on screen. */
|
|
2204
2291
|
async pickLines(_p, names, multiple, _current, hints) {
|
|
@@ -2210,16 +2297,14 @@ Component number (Enter cancels): `);
|
|
|
2210
2297
|
);
|
|
2211
2298
|
return null;
|
|
2212
2299
|
}
|
|
2213
|
-
const
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
);
|
|
2219
|
-
if (
|
|
2220
|
-
|
|
2221
|
-
const picked = pickNumbers(a, names.length).map((i) => names[i]);
|
|
2222
|
-
return picked.length ? picked : null;
|
|
2300
|
+
const options = [
|
|
2301
|
+
{ value: "\0all", label: "(all lines)" },
|
|
2302
|
+
...names.map((n) => ({ value: n, label: n }))
|
|
2303
|
+
];
|
|
2304
|
+
const picked = await this.pickList("Lines to show", options, multiple);
|
|
2305
|
+
if (!picked || !picked.length) return null;
|
|
2306
|
+
if (picked.includes("\0all")) return [];
|
|
2307
|
+
return picked;
|
|
2223
2308
|
}
|
|
2224
2309
|
/** INPA's save-as dialog: a file name, written when the body ends. */
|
|
2225
2310
|
async saveFile() {
|
|
@@ -2233,10 +2318,9 @@ Save as [fault-memory.txt]: `);
|
|
|
2233
2318
|
this.status(_p, `wrote ${picked.name}`);
|
|
2234
2319
|
}
|
|
2235
2320
|
printScreen(p) {
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
`);
|
|
2239
|
-
this.frame = [];
|
|
2321
|
+
const name = `bmweb-screen-${Date.now()}.txt`;
|
|
2322
|
+
writeFileSync4(name, this.gridLines(p).join("\n") + "\n", "utf8");
|
|
2323
|
+
this.status(p, `printed the screen to ${name}`);
|
|
2240
2324
|
}
|
|
2241
2325
|
/**
|
|
2242
2326
|
* The module a scriptchange names. From the home script it is the car's
|
|
@@ -2322,12 +2406,13 @@ ${this.gridLines(p).join("\n")}
|
|
|
2322
2406
|
);
|
|
2323
2407
|
}
|
|
2324
2408
|
/**
|
|
2325
|
-
* Put a frame on the
|
|
2326
|
-
* the
|
|
2409
|
+
* Put a frame on the screen over the last one. The frame always starts
|
|
2410
|
+
* at the top-left of the alternate screen: go there, rewrite the lines
|
|
2327
2411
|
* that differ (each erased to the end of the row), step over the ones
|
|
2328
2412
|
* that match, and erase whatever the old frame had below the new one.
|
|
2329
|
-
*
|
|
2330
|
-
*
|
|
2413
|
+
* With no frame to build on (first paint, after a prompt, after a
|
|
2414
|
+
* resize) the screen is cleared first -- on the alternate screen that
|
|
2415
|
+
* costs no scrollback.
|
|
2331
2416
|
* @param lines - the new frame
|
|
2332
2417
|
*/
|
|
2333
2418
|
flush(lines) {
|
|
@@ -2337,14 +2422,14 @@ ${this.gridLines(p).join("\n")}
|
|
|
2337
2422
|
const prev = this.frame;
|
|
2338
2423
|
if (prev.length === lines.length && prev.every((l, i) => l === lines[i]))
|
|
2339
2424
|
return;
|
|
2340
|
-
let s = prev.length ?
|
|
2425
|
+
let s = prev.length ? "\x1B[H" : "\x1B[2J\x1B[H";
|
|
2341
2426
|
lines.forEach((line, i) => {
|
|
2342
2427
|
if (i < prev.length && prev[i] === line) s += "\x1B[B";
|
|
2343
2428
|
else s += `\r${line}\x1B[K\r
|
|
2344
2429
|
`;
|
|
2345
2430
|
});
|
|
2346
2431
|
if (lines.length < prev.length) s += "\r\x1B[J";
|
|
2347
|
-
|
|
2432
|
+
this.term.write(s);
|
|
2348
2433
|
this.frame = lines;
|
|
2349
2434
|
}
|
|
2350
2435
|
/**
|
|
@@ -2404,10 +2489,6 @@ ${this.progressText}\r
|
|
|
2404
2489
|
if (this.leftResolve) this.leftResolve();
|
|
2405
2490
|
}
|
|
2406
2491
|
};
|
|
2407
|
-
function pickNumbers(a, n) {
|
|
2408
|
-
if (a == null) return [];
|
|
2409
|
-
return a.split(/[,\s]+/).map((s) => Number(s)).filter((i) => Number.isInteger(i) && i >= 1 && i <= n).map((i) => i - 1);
|
|
2410
|
-
}
|
|
2411
2492
|
function cellText(c) {
|
|
2412
2493
|
const text = String(c.text || "");
|
|
2413
2494
|
if (c.kind === "lamp") {
|
|
@@ -2496,6 +2577,7 @@ async function tuiCommand(chassis, sgbd, opts = {}) {
|
|
|
2496
2577
|
}
|
|
2497
2578
|
const program = p;
|
|
2498
2579
|
const unsubscribe = term.onKey((k) => {
|
|
2580
|
+
if (ui.modal) return;
|
|
2499
2581
|
const what = keyToPress(k);
|
|
2500
2582
|
if (what === null) return;
|
|
2501
2583
|
if (what === "quit") {
|
|
@@ -2525,7 +2607,7 @@ async function tuiCommand(chassis, sgbd, opts = {}) {
|
|
|
2525
2607
|
}
|
|
2526
2608
|
|
|
2527
2609
|
// src/bmweb.ts
|
|
2528
|
-
var VERSION = true ? "0.1.
|
|
2610
|
+
var VERSION = true ? "0.1.2" : "0.0.0-dev";
|
|
2529
2611
|
var INCLUDE = {
|
|
2530
2612
|
include: {
|
|
2531
2613
|
kind: "list",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bmweb-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|