Files
arr-tui/webos/dev/drive.mjs
T
nilsandClaude Opus 5.5 de1491617e webOS: open the keyboard only on OK; hovering a tab does not switch views
Text fields are read-only while the focus passes over them (webOS opens its
keyboard for every editable field that gets focus); OK or a click starts
typing, Enter, Back or up/down ends it. The pointer focuses a tab without
switching to it; a click does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:58:23 +01:00

102 lines
3.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Runs the built app (dist/) in Chromium at 1920×1080 from file://, as the
// TV loads it, presses remote buttons from a scene file and takes
// screenshots. For development and the README pictures.
//
// node dev/drive.mjs SCENE OUTDIR
//
// CHROMIUM (default: chromium on PATH). Scene lines:
// key Down [Down ...] remote buttons: Left Right Up Down Enter Back Red
// Green Yellow Blue PageUp PageDown 1-7
// type TEXT type into the focused field
// sleep SECONDS
// point X Y / click X Y the Magic Remote's pointer: move there / click there
// shot NAME OUTDIR/NAME.png
// log print the app's console and the focused element
import puppeteer from "puppeteer-core";
import { mkdirSync, readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
const [scenePath, outDir] = process.argv.slice(2);
if (!scenePath || !outDir) {
console.error("usage: node dev/drive.mjs SCENE OUTDIR");
process.exit(2);
}
mkdirSync(outDir, { recursive: true });
const codes = {
Left: 37, Up: 38, Right: 39, Down: 40, Enter: 13, Back: 461,
Red: 403, Green: 404, Yellow: 405, Blue: 406, PageUp: 33, PageDown: 34,
};
for (let i = 1; i <= 9; i++) codes[String(i)] = 48 + i;
const browser = await puppeteer.launch({
executablePath: process.env.CHROMIUM || "chromium",
headless: true,
// file:// pages may call the local fake services.
args: ["--window-size=1920,1080", "--allow-file-access-from-files", "--no-sandbox"],
defaultViewport: { width: 1920, height: 1080 },
});
const page = await browser.newPage();
const logs = [];
// Request URLs in browser messages can carry an API key (SABnzbd, posters).
const redact = (t) => t.replace(/(apikey=)[^&\s'"]+/gi, "$1…");
page.on("console", (m) => logs.push(redact(`${m.type()}: ${m.text()}`)));
page.on("pageerror", (e) => logs.push(redact(`pageerror: ${e.message}`)));
await page.goto("file://" + resolve(here, "..", "dist", "index.html"));
// press sends a remote button as the TV does: a keydown with its keyCode.
async function press(name) {
const code = codes[name];
if (!code) throw new Error("unknown key " + name);
await page.evaluate((code) => {
const target = document.activeElement || document.body;
const ev = new KeyboardEvent("keydown", { bubbles: true, cancelable: true });
Object.defineProperty(ev, "keyCode", { get: () => code });
Object.defineProperty(ev, "which", { get: () => code });
target.dispatchEvent(ev);
}, code);
await new Promise((r) => setTimeout(r, 120));
}
for (const raw of readFileSync(scenePath, "utf8").split("\n")) {
const line = raw.trim();
if (!line || line.startsWith("#")) continue;
const [cmd, ...rest] = line.split(/\s+/);
switch (cmd) {
case "key":
for (const k of rest) await press(k);
break;
case "type":
await page.keyboard.type(line.slice(5));
break;
case "point":
await page.mouse.move(Number(rest[0]), Number(rest[1]));
break;
case "click":
await page.mouse.click(Number(rest[0]), Number(rest[1]));
break;
case "sleep":
await new Promise((r) => setTimeout(r, parseFloat(rest[0]) * 1000));
break;
case "shot":
await page.screenshot({ path: join(outDir, rest[0] + ".png") });
break;
case "log": {
const focused = await page.evaluate(() => {
const a = document.activeElement;
return a ? `${a.tagName}.${a.className} "${(a.textContent || "").slice(0, 40)}"` : "none";
});
console.log(logs.splice(0).join("\n"));
console.log("focused:", focused);
break;
}
default:
throw new Error("unknown scene command: " + line);
}
}
if (logs.length) console.log(logs.join("\n"));
await browser.close();