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.
278 lines
9.3 KiB
Bash
Executable File
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
|