metadata.json claimed MIT while no LICENSE file existed, so the repository had no effective licence at all. It is now GPL-3.0-or-later: the full FSF text in LICENSE, the identifier in metadata.json, and REUSE-style SPDX-FileCopyrightText and SPDX-License-Identifier headers on all 33 source files, which is the convention KDE itself uses. Worth recording that GPL-3.0 is a preference here rather than an obligation. Nothing in the dependency graph forces it: linkctl is MIT and is invoked as a subprocess rather than linked, and the KDE QML modules the applet imports are LGPL. Removed two things that should not ship in a public repository. tests's QML linter had an absolute sandbox path, /tmp/pyside/PySide6/qmllint, baked into its search list; it now looks for Qt's qmllint and PySide6's pyside6-qmllint on PATH and honours a QMLLINT override, and says how to install one when it finds nothing. The systemd unit carried a Documentation= URL pointing at a host that does not exist, and its ExecStart silently assumed a particular checkout directory; the URL is gone and the path is now an obvious PATH/TO/CHECKOUT placeholder next to the comment telling you to edit it. Added a Credits section naming the work this is built on, since the project is mostly glue: linkctl does the camera control, schlarpc's firmware teardown documents the extension-unit protocol, and vrwallace's controller established that the Link 2 needs both pan and tilt axes in a single VIDIOC_S_EXT_CTRLS call, which is the detail the whole thing depends on. Also made the two Python tests executable, matching the other scripts. Audited for operator-specific detail: no credentials, tokens, private hostnames, IP addresses or absolute home paths in any tracked file or anywhere in the history's diffs. Three merge commits do carry Reviewed-on and Reviewed-by trailers naming a private forge, added by the forge at merge time; those are commit metadata rather than file content and are handled separately. Tested: 169 checks across seven suites plus the QML linter, all passing after the header insertion, which confirms it did not disturb the order-sensitive `.pragma library` line in logic.js or the qmldir module definitions. shellcheck clean.
291 lines
10 KiB
Bash
Executable File
291 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# SPDX-FileCopyrightText: 2026 branderson
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#
|
|
# 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
|
|
|
|
# Degrees moved by one press of a directional shortcut.
|
|
NUDGE=${NUDGE:-5}
|
|
|
|
# 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"
|
|
# Directional nudges. kglobalaccel fires once per keypress and gives no
|
|
# press-and-hold, so each of these is a discrete step rather than smooth
|
|
# motion. For continuous movement use the Plasma widget or a gamepad.
|
|
"pan-left|Meta+Alt+Left|left $NUDGE|Insta360: Pan left"
|
|
"pan-right|Meta+Alt+Right|right $NUDGE|Insta360: Pan right"
|
|
"tilt-up|Meta+Alt+Up|up $NUDGE|Insta360: Tilt up"
|
|
"tilt-down|Meta+Alt+Down|down $NUDGE|Insta360: Tilt down"
|
|
)
|
|
|
|
# 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
|