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.
170 lines
5.8 KiB
Bash
Executable File
170 lines
5.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# SPDX-FileCopyrightText: 2026 branderson
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#
|
|
# linkctl-macro - run a linkctl command as a desktop macro.
|
|
#
|
|
# Wraps linkctl so it is safe to bind to a global hotkey:
|
|
# - resolves the linkctl binary even in the sparse environment a desktop
|
|
# shortcut is launched with
|
|
# - passes --force, because a deliberate keypress means "do it now" even
|
|
# when no application currently holds the camera open
|
|
# - reports the result as a desktop notification, since tracking state and
|
|
# gimbal position are otherwise invisible
|
|
#
|
|
# Usage:
|
|
# linkctl-macro tracking on
|
|
# linkctl-macro tracking toggle
|
|
# linkctl-macro preset load home
|
|
# linkctl-macro center
|
|
#
|
|
# Any linkctl subcommand works; arguments are passed through untouched.
|
|
#
|
|
# Options (before the linkctl subcommand):
|
|
# --no-notify suppress the desktop notification
|
|
# --print echo the resolved command instead of running it
|
|
#
|
|
# Configuration, from the environment or ~/.config/linkctl-macro.conf:
|
|
# LINKCTL path to the linkctl binary
|
|
# LINKCTL_DEVICE video node to target, e.g. /dev/video0 (for multi-camera)
|
|
# LINKCTL_FORCE 1 (default) to pass --force, 0 to respect the idle guard
|
|
# LINKCTL_NOTIFY always (default) | error | never
|
|
|
|
set -uo pipefail
|
|
|
|
readonly CONF="${XDG_CONFIG_HOME:-$HOME/.config}/linkctl-macro.conf"
|
|
# shellcheck source=/dev/null
|
|
[[ -r $CONF ]] && source "$CONF"
|
|
|
|
: "${LINKCTL_DEVICE:=}"
|
|
: "${LINKCTL_FORCE:=1}"
|
|
: "${LINKCTL_NOTIFY:=always}"
|
|
|
|
# A desktop shortcut does not inherit an interactive shell's PATH, so look in
|
|
# the places linkctl is actually installed to before giving up.
|
|
#
|
|
# Sets LINKCTL_BIN on success and RESOLVE_ERR on failure. Both are globals
|
|
# rather than stdout, because a command substitution would run this in a
|
|
# subshell and discard the error message.
|
|
resolve_linkctl() {
|
|
local c
|
|
LINKCTL_BIN=
|
|
RESOLVE_ERR=
|
|
if [[ -n ${LINKCTL:-} ]]; then
|
|
# An explicit setting is authoritative, so a bad one is an error here
|
|
# rather than a confusing exec failure later.
|
|
if [[ -x $LINKCTL ]]; then
|
|
LINKCTL_BIN=$LINKCTL
|
|
return 0
|
|
fi
|
|
RESOLVE_ERR="LINKCTL is set to '$LINKCTL', which is not executable."
|
|
return 1
|
|
fi
|
|
if c=$(command -v linkctl 2>/dev/null); then
|
|
LINKCTL_BIN=$c
|
|
return 0
|
|
fi
|
|
for c in /usr/local/bin/linkctl /usr/bin/linkctl \
|
|
"$HOME/.local/bin/linkctl" "$HOME/.cargo/bin/linkctl" \
|
|
/var/lib/flatpak/exports/bin/linkctl; do
|
|
[[ -x $c ]] && { LINKCTL_BIN=$c; return 0; }
|
|
done
|
|
RESOLVE_ERR="linkctl not found on PATH or in the usual install locations."
|
|
return 1
|
|
}
|
|
|
|
# Map linkctl's documented exit codes onto something a person can act on.
|
|
hint_for() {
|
|
case $1 in
|
|
2) echo "Invalid arguments." ;;
|
|
3) echo "Camera not found. Is it plugged in?" ;;
|
|
4) echo "Several cameras found. Set LINKCTL_DEVICE in $CONF." ;;
|
|
5) echo "Camera is idle and --force was disabled." ;;
|
|
6) echo "Permission denied. Add yourself to the 'video' group or install the udev rule." ;;
|
|
7) echo "This control is not supported on this camera model." ;;
|
|
8) echo "Device I/O error. Try replugging the camera." ;;
|
|
9) echo "linkctl configuration error." ;;
|
|
11) echo "Invalid value for this control." ;;
|
|
12) echo "Vendor control failed. AI tracking is experimental." ;;
|
|
13) echo "Camera is busy." ;;
|
|
*) echo "linkctl exited with status $1." ;;
|
|
esac
|
|
}
|
|
|
|
notify() {
|
|
local urgency=$1 body=$2
|
|
[[ $LINKCTL_NOTIFY == never ]] && return 0
|
|
[[ $LINKCTL_NOTIFY == error && $urgency != critical ]] && return 0
|
|
command -v notify-send >/dev/null 2>&1 || return 0
|
|
# The synchronous hint makes repeated keypresses replace the previous
|
|
# popup instead of stacking a queue of them.
|
|
notify-send \
|
|
--app-name="Insta360 Link" \
|
|
--icon=camera-web \
|
|
--urgency="$urgency" \
|
|
--expire-time=2500 \
|
|
--hint=string:x-canonical-private-synchronous:linkctl-macro \
|
|
"Insta360 Link" "$body"
|
|
}
|
|
|
|
main() {
|
|
local no_notify=0 print_only=0
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--no-notify) no_notify=1; shift ;;
|
|
--print) print_only=1; shift ;;
|
|
--) shift; break ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
|
|
if [[ $# -eq 0 ]]; then
|
|
echo "usage: linkctl-macro [--no-notify] [--print] <linkctl args...>" >&2
|
|
echo " eg: linkctl-macro tracking toggle" >&2
|
|
return 2
|
|
fi
|
|
|
|
(( no_notify )) && LINKCTL_NOTIFY=never
|
|
|
|
if ! resolve_linkctl; then
|
|
local msg="${RESOLVE_ERR:-linkctl not found.} Set LINKCTL in $CONF."
|
|
notify critical "$msg"
|
|
echo "linkctl-macro: $msg" >&2
|
|
return 127
|
|
fi
|
|
|
|
local -a cmd=("$LINKCTL_BIN")
|
|
[[ $LINKCTL_FORCE == 1 ]] && cmd+=(--force)
|
|
[[ -n $LINKCTL_DEVICE ]] && cmd+=(--device "$LINKCTL_DEVICE")
|
|
cmd+=("$@")
|
|
|
|
if (( print_only )); then
|
|
printf '%q ' "${cmd[@]}"; echo
|
|
return 0
|
|
fi
|
|
|
|
local out status
|
|
out=$("${cmd[@]}" 2>&1)
|
|
status=$?
|
|
|
|
if (( status == 0 )); then
|
|
# linkctl already prints the resulting state, eg "Tracking: On", which
|
|
# is exactly the feedback worth surfacing. Fall back to the request.
|
|
local body=${out%%$'\n'*}
|
|
[[ -z $body ]] && body="$*"
|
|
notify normal "$body"
|
|
[[ -n $out ]] && printf '%s\n' "$out"
|
|
else
|
|
local body=${out%%$'\n'*}
|
|
[[ -n $body ]] && body="$body"$'\n'"$(hint_for "$status")" \
|
|
|| body=$(hint_for "$status")
|
|
notify critical "$body"
|
|
[[ -n $out ]] && printf '%s\n' "$out" >&2
|
|
printf 'linkctl-macro: %s\n' "$(hint_for "$status")" >&2
|
|
fi
|
|
return $status
|
|
}
|
|
|
|
main "$@"
|