aboutsummaryrefslogtreecommitdiffstats
path: root/bin/privacy_dots.sh
diff options
context:
space:
mode:
Diffstat (limited to 'bin/privacy_dots.sh')
-rwxr-xr-xbin/privacy_dots.sh199
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}'