stick is the id of the Joystick declared inside the fullRepresentation Component. Ids declared inside a Component are not visible from the enclosing scope, so the inNx, inNy and inEngaged properties declared on the root object could never resolve it. Those bindings threw, inEngaged stayed false, the tick timer never ran, and no input of any kind produced a command.
That is why the reporter's log showed binary resolution, status and zoom 1.0 all succeeding while dragging did nothing: only the movement path was broken, and it was broken at the binding level rather than the command level.
The three properties now live inside the fullRepresentation, where both stick and the root object are in scope. This was introduced when the keyboard support was added, which is exactly when pointer control stopped working.
Also fixed, from the same log
The configuration page logged does not have a property called cfg_<name>Default for all eight keys plus title. Plasma assigns those when it instantiates a config page, and they were never declared, so the Defaults button had nothing to compare against. Declared now.
Startup probed for the binary twice: Component.onCompleted and onConfiguredBinChanged both fired while the component was still being set up, duplicating every follow-up query. Visible in the log as # 1 and # 2 running the identical probe. The second path is guarded.
The diagnostics readout rendered empty, which is why it was no help. It was a Repeater over an array of expressions, and in that shape one throwing sub-expression discards the whole model and the panel shows nothing, which is precisely the failure mode it exists to diagnose. It is now a single label built by a single guarded function, with per-line try/catch.
The actual repair: the verification
qmllint had flagged stick, and my filter discarded it.
Without the KDE QML modules present, qmllint cannot resolve PlasmoidItem, so nothing inside it has a known scope and every identifier is reported as "Unqualified access" — 87 of them on a clean file. Filtering that category wholesale was the only way to get readable output, and it threw away the one finding that mattered. Three rounds of wrong theories followed from that.
tests/qmlstubs/ now holds minimal stand-ins for org.kde.plasma.plasmoid, org.kde.plasma.components, org.kde.plasma.plasma5support and org.kde.kirigami, so the types resolve and the scope analysis is meaningful. With them in place the only legitimate unqualified accesses are root, sh and modelData, each allowlisted in tests/lint_qml.py with its reason, so anything else is a genuine finding.
Proven by reintroducing the bug: the linter fails and names the exact lines and identifier.
FAIL main.qml:117: unqualified access to 'stick'. If this is an id from
another component, QML cannot resolve it from here.
2 files linted, 5 failures
With the fix in place it reports 0 unexpected, and stick no longer appears at all — the fix is confirmed by tooling rather than by argument.
Note for future config keys: adding one to contents/config/main.xml requires adding it to tests/qmlstubs/.../Configuration.qml as well, or the linter will not catch a typo in the key name. This is in the README.
Corrections to earlier claims in this PR
Three causes were advanced here before this one and all three were wrong:
PATH resolution. Ruled out by the reporter: a pacman package installs to /usr/bin. The resolver is kept as defensive hardening for mise/asdf installs, not as a fix.
A stale pinned device node. Ruled out: the device field was never set.
A stale compiled QML cache. Plausible but not the cause; the cache clearing in install.sh is kept as hardening.
Also kept from the earlier work, both real defects even though neither was this fault: command strings are made unique so Plasma's executable engine cannot serve a cached result instead of running anything, and the always-visible status line plus the busy watchdog remove two ways for the widget to fail silently.
Testing
169 checks across seven suites plus the new linter, all passing. shellcheck clean.
Not verified
No live Plasma session here. Unlike the three earlier theories, this one is a confirmed diagnosis: the error came from the reporter's own journal, it names the file and line, the mechanism is plain QML scoping, and the linter reproduces and then clears it.
**Summary generated by LLM agent**
## Root cause
```
main.qml: ReferenceError: stick is not defined
```
`stick` is the id of the `Joystick` declared inside the `fullRepresentation` Component. Ids declared inside a Component are not visible from the enclosing scope, so the `inNx`, `inNy` and `inEngaged` properties declared on the **root** object could never resolve it. Those bindings threw, `inEngaged` stayed `false`, the tick timer never ran, and no input of any kind produced a command.
That is why the reporter's log showed binary resolution, `status` and `zoom 1.0` all succeeding while dragging did nothing: only the movement path was broken, and it was broken at the binding level rather than the command level.
The three properties now live inside the `fullRepresentation`, where both `stick` and the root object are in scope. This was introduced when the keyboard support was added, which is exactly when pointer control stopped working.
## Also fixed, from the same log
The configuration page logged `does not have a property called cfg_<name>Default` for all eight keys plus `title`. Plasma assigns those when it instantiates a config page, and they were never declared, so the Defaults button had nothing to compare against. Declared now.
Startup probed for the binary twice: `Component.onCompleted` and `onConfiguredBinChanged` both fired while the component was still being set up, duplicating every follow-up query. Visible in the log as `# 1` and `# 2` running the identical probe. The second path is guarded.
The diagnostics readout rendered empty, which is why it was no help. It was a `Repeater` over an array of expressions, and in that shape one throwing sub-expression discards the whole model and the panel shows nothing, which is precisely the failure mode it exists to diagnose. It is now a single label built by a single guarded function, with per-line `try`/`catch`.
## The actual repair: the verification
**qmllint had flagged `stick`, and my filter discarded it.**
Without the KDE QML modules present, qmllint cannot resolve `PlasmoidItem`, so nothing inside it has a known scope and every identifier is reported as "Unqualified access" — 87 of them on a clean file. Filtering that category wholesale was the only way to get readable output, and it threw away the one finding that mattered. Three rounds of wrong theories followed from that.
`tests/qmlstubs/` now holds minimal stand-ins for `org.kde.plasma.plasmoid`, `org.kde.plasma.components`, `org.kde.plasma.plasma5support` and `org.kde.kirigami`, so the types resolve and the scope analysis is meaningful. With them in place the only legitimate unqualified accesses are `root`, `sh` and `modelData`, each allowlisted in `tests/lint_qml.py` with its reason, so anything else is a genuine finding.
Proven by reintroducing the bug: the linter fails and names the exact lines and identifier.
```
FAIL main.qml:117: unqualified access to 'stick'. If this is an id from
another component, QML cannot resolve it from here.
2 files linted, 5 failures
```
With the fix in place it reports `0 unexpected`, and `stick` no longer appears at all — the fix is confirmed by tooling rather than by argument.
Note for future config keys: adding one to `contents/config/main.xml` requires adding it to `tests/qmlstubs/.../Configuration.qml` as well, or the linter will not catch a typo in the key name. This is in the README.
## Corrections to earlier claims in this PR
Three causes were advanced here before this one and all three were wrong:
- **PATH resolution.** Ruled out by the reporter: a pacman package installs to `/usr/bin`. The resolver is kept as defensive hardening for `mise`/`asdf` installs, not as a fix.
- **A stale pinned device node.** Ruled out: the device field was never set.
- **A stale compiled QML cache.** Plausible but not the cause; the cache clearing in `install.sh` is kept as hardening.
Also kept from the earlier work, both real defects even though neither was this fault: command strings are made unique so Plasma's executable engine cannot serve a cached result instead of running anything, and the always-visible status line plus the `busy` watchdog remove two ways for the widget to fail silently.
## Testing
169 checks across seven suites plus the new linter, all passing. `shellcheck` clean.
## Not verified
No live Plasma session here. Unlike the three earlier theories, this one is a confirmed diagnosis: the error came from the reporter's own journal, it names the file and line, the mechanism is plain QML scoping, and the linter reproduces and then clears it.
The widget could not run linkctl even though the binary worked fine from a
terminal and through linkctl-macro.
Plasma executes applet commands with KProcess::setShellCommand, which is
`/bin/sh -c` inheriting plasmashell's own environment. That environment never
sources a login profile, so any install that puts linkctl on PATH from a shell
rc file is invisible to the widget: mise and asdf shims, and ~/.local/bin on
some distributions. bin/linkctl-macro has carried a resolver for exactly this
reason since it was written, and the widget should have had one too rather
than trusting a bare name. It now searches the same locations at startup,
caches the absolute path, and holds commands back until it has one.
Three further changes, all about not failing silently.
The status line is now always visible. It previously appeared only while the
pan and tilt readout was unknown, so any failure after the first successful
poll was hidden behind a stale position.
errorMessage explains the exit codes that are easy to misread. 127 is the shell
failing to run the binary rather than anything about the camera, and 3 most
often means a pinned device node has moved, which happens because the Link's
/dev/videoN number is not stable across power cycles or replugs.
A watchdog clears the busy flag after four seconds. Plasma's executable engine
silently drops a request while an identical command is still running, so a lost
callback could leave busy set and freeze every subsequent step with nothing to
indicate why.
Also corrected the focus timing for the keyboard support: Component.onCompleted
can run before the popup is on screen, where the focus grab does not stick, so
the panel claims focus whenever the popup opens and clears held keys when it
closes.
Tested: 164 checks across seven suites, all passing. tests/test_resolver.sh is
new and runs the generated resolver in a real shell against real directories,
confirming it finds a binary on PATH, finds mise shims, ~/.local/bin and
~/.cargo/bin when they are not on PATH, prefers PATH when both exist, ignores
a non-executable file, and prints nothing when the binary is genuinely absent.
11 new node checks cover the command's shape and confirm a hostile configured
name stays quoted.
Not verified against a live Plasma session. The PATH mechanism is established
from KProcess's documented behaviour and the resolver from a real shell, but
whether this was the actual cause on the reporter's machine is unconfirmed.
Plasma's executable data engine keys its command containers by the command
string. DataEngine::connectSource calls requestSource, which returns a
pre-existing container when one matches, and then delivers that container's
cached data; the process itself only starts when the container is first
constructed. Re-issuing a byte-identical command therefore reports success,
with a plausible exit code and stdout, while running nothing at all.
That is precisely what the joystick produces. A stick held at full deflection
emits the same command every tick, so the camera would take one step and then
stop, with the widget showing no error because it received a successful
result. The same applies to a key held down, and to repeated presses of
Center or Home.
Every command now carries a trailing shell comment holding a counter, so the
engine never sees a string it has already cached. `#` starts a comment in sh,
and the commands already contain quotes so they always go through sh -c,
meaning the suffix changes the engine's key without changing what runs.
Resolution state moved from comparing the probe's command string to a plain
boolean, since the string is no longer stable across calls.
Tested: 65 node checks, including that successive sequence numbers differ and
that the suffix lands after a chained diagonal command. tests/test_resolver.sh
additionally runs the generated command through a real shell both with and
without the suffix and requires identical output, so the uniqueness trick
cannot quietly alter behaviour. Full suite of 172 checks across seven suites
passes, shellcheck clean, QML free of syntax errors.
This is a defect established by reading Plasma's source, not a confirmed
diagnosis of the reported fault. The two earlier theories for that fault, PATH
resolution and a stale pinned device node, were both ruled out by the
reporter: linkctl is installed from a pacman package into /usr/bin, and the
widget's device field was never set.
dejima-agent
changed title from Resolve linkctl explicitly in the widget, and surface failures instead of hiding them to Fix cached commands that never run, and surface widget failures2026-10-07 23:10:07 -07:00
Dragging the joystick stopped doing anything, at the same time as the
keyboard support arrived and without the keyboard working either. One cause
explains both.
Before the keyboard existed, the tick timer ran on `stick.engaged` and read
`stick.nx` directly, so dragging depended on nothing but the pad. Adding the
keyboard routed that same path through the new helper: inEngaged reads
keyEngaged, which reads keyVec, which calls Logic.vectorFromKeys. If that
function is not callable the binding throws, keyVec is undefined, and
inNx and inEngaged go with it, so the timer never runs and the pointer stops
working too. Coupling the previously working path to brand new code with no
fallback was the mistake, independent of why the helper might be missing.
One way it can be missing: Qt caches compiled QML and JS on disk, and
replacing an applet's files does not reliably invalidate that cache, so a
stale compiled logic.js from before the helper was added is enough. install.sh
now clears the cache on every install, which costs one slower load.
The keyboard vector is now computed defensively and the pointer path reads
plain numbers that cannot throw, so a missing or stale helper degrades to "no
keyboard" rather than "nothing works".
Removed the two gates that could also silently stop everything. resolvedBin
starts as the configured name instead of empty, so there is always something
to run and the search can only upgrade it; the tick timer no longer requires
resolution to have succeeded; and refresh, action and refreshTracking no
longer return early doing nothing. A failed probe keeps the fallback and says
so rather than disabling the widget, which matters because a packaged install
is on PATH regardless.
Added a Diagnostics toggle showing live internal state: the resolved and
configured binary, the device, which helpers are actually callable, the
stick's own numbers, the keyboard vector, whether the panel holds focus,
whether the timer is running, the busy flag, the sequence number, and the last
command with its exit code. Every command and result also goes to the journal
via console.log. Every failure so far has presented as "nothing happens",
which is the one symptom that carries no information at all.
Tested: 169 checks across seven suites, all passing. shellcheck clean, QML
free of syntax errors. The cache clearing was exercised against a fake cache
tree under a temporary XDG_CACHE_HOME.
Not verified in a live Plasma session. The coupling described above is plain
in the code and is a real defect whatever triggered it; whether a stale QML
cache was the trigger is unconfirmed.
dejima-agent
changed title from Fix cached commands that never run, and surface widget failures to Fix the keyboard path disabling the pointer path, and add widget diagnostics2026-10-07 23:19:26 -07:00
The joystick, the keyboard and every movement command did nothing. The cause
was in the journal all along:
main.qml: ReferenceError: stick is not defined
`stick` is the id of the Joystick inside the `fullRepresentation` Component.
Ids declared inside a Component are not visible from the enclosing scope, so
the inNx, inNy and inEngaged properties declared on the root object could not
resolve it. Those bindings threw, inEngaged stayed false, the tick timer never
ran, and no input of any kind produced a command. Everything else kept
working, which is why status, zoom and binary resolution all looked healthy in
the log while dragging did nothing.
The three properties now live inside the fullRepresentation, where both
`stick` and the root object are in scope.
Also fixed from the same log: the configuration page logged "does not have a
property called cfg_<name>Default" for all eight keys plus `title`, because
Plasma assigns those when it instantiates a config page and they were never
declared. They are declared now, so the Defaults button has something to
compare against.
Startup probed for the binary twice, because Component.onCompleted and
onConfiguredBinChanged both fired while the component was still being set up,
duplicating every follow-up query. The second path is now guarded.
The diagnostics readout is one label built by one guarded function instead of
a Repeater over an array of expressions. In that earlier shape a single
throwing sub-expression discards the whole model and the panel renders empty,
which is precisely the failure mode it is supposed to diagnose, and is what it
did.
The real repair is to the verification, in tests/lint_qml.py. qmllint had
flagged `stick`, and a filter discarded it. Without the KDE QML modules
present qmllint cannot resolve PlasmoidItem, so nothing inside it has a known
scope and all 87 identifiers in a clean file are reported as unqualified
access. Filtering that category wholesale was the only way to get a readable
result, and it threw away the one finding that mattered.
tests/qmlstubs now holds minimal stand-ins for org.kde.plasma.plasmoid,
org.kde.plasma.components, org.kde.plasma.plasma5support and
org.kde.kirigami, so the types resolve and the scope analysis is meaningful.
With the stubs in place the only remaining unqualified accesses are `root`,
`sh` and `modelData`, each allowlisted with a reason, so anything else is a
genuine finding. Confirmed by reintroducing the bug: the linter fails and
names the exact lines and identifier.
Tested: 169 checks across seven suites plus the new linter, all passing.
shellcheck clean.
dejima-agent
changed title from Fix the keyboard path disabling the pointer path, and add widget diagnostics to Fix QML scope error that disabled all camera movement, and close the lint gap that hid it2026-10-07 23:29:05 -07:00
branderson
approved these changes 2026-10-07 23:52:22 -07:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary generated by LLM agent
Root cause
stickis the id of theJoystickdeclared inside thefullRepresentationComponent. Ids declared inside a Component are not visible from the enclosing scope, so theinNx,inNyandinEngagedproperties declared on the root object could never resolve it. Those bindings threw,inEngagedstayedfalse, the tick timer never ran, and no input of any kind produced a command.That is why the reporter's log showed binary resolution,
statusandzoom 1.0all succeeding while dragging did nothing: only the movement path was broken, and it was broken at the binding level rather than the command level.The three properties now live inside the
fullRepresentation, where bothstickand the root object are in scope. This was introduced when the keyboard support was added, which is exactly when pointer control stopped working.Also fixed, from the same log
The configuration page logged
does not have a property called cfg_<name>Defaultfor all eight keys plustitle. Plasma assigns those when it instantiates a config page, and they were never declared, so the Defaults button had nothing to compare against. Declared now.Startup probed for the binary twice:
Component.onCompletedandonConfiguredBinChangedboth fired while the component was still being set up, duplicating every follow-up query. Visible in the log as# 1and# 2running the identical probe. The second path is guarded.The diagnostics readout rendered empty, which is why it was no help. It was a
Repeaterover an array of expressions, and in that shape one throwing sub-expression discards the whole model and the panel shows nothing, which is precisely the failure mode it exists to diagnose. It is now a single label built by a single guarded function, with per-linetry/catch.The actual repair: the verification
qmllint had flagged
stick, and my filter discarded it.Without the KDE QML modules present, qmllint cannot resolve
PlasmoidItem, so nothing inside it has a known scope and every identifier is reported as "Unqualified access" — 87 of them on a clean file. Filtering that category wholesale was the only way to get readable output, and it threw away the one finding that mattered. Three rounds of wrong theories followed from that.tests/qmlstubs/now holds minimal stand-ins fororg.kde.plasma.plasmoid,org.kde.plasma.components,org.kde.plasma.plasma5supportandorg.kde.kirigami, so the types resolve and the scope analysis is meaningful. With them in place the only legitimate unqualified accesses areroot,shandmodelData, each allowlisted intests/lint_qml.pywith its reason, so anything else is a genuine finding.Proven by reintroducing the bug: the linter fails and names the exact lines and identifier.
With the fix in place it reports
0 unexpected, andstickno longer appears at all — the fix is confirmed by tooling rather than by argument.Note for future config keys: adding one to
contents/config/main.xmlrequires adding it totests/qmlstubs/.../Configuration.qmlas well, or the linter will not catch a typo in the key name. This is in the README.Corrections to earlier claims in this PR
Three causes were advanced here before this one and all three were wrong:
/usr/bin. The resolver is kept as defensive hardening formise/asdfinstalls, not as a fix.install.shis kept as hardening.Also kept from the earlier work, both real defects even though neither was this fault: command strings are made unique so Plasma's executable engine cannot serve a cached result instead of running anything, and the always-visible status line plus the
busywatchdog remove two ways for the widget to fail silently.Testing
169 checks across seven suites plus the new linter, all passing.
shellcheckclean.Not verified
No live Plasma session here. Unlike the three earlier theories, this one is a confirmed diagnosis: the error came from the reporter's own journal, it names the file and line, the mechanism is plain QML scoping, and the linter reproduces and then clears it.
Resolve linkctl explicitly in the widget, and surface failures instead of hiding themto Fix cached commands that never run, and surface widget failuresFix cached commands that never run, and surface widget failuresto Fix the keyboard path disabling the pointer path, and add widget diagnosticsThe joystick, the keyboard and every movement command did nothing. The cause was in the journal all along: main.qml: ReferenceError: stick is not defined `stick` is the id of the Joystick inside the `fullRepresentation` Component. Ids declared inside a Component are not visible from the enclosing scope, so the inNx, inNy and inEngaged properties declared on the root object could not resolve it. Those bindings threw, inEngaged stayed false, the tick timer never ran, and no input of any kind produced a command. Everything else kept working, which is why status, zoom and binary resolution all looked healthy in the log while dragging did nothing. The three properties now live inside the fullRepresentation, where both `stick` and the root object are in scope. Also fixed from the same log: the configuration page logged "does not have a property called cfg_<name>Default" for all eight keys plus `title`, because Plasma assigns those when it instantiates a config page and they were never declared. They are declared now, so the Defaults button has something to compare against. Startup probed for the binary twice, because Component.onCompleted and onConfiguredBinChanged both fired while the component was still being set up, duplicating every follow-up query. The second path is now guarded. The diagnostics readout is one label built by one guarded function instead of a Repeater over an array of expressions. In that earlier shape a single throwing sub-expression discards the whole model and the panel renders empty, which is precisely the failure mode it is supposed to diagnose, and is what it did. The real repair is to the verification, in tests/lint_qml.py. qmllint had flagged `stick`, and a filter discarded it. Without the KDE QML modules present qmllint cannot resolve PlasmoidItem, so nothing inside it has a known scope and all 87 identifiers in a clean file are reported as unqualified access. Filtering that category wholesale was the only way to get a readable result, and it threw away the one finding that mattered. tests/qmlstubs now holds minimal stand-ins for org.kde.plasma.plasmoid, org.kde.plasma.components, org.kde.plasma.plasma5support and org.kde.kirigami, so the types resolve and the scope analysis is meaningful. With the stubs in place the only remaining unqualified accesses are `root`, `sh` and `modelData`, each allowlisted with a reason, so anything else is a genuine finding. Confirmed by reintroducing the bug: the linter fails and names the exact lines and identifier. Tested: 169 checks across seven suites plus the new linter, all passing. shellcheck clean.Fix the keyboard path disabling the pointer path, and add widget diagnosticsto Fix QML scope error that disabled all camera movement, and close the lint gap that hid it