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.
288 lines
9.9 KiB
Bash
Executable File
288 lines
9.9 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
|
|
|
|
# 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
|