Document JavaScript scripting (--run, Run Script...)

New page: scripting.md -- the two entry points, every qet.* method
(read the model, export/save, edit geometry with real undo, navigate/
message), three worked examples, exit codes, and the same limitations
the source's own class doc comment states, kept in sync deliberately.

Updates api_reference.md and cli_reference.md, both of which currently
say plainly that QET has no embedded scripting language -- true until
now. api_reference.md in particular carries a September 2026 correction
notice from when this page wrongly claimed Python scripting and a
plugin system existed; updated to state precisely what changed (a real
JavaScript engine) and what still has not (no plugin system, nothing
runs automatically, no project file can carry or trigger a script).

Status: describes PR #891, not yet merged into master. Every new/
changed section says so. Pushing now for discoverability rather than
waiting for merge, per explicit direction; drop the pending-PR notices
once #891 lands.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ispyisail
2026-09-16 20:29:49 +12:00
parent 85b61c8e75
commit 8df96757f3
4 changed files with 307 additions and 19 deletions
+2
@@ -121,6 +121,8 @@
**[CLI Reference](cli_reference)** — command line usage
**[JavaScript Scripting](scripting)** — `--run`, geometry editing, undo
**[Development Roadmap](development_roadmap)**
**[Vision](vision)** — _proposal, under discussion_
+16 -12
@@ -14,14 +14,16 @@ How to drive QET from other programs, and what the XML files look like.
| | |
|---|---|
| **A headless command line** | 13 verbs that open a project and export, inspect or rewrite it without a GUI. The real automation surface. |
| **A headless command line** | 13 verbs that open a project and export, inspect or rewrite it without a GUI. |
| **JavaScript scripting** | `--run script.js project.qet`, or "Run Script..." in the GUI — read the model, export, edit geometry with real undo. See **[JavaScript Scripting](scripting)**. |
| **XML files** | `.qet` and `.elmt` are plain XML you can read and write with any tool. |
| **An external companion program** | `qet_tb_generator`, launched from a menu entry. |
| **C++ source** | For anyone building QET itself, or a fork. |
**What it does not offer:** embedded scripting of any language, a plugin API, a
loadable-module directory, or an IPC/automation interface into a running
instance.
**What it still does not offer:** a plugin API, a loadable-module directory,
or an IPC/automation interface into an already-running instance. The
JavaScript engine above is explicit, one-shot script execution, not a plugin
system — nothing runs unless you tell it to.
---
@@ -282,12 +284,14 @@ Stated plainly, because this page previously claimed otherwise:
| Claim | Reality |
|---|---|
| Embedded Python scripting | No interpreter is linked or loaded. Every Python reference in the source is the `qet_tb_generator` launcher. |
| A plugin system / plugin interface | No `QPluginLoader`, no plugin ABI, nothing that loads external code. |
| `~/.local/share/QElectroTech/plugins/` and platform equivalents | QET never reads these paths. Creating them does nothing. |
| An automation API into a running instance | None. SingleApplication forwards *file arguments* to a running instance; that is all. |
| Embedded Python scripting | Still no Python interpreter. Every Python reference in the source is the `qet_tb_generator` launcher. |
| A plugin system / plugin interface | Still no `QPluginLoader`, no plugin ABI, nothing that loads external code, and no directory a project or a third party can drop code into. |
| An automation API into a running instance | Still none. SingleApplication forwards *file arguments* to a running instance; that is all. |
Whether QET should gain a scripting interface is an open question — see the
[Vision](vision) page and the
[Development Roadmap](development_roadmap). If it does, this page will say so
when it is real, and not before.
**One thing that changed:** QET now has an embedded **JavaScript** engine
(QJSEngine, not Python) for explicit, one-shot script runs — `--run script.js
project.qet`, or "Run Script..." in the GUI. Not a plugin system: nothing
runs automatically, no project file can carry or trigger a script, and it
only reads/exports/edits through a narrow, named API, not arbitrary code
against QET's internals. See **[JavaScript Scripting](scripting)** for what
it actually does.
+10 -7
@@ -196,6 +196,7 @@ Arguments are **positional**, not `--flag=value`. The shape is always
| `--resave` | rewritten `.qet` |
| `--set-titleblock` | `.qet` with title-block fields stamped |
| `--check-elements` | validates `.elmt` files or a directory of them |
| `--run` | run a JavaScript macro against a project — see **[JavaScript Scripting](scripting)** |
Plus `--show-terminals`, which paints terminal markers into PDF/PNG/SVG output.
@@ -289,12 +290,15 @@ wait
For complex automation:
1. Use the export flags above — they cover PDF, images, BOM, wiring lists,
nets, cross-references and title-block stamping
2. For anything they do not cover, write a program of your own that reads and
writes the `.qet` / `.elmt` XML directly
3. See **[Automating QElectroTech](api_reference)** for both
2. For loops, conditionals, or reading the model to decide what to export,
use JavaScript scripting: `--run script.js project.qet`, or "Run
Script..." in the GUI. See **[JavaScript Scripting](scripting)**.
3. For anything neither covers, write a program of your own that reads and
writes the `.qet` / `.elmt` XML directly — see
**[Automating QElectroTech](api_reference)**
QET has no embedded scripting language and no plugin system; automation is the
command line plus your own tools.
QET still has no plugin system: nothing runs automatically, and no project
file can carry or trigger a script. Every script run is explicit.
---
@@ -462,10 +466,9 @@ wait # Wait for all to finish
| Limitation | Workaround |
|-----------|-----------|
| No CLI option to create or edit a diagram | Edit the `.qet` XML directly — see **[Automating QElectroTech](api_reference)** |
| No CLI option to create or edit a diagram *from a flag* | Use JavaScript scripting (`--run`) for edits with real undo, or the `.qet` XML directly for anything scripting doesn't cover — see **[JavaScript Scripting](scripting)** and **[Automating QElectroTech](api_reference)** |
| One project per invocation | Loop in the shell; each run is independent |
| No DXF export from the CLI | Use GUI export (File → Export) |
| No embedded scripting language | Write an external program against the XML |
> **Corrected September 2026.** This section previously claimed QET had no
> headless export and no CLI PDF export, and pointed readers at Python
+279
@@ -0,0 +1,279 @@
# JavaScript Scripting
> **Status: pending.** Everything on this page describes
> [PR #891](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/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](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/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](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 `QObject`s — 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`
```bash
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).
```bash
#!/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
```js
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](cli_reference)** — same logic, same output, called from a
script instead of a flag:
```js
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:
```js
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
```js
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
```js
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](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/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
```js
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)
```js
// 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());
```
```bash
qelectrotech --run bump_and_export.js panel.qet
```
### CI: fail the build if a project won't open or export
```bash
#!/bin/bash
set -e
for f in projects/*.qet; do
qelectrotech --run ci_check.js "$f"
done
```
```js
// 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
```js
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](api_reference)** for the file-format
boundary this respects.
---
## See also
- **[CLI Reference](cli_reference)** — the export flags this feature wraps
- **[Automating QElectroTech](api_reference)** — the file-format and headless
export ground this builds on
- [PR #891](https://github.com/qelectrotech/qelectrotech-source-mirror/pull/891) — implementation, with the exact tests run against it
- [Issue #162](https://github.com/qelectrotech/qelectrotech-source-mirror/issues/162) — the original request and design discussion