Table of Contents
- JavaScript Scripting
- Why this exists, and why JavaScript
- Two ways to run a script
- What a script can do
- Worked examples
- Export everything after a revision (the issue's original ask)
- CI: fail the build if a project won't open or export
- Place several elements from a list, then save
- Errors and exit codes (headless)
- Limitations, on purpose
- See also
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
Qmlmodule 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
QtPdfalready is. A QET build without theQmlmodule 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
--runinvocation 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
- CLI Reference — the export flags this feature wraps
- Automating QElectroTech — the file-format and headless export ground this builds on
- PR #891 — implementation, with the exact tests run against it
- Issue #162 — the original request and design discussion
Getting Started
🌐 Languages — English · Français · Deutsch
Guides
Conductors — wire properties, what feeds which export, and cables
Wire & cable catalogue — cable types, IEC 60757 core colours, assigning a core to a conductor
Printing and exporting — paper, PDF, images, and what each path does differently
Linking elements — master, slave, terminal
PLC modules — I/O tables and linking a wire to a specific point
Using the element editor — drawing tools, saving, checks
Preferences reference — what each settings page does
Keyboard-only control — mouseless QET, and the one real gap
Mouse modifiers — what Shift, Ctrl and Alt change while you drag
Managing collections — folders, writability, building your own shortlist
Templates — reusable multi-element blocks, and why clicking one does nothing
Search & Replace — bulk property changes
Building a nomenclature query — the BOM/summary table builder
Linking wires across pages — folio reports
Variables & formulas — %f, %{label}, sequences
Auto-numbering — schemes, sequences, freezing
Terminal strips — strips, levels, bridges
Title block templates — the .titleblock format
Importing EPLAN parts (.edz) — EPLAN Data Portal
DXF import & export — two unrelated features, one format
The project database — the in-memory SQLite cache
Development
Automating QET — CLI, XML formats, external tools
CLI Reference — command line usage
JavaScript Scripting — --run, geometry editing, undo
Vision — proposal, under discussion
