# Development How to work on this plugin, and the traps that cost an afternoon each. ## Developing node tools/test.js # 369 checks, no network omarchy plugin validate "$PWD" omarchy-restart-shell # see the note below Fourteen things will cost you an afternoon if nobody tells you: **QML caches imported JavaScript libraries for the life of the process.** Editing a `.qml` file hot-reloads; editing anything under `lib/` does not. Run `omarchy-restart-shell` after touching `lib/*.js` or you will be staring at your old code. Do **not** run `omarchy-refresh-shell` — despite the name it resets `shell.json` to defaults and takes your bar layout with it. **The compiled-QML cache can hold a failed compilation.** A new `.qml` file that failed to compile once — a missing `import qs.Ui`, say — can keep failing after the fix. `rm -rf ~/.cache/quickshell/qmlcache` and restart. **A `Repeater` handed a fresh JavaScript array rebuilds every delegate.** The vehicle positions are recomputed several times a second — that recomputation *is* the animation — and feeding that array straight to a `Repeater` reset the model on every tick: measured at Zurich HB, nineteen markers destroyed and rebuilt every second, each an `Item`, two `Rectangle`s, a `Text` and a `MouseArea`. The map still looked roughly right, which is what makes this one expensive to find. The fix is a `ListModel` of journey references synced against the tick, with the delegate looking up its own position; a vehicle entering or leaving the view then costs one delegate rather than all of them. **Assigning to a property breaks its declaration's binding, for good.** A `Process` that declares `stdinEnabled: true` and then closes the pipe with `stdinEnabled = false` in `onStarted` works exactly once. Every later run starts with stdin disabled: the child waits forever on input that never comes, and — if a guard exists against overlapping writes, as it does here — every subsequent write queues behind the one that will never finish. The first save of a session worked and none after it did. Arm such a property at each run rather than relying on the declaration. **A limit enforced by one enormous command is not enforced.** The tile cache kept its newest four hundred sheets and deleted the rest in a single `rm` built from the whole directory listing. That works up to roughly `ARG_MAX` and then stops working entirely: 573 000 paths is about 15 MB of argv, `execve` returns `E2BIG`, nothing is deleted, and the cap quietly stops applying at exactly the size that made it worth having. Nothing in the UI changes, because the cache is not something the UI shows. The same listing was also collected whole — `ls` has no ceiling and neither has `StdioCollector` — so the pathological directory cost half a gigabyte resident in a shell that lives all day. Read a listing one line at a time through a `SplitParser`, cap what a single pass keeps and what it removes, and let the passes repeat. **A panel's contents have no layout until it has been opened.** The chips exist from shell start, but their positioner has not run: every one reports `x = 0` and a width of a few dozen pixels, stacked at the strip's left edge. A probe on a timer fires long before anyone opens the panel, so it measures that state and reports it with total confidence — which sent an afternoon chasing a hit-test bug that only existed before layout. If you instrument geometry here, trigger the probe *after* opening the panel, and check `mapToItem(null, 0, 0)` to see whether what you are measuring is where you think it is. **A forgiving fallback hides the bug it was written to survive.** The mountain presets choose which stops to poll by mode, and `Model.stopsForModes` falls back to every stop when none serves the chosen ones — right for a valley that has only buses, and it also meant that when the panel stopped copying `modes` onto the stops it stored, the filter matched nothing, stepped aside exactly as designed, and polled six bus stops for cable cars. No error, no warning, an empty map and a feature that looked implemented. If a fallback exists to tolerate missing data, something has to distinguish "there are genuinely none" from "the field never arrived" — here, a probe that printed `candidates=16` out of 16 when three were expected. **A `Loader` keeps its item's implicit size after unloading it.** Set `active` false and the item really is destroyed — nothing is drawn — but the Loader goes on reporting the height that item had, and a `Column` goes on reserving it. One look at a stop card left three hundred pixels of void under the map for the rest of the session, one look at a vehicle card left two hundred more, and nothing the user did afterwards shrank it back: it read as a layout that had given up rather than as a stale number. Take the height from the item instead — `height: item ? item.implicitHeight : 0` — which reads zero exactly when there is nothing loaded. **A fill-parent MouseArea swallows everything nested inside it.** Declared after its siblings it paints last, so it is on top, so it takes every press in the item — including presses on a MouseArea inside a child. The boxed line number in a departure row never fired once: clicking "214" quietly followed a bus instead. Restacking with `z: -1` is the idiomatic fix and it is also the assumption that broke, and there is no way to exercise a click on a headless machine — `/dev/uinput` is root-only, and `childAt()`, the obvious way to ask which item would be hit, **ignores `z` entirely**, so it reports the same answer for a broken layout and a fixed one. Both the stop card and the chips now compare the press position against the inner control's own rectangle instead. That needs no stacking assumption, and it can be checked by running the decision on synthetic coordinates, which `tools/` cannot do but a temporary `Component.onCompleted` probe can. **A latch that records the wrong thing is invisible until someone travels.** Detection was meant to run once; the flag was set before the response was examined, so it recorded "asked" rather than "answered" and one rate-limited reply disabled the feature permanently. Nothing failed loudly — the map just opened at the default forever. `Model.detectionFrom` now returns the decision as three cases (usable, settled-but-unusable, no answer) so the distinction is a thing a test can hold, and `tools/test.js` pins the 429 shape exactly. **A byte can hide in a string literal.** `lib/Model.js` carried a literal NUL as a map-key separator — valid JavaScript, invisible in an editor, and enough to make `file(1)` call the source "data" and `grep` skip it silently as binary. A search that returns nothing then reads as "this function does not exist". Write control characters as `\u0000` escapes. **A Swiss stop reference is not shaped the way the OJP documentation says.** The specification's examples write a quay as `ch:1:sloid:91178::0`, with a doubled colon. The platform sends `ch:1:sloid:3000:503:43`, with single ones. Code that folds a quay to its station by splitting on `"::"` passes every example in the documentation and returns an empty departure board against every real response. `Model.stopPlaceOf` counts fields instead, which handles both. **`api.opentransportdata.swiss` publishes an AAAA record of `::`.** That is the unspecified address and it is not routable anywhere. On a host without global IPv6, curl prefers the AAAA, fails to connect in about a millisecond, and does *not* fall back to the A records — so the planner reads as unreachable while every other host works. The plugin retries once forced onto IPv4 after a connection failure. Forcing IPv4 outright would be the easy fix and would break anyone on an IPv6-only network. **Nerd Font glyph codepoints are not the upstream Material Design ones**, and they have moved between Nerd Font releases. Writing them from memory is how the first version of this plugin shipped a **helicopter** in the bar where it meant a locomotive: the glyph existed, rendered cleanly, and was simply the wrong vehicle. A wrong codepoint fails as a plausible picture, not as an error. So `lib/Modes.js` is generated: tools/font-glyphs.py --search train # find a glyph by name tools/font-glyphs.py --emit > lib/Modes.js It parses the installed font's own `cmap` and `post` tables and resolves every glyph **by name**, failing loudly if a name is missing. Check a new glyph by rendering it at the size it will be drawn — several of the first set were the right idea and the wrong picture, and two modes were indistinguishable at thirteen pixels until they were looked at.