feat: keep Clipbin recording in the tray
Add native Linux SNI and Windows notification-area trays, single-instance reopening, background startup and login entries. Preserve recording and discovery after close, with clean Quit and reachable no-tray fallbacks. Track capture-source audio delivery independently of silent output tracks and warn without stopping video. Give clip metadata its own row and shape labels at final bounds to avoid GPUI 0.2.2 truncation cache and text-run bugs. Validated with strict workspace checks, 57 tests, Windows core/OBS/probe cross-clippy, all ten capture scenarios, tray/session/clips/playback rigs and narrow/wide screenshots. Document runtime validation gaps.
This commit is contained in:
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
# Serial tray/window/autostart validation in the private SNI-host rig.
|
||||
set -euo pipefail
|
||||
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
out="${1:-${TMPDIR:-/tmp}/clipbin-background-validation}"
|
||||
mkdir -p "${out}"
|
||||
cargo build --locked -p clipbin-desktop -p rig-pointer
|
||||
cargo build --locked -p clipbin-core --example sni-host
|
||||
export CLIPBIN_BACKGROUND_REPO="${repo}" CLIPBIN_BACKGROUND_OUT="${out}"
|
||||
"${repo}/scripts/rig/headless.sh" bash <<'INNER'
|
||||
set -euo pipefail
|
||||
repo="${CLIPBIN_BACKGROUND_REPO}" out="${CLIPBIN_BACKGROUND_OUT}"
|
||||
export XDG_CONFIG_HOME="${out}/config" XDG_DATA_HOME="${out}/data"
|
||||
mkdir -p "${XDG_CONFIG_HOME}" "${XDG_DATA_HOME}"
|
||||
source "${repo}/scripts/rig/keys.sh"
|
||||
"${repo}/target/debug/examples/sni-host" > "${out}/host.log" 2>&1 & host=$!
|
||||
app= game=
|
||||
trap 'kill "${app:-}" "${game:-}" "${host:-}" 2>/dev/null || true' EXIT
|
||||
sleep 1
|
||||
swaymsg 'for_window [app_id="dev.clipbin.Clipbin"] floating enable' >/dev/null
|
||||
swaymsg 'for_window [app_id="dev.clipbin.Clipbin"] resize set width 1360 px height 860 px' >/dev/null
|
||||
swaymsg 'for_window [app_id="dev.clipbin.Clipbin"] move position 280 110' >/dev/null
|
||||
windows() {
|
||||
swaymsg -t get_tree | python3 -c 'import json,sys
|
||||
nodes=[json.load(sys.stdin)]; total=0
|
||||
while nodes:
|
||||
n=nodes.pop(); nodes.extend(n.get("nodes",[])+n.get("floating_nodes",[])); total+=n.get("app_id")=="dev.clipbin.Clipbin"
|
||||
print(total)'
|
||||
}
|
||||
wait_windows() {
|
||||
for _ in $(seq 50); do [[ "$(windows)" == "$1" ]] && return 0; sleep .2; done
|
||||
echo "FAIL expected $1 Clipbin windows" >&2; exit 1
|
||||
}
|
||||
wait_exit() {
|
||||
for _ in $(seq 100); do if ! kill -0 "${app}" 2>/dev/null; then wait "${app}"; return; fi; sleep .2; done
|
||||
echo 'FAIL Quit did not exit' >&2; exit 1
|
||||
}
|
||||
CLIPBIN_ROUTE=settings "${repo}/target/debug/clipbin-desktop" --background > "${out}/desktop.log" 2>&1 & app=$!
|
||||
sleep 3
|
||||
kill -0 "${app}"
|
||||
[[ "$(windows)" == 0 ]]
|
||||
[[ -f "${XDG_CONFIG_HOME}/autostart/clipbin.desktop" ]]
|
||||
grep -q -- '--background' "${XDG_CONFIG_HOME}/autostart/clipbin.desktop"
|
||||
item="$(sed -n 's/^registered=//p' "${out}/host.log" | tail -1)"
|
||||
[[ -n "${item}" ]]
|
||||
gdbus call --session --dest org.kde.StatusNotifierWatcher --object-path /StatusNotifierWatcher \
|
||||
--method org.freedesktop.DBus.Properties.Get org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems > "${out}/registered.txt"
|
||||
grep -q "${item}" "${out}/registered.txt"
|
||||
gdbus call --session --dest "${item}" --object-path /MenuBar \
|
||||
--method com.canonical.dbusmenu.GetLayout -- 0 -1 '[]' > "${out}/menu.txt"
|
||||
grep -q 'Open Clipbin' "${out}/menu.txt"
|
||||
menu() { gdbus call --session --dest "${item}" --object-path /MenuBar --method com.canonical.dbusmenu.Event "$1" clicked '<0>' 0 > /dev/null; }
|
||||
menu 1
|
||||
wait_windows 1
|
||||
swaymsg '[title="clipbin-test-game"] fullscreen disable' >/dev/null
|
||||
swaymsg '[app_id="dev.clipbin.Clipbin"] floating enable, resize set width 1360 px height 860 px, move position center, focus' >/dev/null
|
||||
sleep 1
|
||||
grim "${out}/settings.png"
|
||||
# Coordinates within the centred 1360x860 settings window at 1920x1080.
|
||||
printf 'move 1330 550\nsleep .2\nclick\nsleep .3\n' | "${repo}/target/debug/rig-pointer" 1920 1080
|
||||
sleep 1
|
||||
[[ ! -f "${XDG_CONFIG_HOME}/autostart/clipbin.desktop" ]]
|
||||
printf 'move 1330 550\nsleep .2\nclick\nsleep .3\n' | "${repo}/target/debug/rig-pointer" 1920 1080
|
||||
sleep 1
|
||||
[[ -f "${XDG_CONFIG_HOME}/autostart/clipbin.desktop" ]]
|
||||
echo 'PASS SNI registration, menu Open, background startup and autostart setting'
|
||||
bash -c 'exec -a reaper python3 -c "import time; time.sleep(120)" SteamLaunch AppId=480 --' & game=$!
|
||||
for _ in $(seq 40); do grep -q 'recorder_status=Recording' "${out}/desktop.log" && break; sleep 1; done
|
||||
grep -q 'recorder_status=Recording' "${out}/desktop.log"
|
||||
gdbus call --session --dest "${item}" --object-path /StatusNotifierItem --method org.freedesktop.DBus.Properties.Get org.kde.StatusNotifierItem ToolTip > "${out}/recording-tooltip.txt"
|
||||
grep -q Recording "${out}/recording-tooltip.txt"
|
||||
swaymsg '[app_id="dev.clipbin.Clipbin"] kill' >/dev/null
|
||||
wait_windows 0
|
||||
before="$(find "${XDG_DATA_HOME}/clipbin/sessions" -name '*.mkv' | wc -l)"
|
||||
sleep 5
|
||||
after="$(find "${XDG_DATA_HOME}/clipbin/sessions" -name '*.mkv' | wc -l)"
|
||||
(( after > before ))
|
||||
"${repo}/target/debug/clipbin-desktop" > "${out}/second-launch.log" 2>&1
|
||||
wait_windows 1
|
||||
swaymsg '[app_id="dev.clipbin.Clipbin"] kill' >/dev/null
|
||||
wait_windows 0
|
||||
menu 1
|
||||
wait_windows 1
|
||||
# Audio delivery loss must be visible in the application and real tray tooltip.
|
||||
kill "$(cat "${CLIPBIN_RIG_DIR}/pulse.pid")"
|
||||
for _ in $(seq 15); do grep -q 'recorder_status=Degraded' "${out}/desktop.log" && break; sleep 1; done
|
||||
grep -q 'recorder_status=Degraded' "${out}/desktop.log"
|
||||
sleep .3
|
||||
gdbus call --session --dest "${item}" --object-path /StatusNotifierItem --method org.freedesktop.DBus.Properties.Get org.kde.StatusNotifierItem ToolTip > "${out}/audio-lost-tooltip.txt"
|
||||
grep -q 'audio capture lost' "${out}/audio-lost-tooltip.txt"
|
||||
grim "${out}/audio-lost.png"
|
||||
echo 'PASS audio loss is degraded in the UI and tray while video continues'
|
||||
# Quit through D-Bus menu while a game is still alive.
|
||||
menu 4
|
||||
wait_exit
|
||||
python3 - "${XDG_DATA_HOME}" <<'PY'
|
||||
import pathlib,json,sys
|
||||
files=list(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'))
|
||||
assert files and all(not json.loads(p.read_text())['session']['active'] for p in files)
|
||||
assert all(s['complete'] for p in files for s in json.loads(p.read_text())['segments'])
|
||||
PY
|
||||
kill "${game}"; wait "${game}" 2>/dev/null || true; game=
|
||||
echo 'PASS close keeps segments growing, second launch reopens, tray Open reopens, tray Quit finalises'
|
||||
# Losing the watcher must reveal the UI; closing then quits cleanly.
|
||||
"${repo}/target/debug/clipbin-desktop" --background > "${out}/host-loss.log" 2>&1 & app=$!
|
||||
sleep 2
|
||||
wait_windows 0
|
||||
kill "${host}"; wait "${host}" 2>/dev/null || true; host=
|
||||
wait_windows 1
|
||||
swaymsg '[app_id="dev.clipbin.Clipbin"] kill' >/dev/null
|
||||
wait_exit
|
||||
# Reconcile an externally disabled saved setting, removing the existing entry.
|
||||
python3 - "${XDG_CONFIG_HOME}/clipbin/settings.json" <<'PYSETTINGS'
|
||||
import json,pathlib,sys
|
||||
p=pathlib.Path(sys.argv[1]); settings=json.loads(p.read_text()); settings['start_on_login']=False; p.write_text(json.dumps(settings))
|
||||
PYSETTINGS
|
||||
# With no watcher at startup --background must open a reachable window.
|
||||
"${repo}/target/debug/clipbin-desktop" --background > "${out}/no-tray.log" 2>&1 & app=$!
|
||||
wait_windows 1
|
||||
grep -q 'Tray unavailable' "${out}/no-tray.log"
|
||||
[[ ! -f "${XDG_CONFIG_HOME}/autostart/clipbin.desktop" ]]
|
||||
swaymsg '[app_id="dev.clipbin.Clipbin"] kill' >/dev/null
|
||||
wait_exit
|
||||
echo 'PASS watcher loss reveals UI; no-tray background falls back to close=Quit'
|
||||
INNER
|
||||
@@ -115,6 +115,19 @@ else
|
||||
result "${name}" 1
|
||||
fi
|
||||
|
||||
# 9. Capture audio disappears while video/output remain healthy.
|
||||
name=audio-source-lost
|
||||
"${rig}" bash -c "'${probe}' record --seconds 15 --out '${out}/${name}' & p=\$!; sleep 6; kill \$(cat \"\${CLIPBIN_RIG_DIR}/pulse.pid\"); wait \$p" \
|
||||
> "${out}/${name}.log" 2>&1
|
||||
probe_exit=$?
|
||||
if [[ "${probe_exit}" == 0 ]] && has "${out}/${name}.log" "status=Recording" \
|
||||
&& has "${out}/${name}.log" "audio capture lost" \
|
||||
&& ! has "${out}/${name}.log" "status=failed"; then
|
||||
result "${name}" 0 "video continued; audio loss surfaced as degraded"
|
||||
else
|
||||
result "${name}" 1
|
||||
fi
|
||||
|
||||
echo
|
||||
if [[ "${failures}" == 0 ]]; then
|
||||
echo "All scenarios passed. Logs in ${out}"
|
||||
|
||||
@@ -20,6 +20,7 @@ done
|
||||
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
out="${1:-${TMPDIR:-/tmp}/clipbin-session-validation}"
|
||||
mkdir -p "${out}"
|
||||
cargo build --locked -p clipbin-core --example sni-host
|
||||
cargo build --locked -p clipbin-desktop --manifest-path "${repo}/Cargo.toml"
|
||||
export CLIPBIN_SESSION_REPO="${repo}" CLIPBIN_SESSION_OUT="${out}"
|
||||
"${repo}/scripts/rig/headless.sh" bash <<'INNER'
|
||||
@@ -46,13 +47,17 @@ wait_log() {
|
||||
echo "FAIL timed out waiting for ${minimum} '${pattern}' entries; log: ${log}" >&2
|
||||
exit 1
|
||||
}
|
||||
"${CLIPBIN_SESSION_REPO}/target/debug/examples/sni-host" > "${CLIPBIN_SESSION_OUT}/tray-host.log" 2>&1 &
|
||||
host=$!
|
||||
sleep 1
|
||||
source "${CLIPBIN_SESSION_REPO}/scripts/rig/keys.sh"
|
||||
out="${CLIPBIN_SESSION_OUT}"
|
||||
portal_config="${XDG_CONFIG_HOME}"
|
||||
export XDG_DATA_HOME="${out}/data" XDG_CONFIG_HOME="${out}/config"
|
||||
mkdir -p "${XDG_DATA_HOME}" "${XDG_CONFIG_HOME}"
|
||||
"${CLIPBIN_SESSION_REPO}/target/debug/clipbin-desktop" > "${out}/desktop.log" 2>&1 &
|
||||
app=$!
|
||||
trap 'kill "${app}" "${game:-}" "${portal:-}" 2>/dev/null || true' EXIT
|
||||
trap 'kill "${app}" "${game:-}" "${portal:-}" "${host}" 2>/dev/null || true' EXIT
|
||||
sleep 3
|
||||
swaymsg '[title="clipbin-test-game"] fullscreen disable' > /dev/null || true
|
||||
swaymsg '[app_id="dev.clipbin.Clipbin"] floating enable, resize set 1360 860, move position center, focus' > /dev/null
|
||||
@@ -147,25 +152,20 @@ import pathlib,json,sys
|
||||
files=list(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'))
|
||||
assert files and all(not json.loads(p.read_text())['session']['active'] for p in files)
|
||||
PY
|
||||
for _ in $(seq 10); do
|
||||
if ! kill -0 "${app}" 2>/dev/null; then break; fi
|
||||
sleep 1
|
||||
done
|
||||
if kill -0 "${app}" 2>/dev/null; then echo 'FAIL closed app kept discovering after game exit' >&2; exit 1; fi
|
||||
wait "${app}"
|
||||
kill -0 "${app}"
|
||||
kill "$(cat "${CLIPBIN_RIG_DIR}/game.pid")"
|
||||
# Reopen to prove the library loads from disk, then Quit cleanly with Ctrl+Q.
|
||||
"${CLIPBIN_SESSION_REPO}/target/debug/clipbin-desktop" > "${out}/reopened.log" 2>&1 &
|
||||
app=$!
|
||||
# A second launch signals the original instance and exits; background discovery survives.
|
||||
"${CLIPBIN_SESSION_REPO}/target/debug/clipbin-desktop" > "${out}/second-launch.log" 2>&1
|
||||
kill -0 "${app}"
|
||||
sleep 4
|
||||
swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null
|
||||
sleep 0.5
|
||||
grim "${out}/library.png"
|
||||
bash -c 'exec -a reaper python3 -c "import time; time.sleep(120)" SteamLaunch AppId=480 --' &
|
||||
game=$!
|
||||
wait_log 'recorder_status=Recording' "${out}/reopened.log" 1 30
|
||||
wait_log 'recorder_status=Recording' "${out}/desktop.log" 2 30
|
||||
swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null
|
||||
wtype -M ctrl -k q -m ctrl
|
||||
rig_key q ctrl
|
||||
for _ in $(seq 20); do
|
||||
if ! kill -0 "${app}" 2>/dev/null; then break; fi
|
||||
sleep 1
|
||||
|
||||
Reference in New Issue
Block a user