Clone
1
scripting
ispyisail edited this page 2026-09-16 20:29:49 +12:00

JavaScript Scripting

Status: pending. Everything on this page describes PR #891, not yet merged. Nothing here works until that lands — check the PR before trying any of this against your own build. This page will drop this notice once it does.

Read a project's model, export it, and edit its geometry from a script — headless for CI, or interactively against the diagram you have open.


Why this exists, and why JavaScript

Issue #162 asked for scripted automation: export derived files (PDF, BOM, cable lists) after every revision, and drive that from CI. The headless export flags (see CLI Reference) already covered the fixed, one-shot cases. This covers the rest: loops, conditionals, reading the model to decide what to do next, and — new in this PR — editing the diagram itself.

The engine is Qt's own QJSEngine, not an embedded Python interpreter. That was a deliberate choice, not the obvious one:

  • No new toolchain to package. QJSEngine ships in the Qml module of every Qt SDK QET already targets. Nothing new to install on Windows/macOS packaging, no interpreter version to pin, no GIL.
  • The core classes reflect into scripts almost for free, since they are already QObjects — no hand-written binding layer to maintain.
  • It is optional at build time, probed the same non-fatal way QtPdf already is. A QET build without the Qml module compiles identically; scripting is simply absent.

Two ways to run a script

Headless — --run

qelectrotech --run script.js project.qet

Opens the project, runs the script, exits. Exit codes match the rest of the CLI: 0 success, 1 the script threw or the project failed to open, 2 called wrongly (missing arguments, file not found).

#!/bin/bash
set -e
qelectrotech --run export_after_revision.js "$1"

Interactive — "Run Script..."

Projet → Exécuter un script... opens a file picker (filtered to *.js) and runs the chosen script against the currently open project. Useful for one-off macros you don't want to wire into CI.


What a script can do

Every script sees one global, qet. Four groups of capability, each with a different scope — read the boundary of each before assuming more:

Read the model

qet.projectTitle()             // -> "My Panel"
qet.filePath()                 // -> "/path/to/project.qet"
qet.folioCount()                // -> 3
qet.folioTitle(0)               // -> "Power"
qet.elementCount(folioIndex)    // -> 42
qet.conductorCount(folioIndex)  // -> 58

Same numbers --info reports, available programmatically instead of parsed from JSON.

Export and save

Thin wrappers around the same --export-* machinery documented in CLI Reference — same logic, same output, called from a script instead of a flag:

qet.exportPdf(output, showTerminals = false)
qet.exportPng(outDir, showTerminals = false)
qet.exportSvg(outDir, showTerminals = false)
qet.exportCables(output)
qet.exportWires(output)
qet.exportBom(output)
qet.exportWiring(output)
qet.exportNets(output)
qet.exportLinks(output)
qet.exportInfo(output)                       // output may be "" for stdout
qet.setTitleBlock(output, ["revision=B", "date=today"])
qet.save(output)                             // output may be "" to save in place

All return true/false.

Important, and worth reading twice: every export method above re-opens the project fresh from its file on disk. They never see edits a script made with the methods in the next section — only save() writes the project's live, in-memory state. Editing then exporting means calling save() first:

qet.addElement(0, "embed://…/relay.elmt", 100, 100);
qet.save("");                    // write the edit to disk
qet.exportPdf("out.pdf");        // now this sees it

Edit geometry, with real undo

qet.addElement(folioIndex, locationPath, x, y)   // -> uuid string, or "" on failure
qet.setElementPosition(folioIndex, elementUuid, x, y)
qet.moveElement(folioIndex, elementUuid, dx, dy)
qet.deleteElement(folioIndex, elementUuid)

qet.undo()      // -> false if the stack is empty
qet.redo()
qet.canUndo()
qet.canRedo()

These go through the exact same undo commands the GUI itself uses (AddGraphicsObjectCommand, QPropertyUndoCommand, DeleteQGraphicsItemCommand). Ctrl+Z in the editor undoes a script's edits exactly as it would the equivalent manual ones — they are, mechanically, the same commands on the same stack, not a side channel.

One consequence worth knowing before it surprises you: Qt's undo stack merges consecutive commands on the same object and property when their label also matches. setElementPosition() and moveElement() on the same element produce the same label, so calling them back-to-back collapses into one undo step — the same way dragging an item repeatedly does, not one step per call.

locationPath is an element collection path — embed://… for something already embedded in this project, common://… / custom://… for the shared collections. Same paths you see in a .qet file's <element type="…"> attribute.

deleteElement() refuses to remove an element with a non-deletable terminal (a linked master/slave, for instance) — same rule the Delete key follows.

Navigate and message

qet.selectElement(elementUuid)   // scene state; works headless, no view needed
qet.deselectAll(folioIndex)
qet.zoomFit()                    // -> false headless (no view to act on)
qet.zoomToContent()
qet.zoomReset()
qet.showMessage("Done.")         // a modal info box; safe headless (auto-dismissed)

Deliberately not here: triggering an arbitrary menu action by name. A script that could invoke any QAction could just as easily open a modal dialog with nobody there to dismiss it — a real, previously-hit hang class in this codebase (see the discussion on #882). Every method above is either non-blocking by construction, or — for showMessage — safe under QET's existing non-interactive mode, which is already on for the whole process before any headless script runs.

Logging

qet.log("anything you want in stderr")

A script has no console of its own; this is how you see output, including under --run.


Worked examples

Export everything after a revision (the issue's original ask)

// bump_and_export.js
qet.setTitleBlock("", ["revision=" + qet.projectTitle(), "date=today"]);
qet.save("");
qet.exportPdf(qet.filePath().replace(".qet", ".pdf"));
qet.exportBom(qet.filePath().replace(".qet", "_bom.csv"));
qet.log("Exported revision for " + qet.projectTitle());
qelectrotech --run bump_and_export.js panel.qet

CI: fail the build if a project won't open or export

#!/bin/bash
set -e
for f in projects/*.qet; do
  qelectrotech --run ci_check.js "$f"
done
// ci_check.js
if (qet.folioCount() === 0) {
  qet.log("ERROR: no folios in " + qet.filePath());
  throw new Error("empty project");
}
var ok = qet.exportPdf("/tmp/check.pdf");
if (!ok) throw new Error("PDF export failed");

A thrown error exits 1 and CI fails the step — no separate exit-code plumbing needed.

Place several elements from a list, then save

var placements = [
  ["embed://…/relay.elmt",    100, 100],
  ["embed://…/contactor.elmt", 200, 100],
  ["embed://…/breaker.elmt",   300, 100],
];
for (var i = 0; i < placements.length; i++) {
  var p = placements[i];
  var uuid = qet.addElement(0, p[0], p[1], p[2]);
  if (!uuid) qet.log("failed to place: " + p[0]);
}
qet.save("");

Errors and exit codes (headless)

Situation Exit code
Script ran to completion 0
Script threw an uncaught exception 1
Project failed to open 1
Missing script.js or project.qet argument 2
Script or project file not found 2

An uncaught exception is reported as Script error: <path>:<line>: <message> on stderr.


Limitations, on purpose

Same three lines as the class-level scope in the source, kept here so a change of mind shows up in one obvious place:

  • No arbitrary GUI actions. See the "Navigate and message" section above for exactly why.
  • Export methods don't see unsaved edits without an explicit save() first — see the callout above.
  • This is not a plugin system. No script runs automatically, on open or otherwise; every run is explicit, either a --run invocation or a menu click. There is still no loadable-module directory and no way for a project file to carry or trigger a script — see Automating QElectroTech for the file-format boundary this respects.

See also