A lit icon tops the rail while another program records from a microphone, uses a camera, or shares the screen. It is read from PipeWire: capture streams for the mic, links out of video sources for the camera and screencasts. The tooltip names the recording apps.
59 lines
2.3 KiB
QML
59 lines
2.3 KiB
QML
pragma Singleton
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Services.Pipewire
|
|
|
|
// Whether anything is listening or watching: an app recording from a
|
|
// microphone, a camera in use, or the screen being shared. All three show up
|
|
// in PipeWire, so this needs no polling — a capture stream exists only while
|
|
// something records, and a video source has links only while something
|
|
// consumes it.
|
|
Singleton {
|
|
id: root
|
|
|
|
// Capture streams from other programs. The shell's own level meters are
|
|
// capture streams too, and do not count.
|
|
readonly property var micUsers: Pipewire.nodes.values.filter(n =>
|
|
n.properties?.["media.class"] === "Stream/Input/Audio"
|
|
&& !root._isShell(n))
|
|
|
|
readonly property var videoLinks: Pipewire.links.values.filter(l =>
|
|
String(l.source?.properties?.["media.class"] ?? "").startsWith("Video/"))
|
|
|
|
// xdg-desktop-portal-hyprland names its screencast nodes "xdph-…".
|
|
readonly property bool screen: root.videoLinks.some(l => root._isScreencast(l.source))
|
|
readonly property bool camera: root.videoLinks.some(l => !root._isScreencast(l.source))
|
|
readonly property bool mic: root.micUsers.length > 0
|
|
readonly property bool active: root.mic || root.camera || root.screen
|
|
|
|
// Who is doing it, for the tooltip.
|
|
readonly property string detail: {
|
|
const parts = [];
|
|
if (root.screen) parts.push("Screen shared");
|
|
if (root.camera) parts.push("Camera on");
|
|
if (root.mic) {
|
|
const apps = [...new Set(root.micUsers.map(n =>
|
|
n.properties?.["application.name"] || n.name || "an app"))];
|
|
parts.push("Mic: " + apps.join(", "));
|
|
}
|
|
return parts.join(" · ");
|
|
}
|
|
|
|
function _isShell(n): bool {
|
|
const who = (String(n.properties?.["application.process.binary"] ?? "") + " "
|
|
+ String(n.properties?.["application.name"] ?? "")).toLowerCase();
|
|
return who.includes("quickshell") || /(^|\s)qs(\s|$)/.test(who);
|
|
}
|
|
|
|
function _isScreencast(n): bool {
|
|
const name = String(n?.name ?? n?.properties?.["node.name"] ?? "");
|
|
return name.startsWith("xdph");
|
|
}
|
|
|
|
// Node properties fill in only once a node is bound.
|
|
PwObjectTracker {
|
|
objects: Pipewire.nodes.values.filter(n => n.isStream || !n.isSink)
|
|
}
|
|
}
|