Commit Graph
3 Commits
Author SHA1 Message Date
brad fa2d4e89d8 Resolve linkctl explicitly in the widget, and surface failures
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.
2026-10-07 22:51:23 -07:00
brad eb303db316 Add keyboard, gamepad and directional-shortcut control
Three input methods for the gimbal, all sharing the widget's velocity model:
hold to move, release to stop.

Keyboard in the applet. WASD and the arrow keys feed the same tick timer the
pointer does, through a new vectorFromKeys helper. Opposing keys cancel and a
diagonal is normalised to length 1, so two keys do not move faster than one.
Auto-repeat is discarded, because it arrives as a press/release cycle and
would make the motion stutter. The held keys are cleared when the popup loses
focus: otherwise the matching release never arrives and the camera keeps
panning on its own. The buttons and zoom slider are set to Qt.NoFocus so the
arrow keys still reach the pad after one of them is clicked.

Gamepad support, as a separate process. Qt 6 removed QtGamepad and ships no
replacement, so the applet cannot read a controller itself. bin/linkctl-gamepad
reads /dev/input/js* directly using only the standard library: the event
struct is eight bytes and needs no third-party package. Defaults target a
DualSense on hid_playstation, but axis and button numbering comes from the
driver rather than the controller and varies between kernels and connection
modes, so --monitor prints live events for mapping and every number is
overridable. Opening a joystick replays the current state of every button with
JS_EVENT_INIT set; those are filtered, or a held button would fire its mapped
action at startup. --wait reconnects a wireless pad that sleeps.

Directional global shortcuts. Meta+Alt plus the arrow keys nudge the gimbal.
These are discrete steps by necessity: kglobalaccel fires once per keypress
and exposes no press-and-hold, so this cannot be smooth motion and the README
says so and points at the widget or a gamepad instead. NUDGE sets the degrees.

Tested: 145 checks across five suites, all passing, shellcheck clean, QML free
of syntax errors. 13 new node checks cover the key vector, including the
cancellation and diagonal-normalisation cases. 36 new python checks cover
event decoding, the INIT flag, deadzone rescaling and command construction.
A new FIFO-driven suite stands in for /dev/input/js* and plays scripted events
through the real select/read/decode/tick path, confirming diagonals emit both
axes, a centred stick goes quiet, a deadzone deflection moves nothing, a real
button press fires, the initial-state burst does not, and disconnect is
handled.

Not verified: no physical controller, camera or Plasma session was available,
so the synthetic events cannot confirm a real DualSense reports the assumed
axis and button numbers.
2026-10-07 22:16:52 -07:00
brad bf6595614a Fix KDE shortcuts never binding, and add a joystick widget
The shortcuts wrote the Plasma 5 value format and so never bound anything,
which is what produced "No default shortcuts" in System Settings.

kglobalacceld's KServiceActionComponent::loadSettings reads the services
_launch key as a bare key sequence and hands it straight to
QKeySequence::fromString. Plasma 5 stored a "keys,default,name" triplet
there and Plasma 6 does not, so "Meta+Alt+T,none,Insta360: Toggle AI
tracking" parsed to an empty sequence and bound no key. The value is now
just the key sequence; alternates would be tab-separated and "none" means
unbound.

Two supporting changes. The desktop entries now carry X-KDE-Shortcuts,
which is where kglobalacceld reads an entry's default shortcut from, so the
binding shows up in System Settings as a default instead of as nothing.
And they are no longer NoDisplay by default: KApplicationTrader skips
NoDisplay services, so hiding them from the launcher also hid them from the
Shortcuts page, which is the opposite of helpful. HIDE_FROM_MENU=1 restores
the old behaviour.

--list is now a diagnostic rather than a bare dump: it reports the
registered key, the default from the desktop entry, whether the entry file
exists, and flags any comma-bearing value as the unparseable Plasma 5 form.
It would have identified this fault directly.

The new plasmoid is a Plasma 6 applet: a velocity-style virtual joystick
plus zoom slider and Home/Center/Tracking buttons. It drives the gimbal
with relative moves, not absolute ones, because linkctl status reports no
pan or tilt while the camera is idle, leaving no trustworthy position to aim
from; relative steps work in either state and cannot drift, and linkctl
clamps them to the device range. The pure logic lives in a .js library so it
is testable outside Plasma.

Tested: 36 new node checks over shell quoting, tick command construction,
inversion, the per-axis threshold and the multi-object JSON merge, each
confirmed meaningful by mutation. Reproduced the broken shortcut state in a
throwaway HOME and verified --list flags it and reinstalling repairs it. All
QML passes qmllint with no syntax errors (KDE modules are unresolvable
offline). shellcheck clean. The applet has not been loaded in a real Plasma
session.
2026-10-07 21:03:38 -07:00