Generic rules for QML/JavaScript plugins in this style. They describe how code is divided, named, tested and failed, not what any one plugin does.
- Always add comments to all classes, entities, functions, and complex logic.
- Use easily readable language for comments and documents, do not be very abstract.
- Always prefer to use OOP-like programming, and following SOLID principles and clean code.
- Do not use em-dashes
—. - Use clear, descriptive names. No abbreviations (
c,s,k,sel,ctx,fn,btn,tmp), including in lambdas, loops over keys, shell variables and QML ids.i(andjwhen nested) is fine for a running index. - Name a variable for what it is:
selection,pluginService,pendingGroup, not its type or a shortening. - Do not shadow or reuse a name for a different meaning in the same scope.
- Every file starts with a short header stating its role and its rules.
- Match the surrounding code: comment density, naming, idiom.
- One concept, one place: no duplicated approaches (for example, one clamp function, not two variants).
- QML defines what exists, how things are connected, and how they react to change. UI, observable state,
property bindings, singletons shared across widgets, service objects, and system objects (such as
Process). - JavaScript decides and calculates. Parsing, formatting, validation, policies, migrations, request building.
Reusable logic lives in
.jsmodules; short component-specific handlers may stay inside QML. - Prefer QML. Add JavaScript only when a calculation or decision needs it, and keep the total small.
- JavaScript modules never touch UI, QML types or logging; they take plain data and return plain data.
- JavaScript modules should preferably be stateless. Use
.pragma library; classes and per-instance data are allowed. Shared queues, timers, caches, and observable application state belong to QML objects.
- Mirror the structure in
src/andtests/. Each folder has one role:QML/Contracts: data holders.QML/Services: stateful objects shared across the plugin.QML/Helpers: per-widget helper items.QML/Views: UI only.JS/Contracts: value domains (enums withisValid/label) and the exception type.JS/Helpers: pure logic modules.JS/<Integration>/<Name>/: everything specific to one external integration (see section 6).
- Dependencies point one way: Views → Helpers/Services → Contracts. A lower layer never imports a higher one.
- Keep DMS entry points focused on composing views and connecting host APIs. Per-instance controller helpers own form actions and widget lifecycles. Controllers receive data and emit signals; they never reference visual controls.
- Shared Dux widgets use
qs.DCommon.Widgetsand their currentD*names. Keep shell-specific plugin types inqs.Modules.Plugins. Bar text and icons follow the host'stextSizeandiconSize. - Use
StyledTextfor text,DTooltipHostwithmultiline: truefor error tooltips, andDScrollbarfor scrolling. - One type per file; the file name equals the type name. Every QML folder has a
qmldirlisting exactly its files.
- A contract is a QML type with typed properties. It is a data holder.
- Allowed in a contract: defaults, and a validity flag per single field. Not allowed: parsing, loops, cross-field checks, policy, conversion, I/O.
- Cross-field validation, parsing, clamping and conversion go to separate JS modules or services.
- One contract per file. When a new shared structure appears, add its contract, a test, and an audit entry.
- Where building thousands of objects would cost more than it gives (raw bulk data), keep plain data and wrap only what is stored or shown. Bulk data is bounded by what is needed (section 8).
- Enumerations of "what exists" are one enum (for example a
ProviderType); nothing else enumerates it.
- A service is stateful: it owns state (queues, caches, configuration, lists of loaded things). Stateless helpers are not services; they are JS modules.
- Shared state is a
pragma Singletonregistered inqmldirassingleton. Per-resource state (one queue per external provider) is an instance created and handed out by a singleton manager. - A manager only manages: it enumerates what is supported, initialises it, and hands out the facade. It does no work itself.
- A facade is the only door to its resource. The rest of the code never reaches around it to the underlying JS/API. Enforce this with an audit test.
- A broken component is recorded and reported (a load error list); it never prevents the others from working.
- Only the current data format flows through the code. Handling of older saved formats lives in one module in the storage layer and runs before anything else sees the data.
- Persisted data is never silently discarded: damaged data is reported and backed up before anything can overwrite it.
- Every write reports whether it really happened; a failed write is shown to the user and never looks like success.
- Only one object touches storage; everything else goes through it.
- Persisted identifiers are stable internal handles (UUIDs), never display text.
- Each integration is a folder with declared properties as plain data (names, URLs, rules, limits) and an API module (build requests, read answers) plus its constants.
- Provider API classes extend
JS/Contracts/ProviderApi.js; declarations extendJS/Contracts/ProviderProperties.js. Both modules exportcreate(). Override every required API method; optional hooks have inherited defaults. - Lint checks every provider's inheritance, required overrides, method signatures, property names and property types
before starting QML.
node audit/CheckProviders.jsruns these contributor checks on their own. - The properties are checked as a whole when the integration is loaded; unknown keys and inconsistent declarations are errors.
- Integrations should preferably be stateless and see only current-format data. They know nothing about storage, UI, retries or other integrations.
- Limits (ranges, resolutions, polling rates) are declared by the integration and verified against the real service. Requests outside the declared limits are refused before any request is made; nothing is silently approximated.
- Retries, queues, timeouts and response interpretation are centralised once, in the facade, not per integration.
- Add a new integration by adding a folder, one enum value and one registry entry. No other file changes.
- JavaScript throws one typed error (
PluginError) with a friendlyuserMessage, developerdetails, and aretryableflag. Never throw a bareErroror a string. - Only QML layers catch and report, through one error service. Users only ever see
userMessage; raw exception text goes to the log. - Nothing swallows an exception. A
catchrethrows, reports, wraps, or is a documented exception. - Logging goes through one logging service (a singleton) that alone knows dev mode: callers just call
debug(scope, text)/error(scope, text)and never check dev mode or useconsole.*. Log decisions and outcomes (queued, started, retried, finished with counts, saved, migrated, skipped and why), never URLs, keys or credentials. - Retry only transient failures, a bounded number of times, with growing delay; permanent failures are final. Every loop and queue is bounded.
- A failure shown per item (a card, a row) replaces only the information that failed; the item stays usable.
- Notifications are rate-limited so a failing source cannot spam the user.
- Work is bounded by what is needed: compute the window (for example, a count of newest items) up front, request no more than the API allows, and parse only that many.
- Timers are explicit and one-shot unless there is a reason to repeat; repeating timers stop when there is nothing
to do. No raw
setTimeout/setInterval. - Process work (shell, network) is queued and limited in concurrency.
- Share identical in-flight requests while preserving each caller's context, parsing limits and error handling. Discard obsolete callers before launching queued work. A rate-limit response pauses the affected provider's whole queue, with a bounded retry budget and growing cooldown.
- Restore the refresh schedule only for valid cached data with matching settings and sane timestamps. Startup fetches only what is due; an explicit manual refresh still fetches immediately, subject to rate limits.
- Keep bulk chart candles as plain validated data. Removing a symbol releases its data, scheduling state and errors.
- All stored and compared times are UTC; local time is never used except for relative labels.
- Values reaching a shell or URL are validated by a strict anchored pattern that cannot admit whitespace, quotes or shell/URL metacharacters. Pass URLs and arguments as separate process arguments; never splice them into shell text.
- Secrets are not logged, and not stored as plain text where an alternative exists.
- No hard-coded user-visible defaults scattered in code: defaults and limits live in constants or in the declaration that owns them.
- Test on the real runtime where behaviour depends on it. Run QML headless and instantiate real contracts, services and singletons; text-matching a source file is not a behaviour test.
- Layers of tests:
- Node tests for pure logic.
- Real QML tests for contracts, services and facades.
- Static audits for architecture rules (layers, one contract per file, no legacy handling outside the migration module, JS size ceiling, no direct reach around a facade).
- An opt-in live test that exercises the real external services end to end.
- Load the real entry points (the widget, the settings page) in an integration test, against stand-ins for the host's own modules that keep the host's contract (required properties, types, when data is delivered), with the network and other external tools replaced by deterministic fakes. A unit test of each file cannot see ordering and timing problems between them.
- Stand-ins for the host's modules must sit exactly where the real types live (audited against the installed host when present); a stand-in in the wrong module hides a missing import.
- Create host-loaded pages in tests the way the host does: the page first, its services attached afterwards, host controls that save on every change, data that changes later. Assert that nothing is reported to the user in that window.
- Any runtime warning from the code under test (an undefined property, a component that is not ready, a binding
error) fails the test run. Cache fault-injection tests may declare a bounded number of Quickshell's known
file-open warnings for an exact path: upstream prints these even with
printErrors: false. Other warnings still fail. - Do not assert source text as a substitute for behaviour; a static check that encodes an assumption hides the bug.
- Tests mirror the source structure. Tests that need fresh state reset it in setup; no state leaks between tests.
- Every bug fix and every rule above gets a test or an audit that would have caught it.
- Keep the whole suite green before finishing any change.
Development tools and their editor imports live in audit/. The installation script remains at
support/setup-symlink.sh. From the repository root, run:
node audit/Lint.js
node tests/RunAll.jsTo format all QML and JavaScript files, run:
npm exec --yes --package=prettier@3.6.2 -- node audit/Format.jsAdd --check to check formatting without changing source files. This uses Qt 6 qmlls and downloads
Prettier as a development tool. Formatting settings live in .qmlformat.ini and .prettierrc.json;
QML JavaScript directives are preserved automatically. Run lint and tests afterwards.
Lint checks provider contracts, QML language-server warnings and errors, and JavaScript syntax. It requires Node.js, installed
Qt 6/Quickshell QML modules, and Qt 6 qmlls. It finds the Qt QML VS Code extension's language server
automatically, or you can set QMLLS to its executable path. Set QML_IMPORT_PATH for a custom QML
installation. No npm packages are required. The test suite requires Quickshell and runs headlessly with
isolated cache directories and deterministic provider responses. node tests/RunAll.js --live additionally
tests the real providers and requires network access.
During desktop testing, dms ipc call plugins reload markets reloads the entry point, but Dux may retain
imported QML views. If a layout change or a fixed load error still appears stale, run dms restart once
to clear the running shell's QML cache. This preserves saved symbols and market data.
The lint command also prepares the editor imports. After a fresh checkout or a Quickshell update, run it
before opening the editor; node audit/PrepareQmlImports.js prepares only the imports. Reload the QML
language server if it was already running. The workspace settings resolve qs.* through the same DMS
interface stand-ins audited by the tests, and Qt/Quickshell through the installed QML directories.
Quickshell has an upstream issue: some versions omit enum information needed by the editor. The lint command adds the missing information to a local copy for editor use, keeping warning checks enabled. This workaround leaves the installed package and runtime behavior unchanged.
Production JavaScript libraries use QML's .pragma and .import syntax, so the workspace associates
them with QML editor mode to avoid false syntax warnings. The lint command checks their directives and
syntax, but does not yet check for issues such as undefined names or unused variables.
- Decide, then act: when the rule is clear, apply it everywhere rather than in one place.
- Ask before assuming when a choice changes behaviour; otherwise pick the conventional default and say so.
- When a rule changes, update the tests, the audits, the documentation and the changelog together.
- Report what changed and what was not verified, plainly.