diff options
Diffstat (limited to 'bin/privacy_dots.sh')
| -rwxr-xr-x | bin/privacy_dots.sh | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/bin/privacy_dots.sh b/bin/privacy_dots.sh new file mode 100755 index 0000000..2074631 --- /dev/null +++ b/bin/privacy_dots.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env bash +# A dot per active capture: microphone, camera, location, screen sharing. +# Copyright (C) 2026 Danilo M. <danix@danix.xyz> +# Licensed under the GNU General Public License v2 only. +# +# dependencies: pipewire (pw-dump), jq, psmisc (fuser) +# +# Shows nothing when everything is off, which is the point: the pill only +# appears when something is actually capturing. +# +# The colours come from the palette, not from here: install.sh writes +# ~/.config/waybar-udt/dots.colors from the selected udt scheme, and this +# sources it. The defaults below are only what a missing file falls back to. +set -euo pipefail + +JQ_BIN="${JQ:-jq}" +PW_DUMP_CMD="${PW_DUMP:-pw-dump}" + +# Palette. Sourced, with fallbacks, so this script works standalone. +DOT_SUCCESS="#a6da95" +DOT_WARNING="#eed49f" +DOT_CRITICAL="#ed8796" +DOT_INFO="#8bd5ca" +DOT_ACCENT="#b7bdf8" +DOT_FAINT="#6e738d" +colors="$HOME/.config/waybar-udt/dots.colors" +# shellcheck source=/dev/null +[[ -r "$colors" ]] && . "$colors" + +mic=0 +cam=0 +loc=0 +scr=0 + +mic_app="" +cam_app="" +loc_app="" +scr_app="" + +# mic & camera +if command -v "$PW_DUMP_CMD" >/dev/null 2>&1 && command -v "$JQ_BIN" >/dev/null 2>&1; then + dump="$($PW_DUMP_CMD 2>/dev/null || true)" + + mic="$( + printf '%s' "$dump" \ + | $JQ_BIN -r ' + [ .[] + | select(.type=="PipeWire:Interface:Node") + | select((.info.props."media.class"=="Audio/Source" or .info.props."media.class"=="Audio/Source/Virtual")) + | select((.info.state=="running") or (.state=="running")) + ] | (if length>0 then 1 else 0 end) + ' 2>/dev/null || echo 0 + )" + + if [[ "$mic" -eq 1 ]]; then + mic_app="$( + printf '%s' "$dump" \ + | $JQ_BIN -r ' + [ .[] + | select(.type=="PipeWire:Interface:Node") + | select((.info.props."media.class"=="Stream/Input/Audio")) + | select((.info.state=="running") or (.state=="running")) + | .info.props["node.name"] + ] | unique | join(", ") + ' 2>/dev/null || echo "" + )" + fi + + if command -v fuser >/dev/null 2>&1; then + cam=0 + for dev in /dev/video*; do + if [ -e "$dev" ] && fuser "$dev" >/dev/null 2>&1; then + cam=1 + break + fi + done + else + cam=0 + fi + + if command -v fuser >/dev/null 2>&1; then + for dev in /dev/video*; do + if [ -e "$dev" ] && fuser "$dev" >/dev/null 2>&1; then + pids=$(fuser "$dev" 2>/dev/null) + for pid in $pids; do + pname=$(ps -p "$pid" -o comm=) + if [[ -n "$pname" ]]; then + cam_app+="$pname, " + fi + done + fi + done + cam_app="${cam_app%, }" + fi + +fi + +# location +if command -v gdbus >/dev/null 2>&1; then + if pids=$(pgrep -x geoclue); then + loc=1 + for pid in $pids; do + pname=$(ps -p "$pid" -o comm=) + [[ -n "$pname" ]] && loc_app+="$pname, " + done + loc_app="${loc_app%, }" + else + loc=0 + fi +fi + +# screen sharing +if command -v "$PW_DUMP_CMD" >/dev/null 2>&1 && command -v "$JQ_BIN" >/dev/null 2>&1; then + if [[ -z "${dump:-}" ]]; then + dump="$($PW_DUMP_CMD 2>/dev/null || true)" + fi + + scr="$( + printf '%s' "$dump" \ + | $JQ_BIN -e ' + [ .[] + | select(.info?.props?) + | select( + (.info.props["media.name"]? // "") + | test("^(xdph-streaming|gsr-default)") + ) + ] + | (if length > 0 then true else false end) + ' >/dev/null && echo 1 || echo 0 + )" +fi + +if [[ "$scr" -eq 1 ]]; then + scr_app="$( + printf '%s' "$dump" \ + | $JQ_BIN -r ' + [ .[] + | select(.type=="PipeWire:Interface:Node") + | select((.info.props."media.class"=="Stream/Input/Video") or (.info.props."media.name"=="gsr-default_output")) + | select((.info.state=="running") or (.state=="running")) + | .info.props["media.name"] + ] | unique | join(", ") + ' 2>/dev/null || echo "" + )" +fi + +# output. Capture is a state worth noticing, so these are the alert roles: +# the microphone is the quiet one, the camera and the screen are not. +dot() { + local on="$1" color="$2" + if [[ "$on" -eq 1 ]]; then + printf '<span foreground="%s">●</span>' "$color" + else + printf '' + fi +} + +dots=() +mic_dot="$(dot "$mic" "$DOT_SUCCESS")"; [[ -n "$mic_dot" ]] && dots+=("$mic_dot") +cam_dot="$(dot "$cam" "$DOT_WARNING")"; [[ -n "$cam_dot" ]] && dots+=("$cam_dot") +loc_dot="$(dot "$loc" "$DOT_INFO")"; [[ -n "$loc_dot" ]] && dots+=("$loc_dot") +scr_dot="$(dot "$scr" "$DOT_CRITICAL")"; [[ -n "$scr_dot" ]] && dots+=("$scr_dot") + +text="${dots[*]}" + +if [[ -n "$mic_app" ]]; then + mic_status="Mic: $mic_app" +else + mic_status="Mic: off" +fi + +if [[ -n "$cam_app" ]]; then + cam_status="Cam: $cam_app" +else + cam_status="Cam: off" +fi + +if [[ -n "$loc_app" ]]; then + loc_status="Location: $loc_app" +else + loc_status="Location: off" +fi + +if [[ -n "$scr_app" ]]; then + scr_status="Screen sharing: $scr_app" +else + scr_status="Screen sharing: off" +fi + +tooltip="$mic_status | $cam_status | $loc_status | $scr_status" + +classes="privacydot" +[[ $mic -eq 1 ]] && classes="$classes mic-on" || classes="$classes mic-off" +[[ $cam -eq 1 ]] && classes="$classes cam-on" || classes="$classes cam-off" +[[ $loc -eq 1 ]] && classes="$classes loc-on" || classes="$classes loc-off" +[[ $scr -eq 1 ]] && classes="$classes scr-on" || classes="$classes scr-off" + +$JQ_BIN -c -n --arg text "$text" --arg tooltip "$tooltip" --arg class "$classes" \ + '{text:$text, tooltip:$tooltip, class:$class}' |
