Files
insta360-link-tools/kde/install-shortcuts.sh
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

278 lines
9.3 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# install-shortcuts.sh - register global hotkeys for the Insta360 Link under
# KDE Plasma.
#
# Plasma binds global shortcuts to application entries, so for each action
# this installs a .desktop file and the matching entry in kglobalshortcutsrc.
# The shortcuts then work from anywhere in the session, on Wayland as well as
# X11, which OBS's own hotkeys do not under Wayland.
#
# Usage:
# ./install-shortcuts.sh --dry-run show what would change, touch nothing
# ./install-shortcuts.sh install
# ./install-shortcuts.sh --list show what is currently registered
# ./install-shortcuts.sh --uninstall remove everything it installed
#
# Edit the BINDINGS table below to change the keys or the actions. Re-running
# the script is safe and applies the new table.
set -euo pipefail
# id | key sequence | linkctl-macro arguments | human label
#
# Meta+Alt is used because Plasma leaves that combination largely free.
# Anything Plasma already owns is reported as a conflict rather than stolen.
BINDINGS=(
"tracking-toggle|Meta+Alt+T|tracking toggle|Insta360: Toggle AI tracking"
"tracking-on|Meta+Alt+1|tracking on|Insta360: AI tracking on"
"tracking-off|Meta+Alt+0|tracking off|Insta360: AI tracking off"
"preset-home|Meta+Alt+H|preset load home|Insta360: Load home preset"
"center|Meta+Alt+C|center|Insta360: Recenter gimbal"
)
# The entries are visible by default: KApplicationTrader skips NoDisplay
# services, so hiding them also hides them from the Shortcuts page in System
# Settings. Set HIDE_FROM_MENU=1 to keep the launcher clean at that cost.
HIDE_FROM_MENU=${HIDE_FROM_MENU:-0}
readonly APPDIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
readonly PREFIX="linkctl-"
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
readonly SCRIPT_DIR
MACRO=${MACRO:-$SCRIPT_DIR/../bin/linkctl-macro}
DRY_RUN=0
MODE=install
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
info() { printf '%s\n' "$*"; }
run() {
if (( DRY_RUN )); then
printf ' would run: %s\n' "$*"
else
"$@"
fi
}
# Like run(), but discards the command's own chatter. Kept separate so that a
# redirect never swallows the dry-run description.
run_quiet() {
if (( DRY_RUN )); then
printf ' would run: %s\n' "$*"
else
"$@" >/dev/null 2>&1
fi
}
# Plasma 6 ships the *6 tools, Plasma 5 the *5 ones.
detect_plasma() {
if command -v kwriteconfig6 >/dev/null 2>&1; then
KWRITE=kwriteconfig6; KREAD=kreadconfig6
SYCOCA=kbuildsycoca6; PLASMA=6
elif command -v kwriteconfig5 >/dev/null 2>&1; then
KWRITE=kwriteconfig5; KREAD=kreadconfig5
SYCOCA=kbuildsycoca5; PLASMA=5
else
die "neither kwriteconfig6 nor kwriteconfig5 found; is this a KDE Plasma session?"
fi
}
desktop_file() { printf '%s/%s%s.desktop\n' "$APPDIR" "$PREFIX" "$1"; }
desktop_name() { printf '%s%s.desktop\n' "$PREFIX" "$1"; }
write_desktop() {
local id=$1 args=$2 label=$3 keys=$4 path
path=$(desktop_file "$id")
if (( DRY_RUN )); then
printf ' would write %s\n' "$path"
printf ' Exec=%s %s\n' "$MACRO" "$args"
printf ' X-KDE-Shortcuts=%s\n' "$keys"
return
fi
local nodisplay=false
(( HIDE_FROM_MENU )) && nodisplay=true
# X-KDE-Shortcuts is where kglobalacceld reads an entry's *default*
# shortcut from, so declaring it here is what makes the binding show up in
# System Settings instead of "No default shortcuts".
cat > "$path" <<EOF
[Desktop Entry]
Type=Application
Name=$label
Comment=Insta360 Link control via linkctl
Exec=$MACRO $args
Icon=camera-web
Terminal=false
NoDisplay=$nodisplay
Categories=AudioVideo;
X-KDE-Shortcuts=$keys
X-KDE-GlobalAccel-CommandShortcut=true
StartupNotify=false
EOF
info " wrote $path"
}
# Warn if some other action already owns this key sequence.
check_conflict() {
local id=$1 keys=$2 me existing
me=$(desktop_name "$id")
existing=$(grep -rl --include=kglobalshortcutsrc -F "$keys" \
"${XDG_CONFIG_HOME:-$HOME/.config}" 2>/dev/null || true)
[[ -z $existing ]] && return 0
local owner
owner=$(grep -B100 -F "$keys" "$existing" 2>/dev/null \
| grep -oE '^\[[^]]+\](\[[^]]+\])?' | tail -1 || true)
if [[ -n $owner && $owner != *"$me"* ]]; then
printf ' note: %s may already be bound by %s\n' "$keys" "$owner"
fi
}
stop_daemon() {
# Write while the daemon is down: it rewrites kglobalshortcutsrc on exit
# and would otherwise discard these entries.
if (( PLASMA == 6 )); then
run systemctl --user stop plasma-kglobalaccel.service 2>/dev/null \
|| run kquitapp6 kglobalacceld 2>/dev/null || true
else
run kquitapp5 kglobalaccel5 2>/dev/null || true
fi
}
start_daemon() {
if (( PLASMA == 6 )); then
if ! run systemctl --user start plasma-kglobalaccel.service 2>/dev/null; then
run kstart kglobalacceld 2>/dev/null \
|| info " could not restart kglobalacceld; log out and back in to apply"
fi
else
run kstart5 kglobalaccel5 2>/dev/null \
|| info " could not restart kglobalaccel5; log out and back in to apply"
fi
}
do_install() {
[[ -x $MACRO ]] || die "linkctl-macro not executable at $MACRO (set MACRO=/path)"
MACRO=$(cd -- "$(dirname -- "$MACRO")" && pwd)/$(basename -- "$MACRO")
info "Plasma $PLASMA detected; using $KWRITE"
info "macro script: $MACRO"
(( DRY_RUN )) && info "(dry run: nothing will be changed)"
run mkdir -p "$APPDIR"
info "Checking for conflicts:"
local entry id keys args label
for entry in "${BINDINGS[@]}"; do
IFS='|' read -r id keys args label <<< "$entry"
check_conflict "$id" "$keys"
done
info "Writing application entries:"
for entry in "${BINDINGS[@]}"; do
IFS='|' read -r id keys args label <<< "$entry"
write_desktop "$id" "$args" "$label" "$keys"
done
info "Stopping the global shortcut daemon:"
stop_daemon
sleep 1
info "Registering shortcuts:"
for entry in "${BINDINGS[@]}"; do
IFS='|' read -r id keys args label <<< "$entry"
# _launch is a bare key sequence. Plasma 5 stored a
# "keys,default,name" triplet here and Plasma 6 does not: it passes
# the value straight to QKeySequence::fromString, so a triplet parses
# to nothing and the shortcut silently never binds. Alternates are
# separated by a tab, and "none" means unbound.
run "$KWRITE" --file kglobalshortcutsrc \
--group services --group "$(desktop_name "$id")" \
--key _launch "$keys"
info " $keys -> linkctl $args"
done
info "Rebuilding the application cache:"
run_quiet "$SYCOCA" --noincremental \
|| info " $SYCOCA reported a problem (usually harmless)"
info "Restarting the global shortcut daemon:"
start_daemon
if (( DRY_RUN )); then
info "Dry run complete."
else
info ""
info "Done. Check them under System Settings > Keyboard > Shortcuts,"
info "searching for 'Insta360'. Test one with: $MACRO tracking toggle"
fi
}
do_uninstall() {
info "Removing shortcuts and application entries:"
stop_daemon
sleep 1
local entry id keys args label path
for entry in "${BINDINGS[@]}"; do
IFS='|' read -r id keys args label <<< "$entry"
run "$KWRITE" --file kglobalshortcutsrc \
--group services --group "$(desktop_name "$id")" \
--key _launch --delete
path=$(desktop_file "$id")
[[ -e $path ]] && run rm -f "$path" && info " removed $path"
done
run_quiet "$SYCOCA" --noincremental || true
start_daemon
info "Done."
}
do_list() {
local entry id keys args label value path dflt problems=0
printf '%-20s %-12s %-14s %-12s %s\n' ACTION KEY _launch DEFAULT ENTRY
for entry in "${BINDINGS[@]}"; do
IFS='|' read -r id keys args label <<< "$entry"
value=$("$KREAD" --file kglobalshortcutsrc \
--group services --group "$(desktop_name "$id")" \
--key _launch 2>/dev/null || true)
path=$(desktop_file "$id")
if [[ -r $path ]]; then
dflt=$(sed -n 's/^X-KDE-Shortcuts=//p' "$path" | head -1)
else
dflt=""
fi
# A comma here means a Plasma 5 triplet, which Plasma 6 cannot parse.
if [[ $value == *,* ]]; then
problems=1
value="$value <-- INVALID"
fi
printf '%-20s %-12s %-14s %-12s %s\n' \
"$id" "$keys" "${value:-<unset>}" "${dflt:-<none>}" \
"$([[ -r $path ]] && echo present || echo MISSING)"
done
if (( problems )); then
echo
echo "A _launch value containing commas is the Plasma 5 format and never"
echo "binds under Plasma 6. Re-run this script to rewrite them."
fi
}
while [[ $# -gt 0 ]]; do
case $1 in
--dry-run|-n) DRY_RUN=1; shift ;;
--uninstall) MODE=uninstall; shift ;;
--list|-l) MODE=list; shift ;;
-h|--help)
# Print the header comment block, stopping at the first code line.
awk 'NR>1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' \
"${BASH_SOURCE[0]}"
exit 0 ;;
*) die "unknown option: $1" ;;
esac
done
detect_plasma
case $MODE in
install) do_install ;;
uninstall) do_uninstall ;;
list) do_list ;;
esac