Krolyxon
Commit 8be0991

Branches

Refs →

aurora.configquickshellauroraPrivacy.qml

91 lines · 4.0 KB · QML

1pragma Singleton
2import QtQuick
3import Quickshell
4import Quickshell.Io
5import Quickshell.Hyprland
6import Quickshell.Services.Pipewire
7
8// Privacy indicators for the bar: who records the mic, uses the camera or shares the screen.
9// mic Pipewire input streams linked to an audio source (not a sink monitor, so cava/visualizers don't count)
10// camera processes holding /dev/video* open (mpv, browsers without the camera portal) + Pipewire
11// video streams fed by a v4l2/libcamera node
12// screen Hyprland's `screencast` event (xdg-desktop-portal-hyprland) + the apps consuming its video node.
13// wf-recorder (Rec) uses screencopy instead and has its own bar pill.
14Singleton {
15 id: root
16 readonly property bool enabled: Settings.privacyIndicators
17 readonly property var ignore: ["quickshell", "qs", "cava"]
18
19 function appName(n) {
20 var p = n.properties
21 return p["application.name"] || p["application.process.binary"] || n.name
22 }
23 // apps whose `streamCls` streams are linked to a node of class `srcCls`*, filtered by keep(source)
24 function captures(srcCls, streamCls, keep) {
25 var out = [], l = Pipewire.linkGroups.values
26 for (var i = 0; i < l.length; i++) {
27 var s = l[i].source, t = l[i].target
28 if (!s || !t) continue
29 if ((s.properties["media.class"] || "").indexOf(srcCls) !== 0) continue
30 if ((t.properties["media.class"] || "") !== streamCls) continue
31 if (ignore.indexOf(t.properties["application.process.binary"] || "") >= 0) continue
32 if (keep && !keep(s)) continue
33 var a = appName(t)
34 if (out.indexOf(a) < 0) out.push(a)
35 }
36 return out
37 }
38 function isCam(n) {
39 var api = n.properties["device.api"] || ""
40 return api === "v4l2" || api === "libcamera"
41 }
42
43 readonly property var micApps: enabled ? captures("Audio/Source", "Stream/Input/Audio", null) : []
44 readonly property var pwCamApps: enabled ? captures("Video/Source", "Stream/Input/Video", isCam) : []
45 readonly property var shareApps: enabled ? captures("Video/Source", "Stream/Input/Video", n => !isCam(n)) : []
46 property var devCamApps: [] // from the /proc scan
47 property bool casting: false // Hyprland screencast event
48
49 readonly property var camApps: {
50 var out = pwCamApps.slice()
51 for (var i = 0; i < devCamApps.length; i++) if (out.indexOf(devCamApps[i]) < 0) out.push(devCamApps[i])
52 return out
53 }
54 readonly property bool mic: micApps.length > 0
55 readonly property bool cam: camApps.length > 0
56 readonly property bool screen: enabled && (casting || shareApps.length > 0)
57
58 Connections {
59 target: Hyprland
60 function onRawEvent(ev) {
61 if (ev.name === "screencast") root.casting = ev.data.split(",")[0] === "1"
62 }
63 }
64
65 // /dev/video* holders. Only our own processes are readable, which is every app that matters here.
66 // pipewire/wireplumber holding it means a portal client: that one shows up in pwCamApps instead.
67 Timer {
68 interval: 3000
69 running: root.enabled
70 repeat: true
71 triggeredOnStart: true
72 onTriggered: if (!camProc.running) camProc.running = true
73 }
74 Process {
75 id: camProc
76 command: ["sh", "-c", "find /proc/[0-9]*/fd -maxdepth 1 -lname '/dev/video*' 2>/dev/null"
77 + " | cut -d/ -f3 | sort -u | while read -r p; do cat \"/proc/$p/comm\" 2>/dev/null; done"]
78 stdout: StdioCollector {
79 onStreamFinished: {
80 var out = [], l = text.split("\n")
81 for (var i = 0; i < l.length; i++) {
82 var c = l[i].trim()
83 if (c === "" || c === "pipewire" || c === "wireplumber" || out.indexOf(c) >= 0) continue
84 out.push(c)
85 }
86 if (out.join() !== root.devCamApps.join()) root.devCamApps = out
87 }
88 }
89 }
90 onEnabledChanged: if (!enabled) { devCamApps = []; casting = false }
91}