From 55ac16640207093a48d20fd6312a450bf9e26bf6 Mon Sep 17 00:00:00 2001 From: Dmitri Date: Wed, 15 Apr 2026 16:30:27 +0200 Subject: [PATCH] noctalia --- environment.d/10-path.conf | 1 + foot/foot.ini | 3 + foot/themes/noctalia | 22 ++ link.sh | 2 + noctalia/plugins.json | 4 + .../plugins/privacy-indicator/BarWidget.qml | 175 ++++++++++ noctalia/plugins/privacy-indicator/Main.qml | 316 ++++++++++++++++++ noctalia/plugins/privacy-indicator/Panel.qml | 170 ++++++++++ noctalia/plugins/privacy-indicator/README.md | 47 +++ .../plugins/privacy-indicator/Settings.qml | 141 ++++++++ .../plugins/privacy-indicator/i18n/de.json | 54 +++ .../plugins/privacy-indicator/i18n/en.json | 58 ++++ .../plugins/privacy-indicator/i18n/es.json | 46 +++ .../plugins/privacy-indicator/i18n/fr.json | 54 +++ .../plugins/privacy-indicator/i18n/hu.json | 46 +++ .../plugins/privacy-indicator/i18n/it.json | 46 +++ .../plugins/privacy-indicator/i18n/ja.json | 46 +++ .../plugins/privacy-indicator/i18n/ku.json | 46 +++ .../plugins/privacy-indicator/i18n/nl.json | 46 +++ .../plugins/privacy-indicator/i18n/pl.json | 46 +++ .../plugins/privacy-indicator/i18n/pt.json | 46 +++ .../plugins/privacy-indicator/i18n/ru.json | 46 +++ .../plugins/privacy-indicator/i18n/tr.json | 46 +++ .../plugins/privacy-indicator/i18n/uk-UA.json | 46 +++ .../plugins/privacy-indicator/i18n/vi.json | 54 +++ .../plugins/privacy-indicator/i18n/zh-CN.json | 46 +++ .../plugins/privacy-indicator/i18n/zh-TW.json | 46 +++ .../plugins/privacy-indicator/manifest.json | 37 ++ .../plugins/privacy-indicator/preview.png | Bin 0 -> 26449 bytes .../plugins/privacy-indicator/settings.json | 9 + noctalia/settings.json | 73 +++- sway/config | 52 +-- sway/noctalia | 18 + sway/scripts/lid.sh | 25 ++ zathura/noctaliarc | 31 ++ 35 files changed, 1887 insertions(+), 57 deletions(-) create mode 100644 environment.d/10-path.conf create mode 100644 foot/themes/noctalia create mode 100644 noctalia/plugins/privacy-indicator/BarWidget.qml create mode 100644 noctalia/plugins/privacy-indicator/Main.qml create mode 100644 noctalia/plugins/privacy-indicator/Panel.qml create mode 100644 noctalia/plugins/privacy-indicator/README.md create mode 100644 noctalia/plugins/privacy-indicator/Settings.qml create mode 100644 noctalia/plugins/privacy-indicator/i18n/de.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/en.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/es.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/fr.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/hu.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/it.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/ja.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/ku.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/nl.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/pl.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/pt.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/ru.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/tr.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/uk-UA.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/vi.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/zh-CN.json create mode 100644 noctalia/plugins/privacy-indicator/i18n/zh-TW.json create mode 100644 noctalia/plugins/privacy-indicator/manifest.json create mode 100644 noctalia/plugins/privacy-indicator/preview.png create mode 100644 noctalia/plugins/privacy-indicator/settings.json create mode 100644 sway/noctalia create mode 100755 sway/scripts/lid.sh create mode 100644 zathura/noctaliarc diff --git a/environment.d/10-path.conf b/environment.d/10-path.conf new file mode 100644 index 0000000..c186183 --- /dev/null +++ b/environment.d/10-path.conf @@ -0,0 +1 @@ +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin diff --git a/foot/foot.ini b/foot/foot.ini index ee26c9e..33c216c 100644 --- a/foot/foot.ini +++ b/foot/foot.ini @@ -1,3 +1,6 @@ +[main] +include=~/.config/foot/themes/noctalia + # -*- conf -*- # shell=$SHELL (if set, otherwise user's default shell from /etc/passwd) diff --git a/foot/themes/noctalia b/foot/themes/noctalia new file mode 100644 index 0000000..8c4ad91 --- /dev/null +++ b/foot/themes/noctalia @@ -0,0 +1,22 @@ +[colors-dark] +foreground=ebdbb2 +background=282828 +regular0=282828 +regular1=cc241d +regular2=98971a +regular3=d79921 +regular4=458588 +regular5=b16286 +regular6=689d6a +regular7=a89984 +bright0=928374 +bright1=fb4934 +bright2=b8bb26 +bright3=fabd2f +bright4=83a598 +bright5=d3869b +bright6=8ec07c +bright7=ebdbb2 +selection-foreground=ebdbb2 +selection-background=665c54 +cursor=282828 ebdbb2 diff --git a/link.sh b/link.sh index ca3ac03..7c7abd2 100755 --- a/link.sh +++ b/link.sh @@ -16,3 +16,5 @@ ln -s ~/Documents/dotfiles/zathura ~/.config/ ln -s ~/Documents/dotfiles/opencode ~/.config/ ln -s ~/Documents/dotfiles/.zshrc ~/ ln -s ~/Documents/dotfiles/.gitconfig ~/ +mkdir -p ~/.config/environment.d +ln -s ~/Documents/dotfiles/environment.d/10-path.conf ~/.config/environment.d/10-path.conf diff --git a/noctalia/plugins.json b/noctalia/plugins.json index 04b0a22..146b536 100644 --- a/noctalia/plugins.json +++ b/noctalia/plugins.json @@ -7,6 +7,10 @@ } ], "states": { + "privacy-indicator": { + "enabled": true, + "sourceUrl": "https://github.com/noctalia-dev/noctalia-plugins" + } }, "version": 2 } diff --git a/noctalia/plugins/privacy-indicator/BarWidget.qml b/noctalia/plugins/privacy-indicator/BarWidget.qml new file mode 100644 index 0000000..bdebd41 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/BarWidget.qml @@ -0,0 +1,175 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property var pluginApi: null + + property ShellScreen screen + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + // Bar positioning properties + readonly property string screenName: screen ? screen.name : "" + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isVertical: barPosition === "left" || barPosition === "right" + readonly property real barHeight: Style.getBarHeightForScreen(screenName) + readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName) + readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName) + + // Access main instance for state + readonly property var mainInstance: pluginApi?.mainInstance + + property bool micActive: mainInstance ? mainInstance.micActive : false + property bool camActive: mainInstance ? mainInstance.camActive : false + property bool scrActive: mainInstance ? mainInstance.scrActive : false + property var micApps: mainInstance ? mainInstance.micApps : [] + property var camApps: mainInstance ? mainInstance.camApps : [] + property var scrApps: mainInstance ? mainInstance.scrApps : [] + + property var cfg: pluginApi?.pluginSettings || ({}) + property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) + + property bool hideInactive: cfg.hideInactive ?? defaults.hideInactive ?? false + property bool enableToast: cfg.enableToast ?? defaults.enableToast ?? true + property bool removeMargins: cfg.removeMargins ?? defaults.removeMargins ?? false + property int iconSpacing: cfg.iconSpacing ?? defaults.iconSpacing ?? 4 + property string activeColorKey: cfg.activeColor ?? defaults.activeColor ?? "primary" + property string inactiveColorKey: cfg.inactiveColor ?? defaults.inactiveColor ?? "none" + + readonly property color activeColor: Color.resolveColorKey(activeColorKey) + readonly property color inactiveColor: inactiveColorKey === "none" ? Qt.alpha(Color.mOnSurfaceVariant, 0.3) : Color.resolveColorKey(inactiveColorKey) + readonly property color micColor: micActive ? activeColor : inactiveColor + readonly property color camColor: camActive ? activeColor : inactiveColor + readonly property color scrColor: scrActive ? activeColor : inactiveColor + + readonly property bool isVisible: !hideInactive || micActive || camActive || scrActive + + property real margins: removeMargins ? 0 : Style.marginM * 2 + + readonly property real contentWidth: isVertical ? Style.capsuleHeight : Math.round(layout.implicitWidth + margins) + readonly property real contentHeight: isVertical ? Math.round(layout.implicitHeight + margins) : Style.capsuleHeight + + implicitWidth: contentWidth + implicitHeight: contentHeight + + Layout.alignment: Qt.AlignVCenter + visible: root.isVisible + opacity: root.isVisible ? 1.0 : 0.0 + + function buildTooltip() { + var parts = []; + + if (micActive && micApps.length > 0) { + parts.push("Mic: " + micApps.join(", ")); + } + + if (camActive && camApps.length > 0) { + parts.push("Cam: " + camApps.join(", ")); + } + + if (scrActive && scrApps.length > 0) { + parts.push("Screen sharing: " + scrApps.join(", ")); + } + + return parts.length > 0 ? parts.join("\n") : ""; + } + + Rectangle { + id: visualCapsule + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, height) + width: root.contentWidth + height: root.contentHeight + radius: Style.radiusM + color: Style.capsuleColor + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + Item { + id: layout + + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + + implicitWidth: iconsLayout.implicitWidth + implicitHeight: iconsLayout.implicitHeight + + GridLayout { + id: iconsLayout + + columns: root.isVertical ? 1 : 3 + rows: root.isVertical ? 3 : 1 + + rowSpacing: root.iconSpacing + columnSpacing: root.iconSpacing + + NIcon { + visible: micActive || !root.hideInactive + icon: micActive ? "microphone" : "microphone-off" + color: root.micColor + } + NIcon { + visible: camActive || !root.hideInactive + icon: camActive ? "camera" : "camera-off" + color: root.camColor + } + NIcon { + visible: scrActive || !root.hideInactive + icon: scrActive ? "screen-share" : "screen-share-off" + color: root.scrColor + } + } + } + } + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": pluginApi?.tr("menu.settings"), + "action": "settings", + "icon": "settings" + }, + ] + + onTriggered: function (action) { + contextMenu.close(); + PanelService.closeContextMenu(screen); + if (action === "settings") { + BarService.openPluginSettings(root.screen, pluginApi.manifest); + } + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton | Qt.LeftButton + hoverEnabled: true + + onClicked: function (mouse) { + if (mouse.button === Qt.RightButton) { + PanelService.showContextMenu(contextMenu, root, screen); + } else if (mouse.button === Qt.LeftButton) { + if (pluginApi) pluginApi.openPanel(root.screen, root); + } + } + + onEntered: { + var tooltipText = buildTooltip(); + if (tooltipText) { + TooltipService.show(root, tooltipText, BarService.getTooltipDirection()); + } + } + onExited: TooltipService.hide() + } +} diff --git a/noctalia/plugins/privacy-indicator/Main.qml b/noctalia/plugins/privacy-indicator/Main.qml new file mode 100644 index 0000000..0c64564 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/Main.qml @@ -0,0 +1,316 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pipewire +import qs.Commons +import qs.Services.UI + +Item { + id: root + property var pluginApi: null + + // --- Logic extracted from BarWidget.qml --- + + property bool micActive: false + property bool camActive: false + property bool scrActive: false + property var micApps: [] + property var camApps: [] + property var scrApps: [] + + property var accessHistory: [] + + // Previous states for history tracking + property var _prevMicApps: [] + property var _prevCamApps: [] + property var _prevScrApps: [] + + // Get active color from settings or default + property var cfg: pluginApi?.pluginSettings || ({}) + property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) + property bool enableToast: cfg.enableToast ?? defaults.enableToast ?? true + property string activeColorKey: cfg.activeColor ?? defaults.activeColor ?? "primary" + property string micFilterRegex: cfg.micFilterRegex ?? defaults.micFilterRegex ?? "" + property string camFilterRegex: cfg.camFilterRegex ?? defaults.camFilterRegex ?? "" + + PwObjectTracker { + objects: Pipewire.ready ? Pipewire.nodes.values : [] + } + + Process { + id: cameraDetectionProcess + running: false + command: ["sh", "-c", "for dev in /sys/class/video4linux/video*; do [ -e \"$dev/name\" ] && grep -qv 'Metadata' \"$dev/name\" && dev_name=$(basename \"$dev\") && find /proc/[0-9]*/fd -lname \"/dev/$dev_name\" 2>/dev/null; done | cut -d/ -f3 | xargs -r ps -o comm= -p | sort -u | tr '\\n' ',' | sed 's/,$//'"] + stdout: StdioCollector { + onStreamFinished: { + var appsString = this.text.trim(); + var apps = appsString.length > 0 ? appsString.split(',') : []; + + var filterRegex = null; + if (root.camFilterRegex && root.camFilterRegex.length > 0) { + try { + filterRegex = new RegExp(root.camFilterRegex); + } catch (e) { + Logger.w("PrivacyIndicator: Invalid camFilterRegex:", root.camFilterRegex); + } + } + + var appNames = []; + for (var i = 0; i < apps.length; i++) { + appName = apps[i]; + if (filterRegex && appName && filterRegex.test(appName)) continue; + if (appName && appNames.indexOf(appName) === -1) appNames.push(appName); + } + + root.camApps = appNames; + root.camActive = appNames.length > 0; + } + } + } + + + Timer { + interval: 1000 + repeat: true + running: true + triggeredOnStart: true + onTriggered: updatePrivacyState() + } + + function hasNodeLinks(node, links) { + for (var i = 0; i < links.length; i++) { + var link = links[i]; + if (link && (link.source === node || link.target === node)) return true; + } + return false; + } + + function getAppName(node) { + return node.properties["application.name"] || node.nickname || node.name || ""; + } + + function updateMicrophoneState(nodes, links) { + var appNames = []; + var isActive = false; + + var filterRegex = null; + if (root.micFilterRegex && root.micFilterRegex.length > 0) { + try { + filterRegex = new RegExp(root.micFilterRegex); + } catch (e) { + Logger.w("PrivacyIndicator: Invalid micFilterRegex:", root.micFilterRegex); + } + } + + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + if (!node || !node.isStream || !node.audio || node.isSink) continue; + if (!hasNodeLinks(node, links) || !node.properties) continue; + var mediaClass = node.properties["media.class"] || ""; + if (mediaClass === "Stream/Input/Audio") { + if (node.properties["stream.capture.sink"] === "true") continue; + + var appName = getAppName(node); + if (filterRegex && appName && filterRegex.test(appName)) continue; + + isActive = true; + if (appName && appNames.indexOf(appName) === -1) appNames.push(appName); + } + } + root.micActive = isActive; + root.micApps = appNames; + } + + function updateCameraState() { + cameraDetectionProcess.running = true; + } + + function isScreenShareNode(node) { + if (!node.properties) return false; + var mediaClass = node.properties["media.class"] || ""; + if (mediaClass.indexOf("Audio") >= 0) return false; + if (mediaClass.indexOf("Video") === -1) return false; + var mediaName = (node.properties["media.name"] || "").toLowerCase(); + if (mediaName.match(/^(xdph-streaming|gsr-default|game capture|screen|desktop|display|cast|webrtc|v4l2)/) || + mediaName === "gsr-default_output" || + mediaName.match(/screen-cast|screen-capture|desktop-capture|monitor-capture|window-capture|game-capture/i)) { + return true; + } + return false; + } + + function updateScreenShareState(nodes, links) { + var appNames = []; + var isActive = false; + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + if (!node || !hasNodeLinks(node, links) || !node.properties) continue; + if (isScreenShareNode(node)) { + isActive = true; + var appName = getAppName(node); + if (appName && appNames.indexOf(appName) === -1) appNames.push(appName); + } + } + root.scrActive = isActive; + root.scrApps = appNames; + } + + function updatePrivacyState() { + if (!Pipewire.ready) return; + var nodes = Pipewire.nodes.values || []; + var links = Pipewire.links.values || []; + updateMicrophoneState(nodes, links); + updateCameraState(); + updateScreenShareState(nodes, links); + } + + // --- History Persistence --- + + property string stateFile: "" + property bool isLoaded: false + + Component.onCompleted: { + // Setup state file path + Qt.callLater(() => { + if (typeof Settings !== 'undefined' && Settings.cacheDir) { + stateFile = Settings.cacheDir + "privacy-history.json"; + historyFileView.path = stateFile; + } + }); + } + + FileView { + id: historyFileView + printErrors: false + watchChanges: false + + adapter: JsonAdapter { + id: adapter + property var history: [] + } + + onLoaded: { + root.isLoaded = true; + if (adapter.history) { + // Restore history + root.accessHistory = adapter.history; + } + } + + onLoadFailed: error => { + // If file doesn't exist (error 2), we are ready to save new data + if (error === 2) { + root.isLoaded = true; + } else { + console.error("PrivacyIndicator: Failed to load history file:", error); + root.isLoaded = true; // Try to continue anyway + } + } + } + + function saveHistory() { + if (!stateFile || !isLoaded) return; + + adapter.history = root.accessHistory; + + // Ensure cache directory exists and save + try { + Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]); + Qt.callLater(() => { + try { + historyFileView.writeAdapter(); + } catch (e) { + console.error("PrivacyIndicator: Failed to save history", e); + } + }); + } catch (e) { + console.error("PrivacyIndicator: Failed to save history", e); + } + } + + function addToHistory(app, type, icon, colorKey, action) { + var time = new Date().toLocaleTimeString(Qt.locale(), Locale.ShortFormat); + var entry = { + "appName": app, + "type": type, + "icon": icon, + "colorKey": colorKey, + "time": time, + "timestamp": Date.now(), + "action": action // "started" or "stopped" + }; + var newHistory = [entry].concat(accessHistory); + if (newHistory.length > 50) newHistory = newHistory.slice(0, 50); // Increased limit as we have more entries now + accessHistory = newHistory; + saveHistory(); + } + + function clearHistory() { + accessHistory = []; + saveHistory(); + } + + function checkAppChanges(newApps, oldApps, type, icon, colorKey) { + if (!newApps && !oldApps) return; + + // Check for new apps (Started) + if (newApps) { + for (var i = 0; i < newApps.length; i++) { + var app = newApps[i]; + if (!oldApps || oldApps.indexOf(app) === -1) { + addToHistory(app, type, icon, colorKey, "started"); + } + } + } + + // Check for removed apps (Stopped) + if (oldApps) { + for (var j = 0; j < oldApps.length; j++) { + var oldApp = oldApps[j]; + if (!newApps || newApps.indexOf(oldApp) === -1) { + addToHistory(oldApp, type, icon, colorKey, "stopped"); + } + } + } + } + + + onMicAppsChanged: { + checkAppChanges(micApps, _prevMicApps, "Microphone", "microphone", activeColorKey); + _prevMicApps = micApps; + } + // Helper to detect activation edge + property bool oldMicActive: false + onMicActiveChanged: { + if (enableToast && micActive && !oldMicActive) { + ToastService.showNotice(pluginApi?.tr("toast.mic-on"), "", "microphone"); + } + oldMicActive = micActive + } + + property bool oldCamActive: false + onCamActiveChanged: { + if (enableToast && camActive && !oldCamActive) { + ToastService.showNotice(pluginApi?.tr("toast.cam-on"), "", "camera"); + } + oldCamActive = camActive + } + onCamAppsChanged: { + checkAppChanges(camApps, _prevCamApps, "Camera", "camera", activeColorKey); + _prevCamApps = camApps; + } + + property bool oldScrActive: false + onScrActiveChanged: { + if (enableToast && scrActive && !oldScrActive) { + ToastService.showNotice(pluginApi?.tr("toast.screen-on"), "", "screen-share"); + } + oldScrActive = scrActive + } + onScrAppsChanged: { + checkAppChanges(scrApps, _prevScrApps, "Screen", "screen-share", activeColorKey); + _prevScrApps = scrApps; + } + + +} diff --git a/noctalia/plugins/privacy-indicator/Panel.qml b/noctalia/plugins/privacy-indicator/Panel.qml new file mode 100644 index 0000000..0cfcc22 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/Panel.qml @@ -0,0 +1,170 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Widgets + +Item { + id: root + + property var pluginApi: null + + // Standard panel properties + readonly property var geometryPlaceholder: panelContainer + property real contentPreferredWidth: 320 * Style.uiScaleRatio + property real contentPreferredHeight: 450 * Style.uiScaleRatio + + readonly property var mainInstance: pluginApi?.mainInstance + readonly property bool allowAttach: true + + Rectangle { + id: panelContainer + anchors.fill: parent + color: "transparent" + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + // Header Box + NBox { + Layout.fillWidth: true + Layout.preferredHeight: headerRow.implicitHeight + Style.marginM * 2 + + RowLayout { + id: headerRow + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + NIcon { + icon: "shield-check" + color: Color.mPrimary + pointSize: Style.fontSizeL + } + + NText { + Layout.fillWidth: true + text: pluginApi?.tr("history.title") + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeL + color: Color.mOnSurface + } + + NIconButton { + icon: "trash" + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + if (mainInstance) mainInstance.clearHistory(); + } + } + } + } + + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + NScrollView { + id: scrollView + anchors.fill: parent + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + + ColumnLayout { + width: scrollView.availableWidth + spacing: Style.marginS + + Repeater { + model: mainInstance ? mainInstance.accessHistory : [] + + delegate: Rectangle { + Layout.fillWidth: true + implicitHeight: 56 * Style.uiScaleRatio + radius: Style.radiusM + color: Color.mSurfaceVariant + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + Rectangle { + width: 32 * Style.uiScaleRatio + height: 32 * Style.uiScaleRatio + radius: width/2 + color: Qt.alpha(iconColor, 0.1) + + readonly property color iconColor: Color.resolveColorKey(modelData.colorKey || "primary") + + NIcon { + anchors.centerIn: parent + icon: modelData.icon + color: parent.iconColor + pointSize: Style.fontSizeM + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + NText { + Layout.fillWidth: true + text: modelData.appName + elide: Text.ElideRight + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeM + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NText { + text: modelData.time + color: Qt.alpha(Color.mOnSurface, 0.7) + pointSize: Style.fontSizeS + } + + NText { + text: "•" + color: Qt.alpha(Color.mOnSurface, 0.3) + pointSize: Style.fontSizeS + } + + NText { + text: { + const action = modelData.action || "started"; + return pluginApi?.tr("history.action." + action) || action; + } + color: (modelData.action || "started") === "stopped" ? Color.resolveColorKey("error") : Color.resolveColorKey("primary") + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeS + } + } + } + } + } + } + + // Empty state + NText { + Layout.alignment: Qt.AlignHCenter + visible: (!mainInstance || mainInstance.accessHistory.length === 0) + text: pluginApi?.tr("history.empty") + color: Qt.alpha(Color.mOnSurface, 0.5) + pointSize: Style.fontSizeM + Layout.topMargin: Style.marginL + } + + Item { Layout.fillHeight: true } // spacer + } + } + } + } + } +} diff --git a/noctalia/plugins/privacy-indicator/README.md b/noctalia/plugins/privacy-indicator/README.md new file mode 100644 index 0000000..d192040 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/README.md @@ -0,0 +1,47 @@ +# Privacy Indicator Plugin + +A privacy indicator widget that monitors and displays when microphone, camera, or screen sharing is active on your system. + +## Features + +- **Microphone Monitoring**: Detects active microphone usage via Pipewire +- **Camera Monitoring**: Detects active camera usage by checking `/dev/video*` devices +- **Screen Sharing Detection**: Monitors screen sharing sessions via Pipewire +- **Visual Indicators**: Shows icons that change color based on active state + - Active: Primary color + - Inactive: Semi-transparent variant color +- **App Information**: Tooltip displays which applications are using each resource +- **Adaptive Layout**: Automatically adjusts layout for horizontal or vertical bar positions + +## Configuration + +Access the plugin settings in Noctalia to configure the following options: + +- **Hide Inactive States**: If enabled, microphone, camera, and screen icons are hidden whenever they are inactive. Only active states are shown. +- **Remove Margins**: If enabled, removes all outer margins of the widget. +- **Icon Spacing**: Controls the horizontal/vertical spacing between the icons. +- **Active/Inactive Icon Color**: Customize the colors for active and inactive states. +- **Microphone Filter Regex**: Regex pattern to filter out specific microphone applications. Matching apps are completely excluded from detection (they won't trigger the indicator or appear in tooltips). Use `|` to specify multiple patterns, e.g., `effect_input.rnnoise|easyeffects`. + + +## Usage + +The widget displays three icons in the bar: +- **Microphone**: Shows when any app is using the microphone +- **Camera**: Shows when any app is accessing the camera +- **Screen Share**: Shows when screen sharing is active + +Hover over the widget to see a tooltip listing which applications are using each resource. + +## Requirements + +- Noctalia Shell 3.6.0 or higher +- Pipewire (for microphone and screen sharing detection) +- Access to `/dev/video*` devices (for camera detection) + +## Technical Details + +- Updates privacy state every second +- Uses Pipewire API to monitor audio/video streams +- Checks `/proc/[0-9]*/fd/` for camera device access +- Detects screen sharing by analyzing Pipewire node properties and media class diff --git a/noctalia/plugins/privacy-indicator/Settings.qml b/noctalia/plugins/privacy-indicator/Settings.qml new file mode 100644 index 0000000..84d77e7 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/Settings.qml @@ -0,0 +1,141 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + + property var pluginApi: null + + property var cfg: pluginApi?.pluginSettings || ({}) + property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) + + property bool hideInactive: cfg.hideInactive ?? defaults.hideInactive ?? false + property bool enableToast: cfg.enableToast ?? defaults.enableToast ?? true + property bool removeMargins: cfg.removeMargins ?? defaults.removeMargins ?? false + property int iconSpacing: cfg.iconSpacing ?? defaults.iconSpacing ?? 4 + property string activeColor: cfg.activeColor ?? defaults.activeColor ?? "primary" + property string inactiveColor: cfg.inactiveColor ?? defaults.inactiveColor ?? "none" + property string micFilterRegex: cfg.micFilterRegex ?? defaults.micFilterRegex + property string camFilterRegex: cfg.camFilterRegex ?? defaults.camFilterRegex + + spacing: Style.marginL + + Component.onCompleted: { + Logger.i("PrivacyIndicator", "Settings UI loaded"); + } + + ColumnLayout { + spacing: Style.marginM + Layout.fillWidth: true + + NToggle { + label: pluginApi?.tr("settings.hideInactive.label") + description: pluginApi?.tr("settings.hideInactive.desc") + + checked: root.hideInactive + onToggled: checked => { + root.hideInactive = checked; + } + } + + NToggle { + label: pluginApi?.tr("settings.enableToast.label") + description: pluginApi?.tr("settings.enableToast.desc") + + checked: root.enableToast + onToggled: checked => { + root.enableToast = checked; + } + } + + NToggle { + label: pluginApi?.tr("settings.removeMargins.label") + description: pluginApi?.tr("settings.removeMargins.desc") + + checked: root.removeMargins + onToggled: checked => { + root.removeMargins = checked; + } + } + + NColorChoice { + label: pluginApi?.tr("settings.activeColor.label") + description: pluginApi?.tr("settings.activeColor.desc") + currentKey: root.activeColor + onSelected: key => root.activeColor = key + } + + NColorChoice { + label: pluginApi?.tr("settings.inactiveColor.label") + description: pluginApi?.tr("settings.inactiveColor.desc") + currentKey: root.inactiveColor + onSelected: key => root.inactiveColor = key + noneColor: Qt.alpha(Color.mOnSurfaceVariant, 0.3) + noneOnColor: Qt.alpha(Color.mOnSurface, 0.7) + } + + NComboBox { + label: pluginApi?.tr("settings.iconSpacing.label") + description: pluginApi?.tr("settings.iconSpacing.desc") + + model: { + const labels = ["XXS", "XS", "S", "M", "L", "XL"]; + const values = [Style.marginXXS, Style.marginXS, Style.marginS, Style.marginM, Style.marginL, Style.marginXL]; + + const result = []; + for (var i = 0; i < labels.length; ++i) { + const v = values[i]; + result.push({ + key: v.toFixed(0), + name: `${labels[i]} (${v}px)` + }); + } + return result; + } + + // INFO: From my understanding, the toFixed(0) shouldn't be needed here and there, but without the + // current key does not show when opening the settings window. + currentKey: root.iconSpacing.toFixed(0) + onSelected: key => root.iconSpacing = key + } + + NTextInput { + Layout.fillWidth: true + label: pluginApi?.tr("settings.micFilterRegex.label") + description: pluginApi?.tr("settings.micFilterRegex.desc") + placeholderText: "effect_input.rnnoise|easyeffects" + text: root.micFilterRegex + onTextChanged: root.micFilterRegex = text + } + + NTextInput { + Layout.fillWidth: true + label: pluginApi?.tr("settings.camFilterRegex.label") + description: pluginApi?.tr("settings.camFilterRegex.desc") + placeholderText: "droidcam" + text: root.camFilterRegex + onTextChanged: root.camFilterRegex = text + } + } + + function saveSettings() { + if (!pluginApi) { + Logger.e("PrivacyIndicator", "Cannot save settings: pluginApi is null"); + return; + } + + pluginApi.pluginSettings.hideInactive = root.hideInactive; + pluginApi.pluginSettings.enableToast = root.enableToast; + pluginApi.pluginSettings.iconSpacing = root.iconSpacing; + pluginApi.pluginSettings.removeMargins = root.removeMargins; + pluginApi.pluginSettings.activeColor = root.activeColor; + pluginApi.pluginSettings.inactiveColor = root.inactiveColor; + pluginApi.pluginSettings.micFilterRegex = root.micFilterRegex; + + pluginApi.saveSettings(); + + Logger.i("PrivacyIndicator", "Settings saved successfully"); + } +} diff --git a/noctalia/plugins/privacy-indicator/i18n/de.json b/noctalia/plugins/privacy-indicator/i18n/de.json new file mode 100644 index 0000000..b25635e --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/de.json @@ -0,0 +1,54 @@ +{ + "menu": { + "settings": "Widget Einstellungen" + }, + "settings": { + "activeColor": { + "desc": "Farbe der Symbole, wenn sie aktiv sind.", + "label": "Aktive Farbsymbol" + }, + "hideInactive": { + "desc": "Mikrofon-, Kamera- und Bildschirmsymbole ausblenden, wenn sie inaktiv sind.", + "label": "Inaktive Zustände ausblenden" + }, + "enableToast": { + "desc": "Zeige Toast Mitteilungen an, wenn sich einer der Zustände ändert.", + "label": "Aktiviere Toast Mitteilungen" + }, + "inactiveColor": { + "desc": "Farbe der Symbole, wenn sie inaktiv sind.", + "label": "Inaktive Farbsymbol" + }, + "iconSpacing": { + "desc": "Den Abstand zwischen den Symbolen festlegen.", + "label": "Symbolabstand" + }, + "removeMargins": { + "desc": "Alle äußeren Ränder des Widgets entfernen.", + "label": "Ränder entfernen" + }, + "micFilterRegex": { + "desc": "Regex Muster zum Herausfiltern von Mikrofon-Apps. Entsprechende Apps werden vollständig von der Erkennung ausgeschlossen.", + "label": "Regex für Mikrofonfilter" + } + }, + "tooltip": { + "cam-on": "Kamera: {apps}", + "mic-on": "Mikrofon: {apps}", + "screen-on": "Bildschirmfreigabe: {apps}" + }, + "toast": { + "cam-on": "Kamera ist aktiv", + "mic-on": "Mikrofon ist aktiv", + "screen-on": "Bildschirmfreigabe ist aktiv" + }, + "history": { + "title": "Zugriffsverlauf", + "empty": "Kein kürzlicher Zugriff", + "clear": "Leeren", + "action": { + "started": "Gestartet", + "stopped": "Beendet" + } + } +} \ No newline at end of file diff --git a/noctalia/plugins/privacy-indicator/i18n/en.json b/noctalia/plugins/privacy-indicator/i18n/en.json new file mode 100644 index 0000000..fa65418 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/en.json @@ -0,0 +1,58 @@ +{ + "menu": { + "settings": "Widget settings" + }, + "settings": { + "activeColor": { + "desc": "Color of the icons when active.", + "label": "Active icon color" + }, + "hideInactive": { + "desc": "Hide microphone, camera, and screen icons when there are inactive.", + "label": "Hide inactive states" + }, + "enableToast": { + "desc": "Show toast messages when one of the states changes.", + "label": "Enable toast notifications" + }, + "inactiveColor": { + "desc": "Color of the icons when inactive.", + "label": "Inactive icon color" + }, + "iconSpacing": { + "desc": "Set the spacing between the icons.", + "label": "Icon spacing" + }, + "removeMargins": { + "desc": "Remove all outer margins of the widget.", + "label": "Remove margins" + }, + "micFilterRegex": { + "desc": "Regex pattern to filter out microphone applications. Matching apps are completely excluded from detection.", + "label": "Microphone filter regex" + }, + "camFilterRegex": { + "desc": "Regex pattern to filter out camera applications. Matching apps are completely excluded from detection.", + "label": "Camera filter regex" + } + }, + "tooltip": { + "cam-on": "Camera: {apps}", + "mic-on": "Microphone: {apps}", + "screen-on": "Screen sharing: {apps}" + }, + "toast": { + "cam-on": "Camera is active", + "mic-on": "Microphone is active", + "screen-on": "Screen sharing is active" + }, + "history": { + "title": "Access History", + "empty": "No recent access", + "clear": "Clear", + "action": { + "started": "Started", + "stopped": "Stopped" + } + } +} diff --git a/noctalia/plugins/privacy-indicator/i18n/es.json b/noctalia/plugins/privacy-indicator/i18n/es.json new file mode 100644 index 0000000..93a48a7 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/es.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Ocultar los iconos de micrófono, cámara y pantalla cuando estén inactivos.", + "label": "Ocultar estados inactivos" + }, + "iconSpacing": { + "desc": "Configurar el espacio entre los iconos.", + "label": "Espaciado de iconos" + }, + "removeMargins": { + "desc": "Quita todos los márgenes externos del widget.", + "label": "Quitar márgenes" + }, + "activeColor": { + "desc": "Color de los iconos cuando están activos.", + "label": "Color de icono activo" + }, + "inactiveColor": { + "desc": "Color de los iconos cuando están inactivos.", + "label": "Color de icono inactivo" + } + }, + "tooltip": { + "cam-on": "Cámara: {apps}", + "mic-on": "Micrófono: {apps}", + "screen-on": "Compartir pantalla: {apps}" + }, + "toast": { + "cam-on": "La cámara está activa", + "mic-on": "El micrófono está activo", + "screen-on": "Compartir pantalla está activo" + }, + "menu": { + "settings": "Configuración del widget" + }, + "history": { + "title": "Historial de acceso", + "empty": "Sin accesos recientes", + "clear": "Borrar", + "action": { + "started": "Iniciado", + "stopped": "Detenido" + } + } +} \ No newline at end of file diff --git a/noctalia/plugins/privacy-indicator/i18n/fr.json b/noctalia/plugins/privacy-indicator/i18n/fr.json new file mode 100644 index 0000000..9053131 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/fr.json @@ -0,0 +1,54 @@ +{ + "menu": { + "settings": "Paramètres du widget" + }, + "settings": { + "activeColor": { + "desc": "Couleur des icônes lorsqu'elles sont actives.", + "label": "Couleur d'icône active" + }, + "hideInactive": { + "desc": "Masquer les icônes du micro, de la caméra et de l’écran lorsqu’ils sont inactifs.", + "label": "Masquer les états inactifs" + }, + "enableToast": { + "desc": "Afficher des messages toast lorsque l'un des états change.", + "label": "Activer les notifications toast" + }, + "inactiveColor": { + "desc": "Couleur des icônes lorsqu'elles sont inactives.", + "label": "Couleur d'icône inactive" + }, + "iconSpacing": { + "desc": "Définir l’espacement entre les icônes.", + "label": "Espacement des icônes" + }, + "removeMargins": { + "desc": "Supprime toutes les marges extérieures du widget.", + "label": "Supprimer les marges" + }, + "micFilterRegex": { + "desc": "Motif Regex pour filtrer les applications de microphone. Les applications correspondantes sont complètement exclues de la détection.", + "label": "Regex de filtrage du microphone" + } + }, + "tooltip": { + "cam-on": "Caméra: {apps}", + "mic-on": "Microphone: {apps}", + "screen-on": "Partage d'écran: {apps}" + }, + "toast": { + "cam-on": "La caméra est active", + "mic-on": "Le microphone est actif", + "screen-on": "Le partage d'écran est actif" + }, + "history": { + "title": "Historique d'accès", + "empty": "Aucun accès récent", + "clear": "Effacer", + "action": { + "started": "Démarré", + "stopped": "Arrêté" + } + } +} \ No newline at end of file diff --git a/noctalia/plugins/privacy-indicator/i18n/hu.json b/noctalia/plugins/privacy-indicator/i18n/hu.json new file mode 100644 index 0000000..4a7ba8c --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/hu.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Mikrofon, kamera és képernyő ikonok elrejtése, ha éppen nincsenek használatban.", + "label": "Inaktív állapotok elrejtése" + }, + "iconSpacing": { + "desc": "Állítsa be az ikonok közötti távolságot.", + "label": "Ikon távolság" + }, + "removeMargins": { + "desc": "Távolítsd el a widget összes külső margóját.", + "label": "Margók eltávolítása" + }, + "activeColor": { + "desc": "Az ikonok színe, amikor aktívak.", + "label": "Aktív ikon színe" + }, + "inactiveColor": { + "desc": "Az ikonok színe, amikor inaktívak.", + "label": "Inaktív ikon színe" + } + }, + "tooltip": { + "cam-on": "Kamera: {apps}", + "mic-on": "Mikrofon: {apps}", + "screen-on": "Képernyőmegosztás: {apps}" + }, + "toast": { + "cam-on": "A kamera aktív", + "mic-on": "A mikrofon aktív", + "screen-on": "Képernyőmegosztás aktív" + }, + "menu": { + "settings": "Widget beállítások" + }, + "history": { + "title": "Hozzáférési előzmények", + "empty": "Nincs nemrégiben történt hozzáférés", + "clear": "Törlés", + "action": { + "started": "Elindítva", + "stopped": "Leállítva" + } + } +} \ No newline at end of file diff --git a/noctalia/plugins/privacy-indicator/i18n/it.json b/noctalia/plugins/privacy-indicator/i18n/it.json new file mode 100644 index 0000000..d0eee79 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/it.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Nascondi le icone di microfono, fotocamera e schermo quando sono inattive.", + "label": "Nascondi stati inattivi" + }, + "iconSpacing": { + "desc": "Imposta la distanza tra le icone.", + "label": "Spaziatura delle icone" + }, + "removeMargins": { + "desc": "Rimuove tutti i margini esterni del widget.", + "label": "Rimuovi margini" + }, + "activeColor": { + "desc": "Colore delle icone quando sono attive.", + "label": "Colore icona attiva" + }, + "inactiveColor": { + "desc": "Colore delle icone quando sono inattive.", + "label": "Colore icona inattiva" + } + }, + "tooltip": { + "cam-on": "Kamera: {apps}", + "mic-on": "Mikrofonoa: {apps}", + "screen-on": "Ekran-partaĝado: {apps}" + }, + "toast": { + "cam-on": "La fotocamera è attiva", + "mic-on": "Il microfono è attivo", + "screen-on": "La condivisione dello schermo è attiva" + }, + "menu": { + "settings": "Impostazioni widget" + }, + "history": { + "title": "Cronologia accessi", + "empty": "Nessun accesso recente", + "clear": "Pulisci", + "action": { + "started": "Iniziato", + "stopped": "Terminato" + } + } +} \ No newline at end of file diff --git a/noctalia/plugins/privacy-indicator/i18n/ja.json b/noctalia/plugins/privacy-indicator/i18n/ja.json new file mode 100644 index 0000000..ba61aeb --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/ja.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "マイク・カメラ・画面共有のアイコンを、非アクティブなときは非表示にします。", + "label": "非アクティブ状態を非表示" + }, + "iconSpacing": { + "desc": "アイコン同士の間隔を設定します。", + "label": "アイコン間隔" + }, + "removeMargins": { + "desc": "ウィジェットの外側の余白をすべて削除します。", + "label": "余白を削除" + }, + "activeColor": { + "desc": "アクティブ時のアイコンの色。", + "label": "アクティブ時の色" + }, + "inactiveColor": { + "desc": "非アクティブ時のアイコンの色。", + "label": "非アクティブ時の色" + } + }, + "tooltip": { + "cam-on": "カメラ: {apps}", + "mic-on": "マイク: {apps}", + "screen-on": "画面共有: {apps}" + }, + "toast": { + "cam-on": "カメラがアクティブです", + "mic-on": "マイクがアクティブです", + "screen-on": "画面共有がアクティブです" + }, + "menu": { + "settings": "ウィジェット設定" + }, + "history": { + "title": "アクセス履歴", + "empty": "最近のアクセスはありません", + "clear": "クリア", + "action": { + "started": "開始", + "stopped": "停止" + } + } +} \ No newline at end of file diff --git a/noctalia/plugins/privacy-indicator/i18n/ku.json b/noctalia/plugins/privacy-indicator/i18n/ku.json new file mode 100644 index 0000000..6a58f84 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/ku.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Îkonên mîkrofon, kamera û ekranê dema ku neçalak bin veşêre.", + "label": "Rewşa neçalak veşêre" + }, + "iconSpacing": { + "desc": "Cihê di navbera îkonan de diyar bike.", + "label": "Dûrahiya îkonan" + }, + "removeMargins": { + "desc": "Hemû marjînalên derveyî yên widgetê rake.", + "label": "Derdestên derxînin" + }, + "activeColor": { + "desc": "Color of the icons when active.", + "label": "Active icon color" + }, + "inactiveColor": { + "desc": "Color of the icons when inactive.", + "label": "Inactive icon color" + } + }, + "tooltip": { + "cam-on": "Kamera: {apps}", + "mic-on": "Mîkrofon: {apps}", + "screen-on": "Parvekirina ekranê: {apps}" + }, + "toast": { + "cam-on": "Kamera çalak e", + "mic-on": "Mîkrofon çalak e", + "screen-on": "Parvekirina ekranê çalak e" + }, + "menu": { + "settings": "Widget settings" + }, + "history": { + "title": "Access History", + "empty": "No recent access", + "clear": "Clear", + "action": { + "started": "Started", + "stopped": "Stopped" + } + } +} \ No newline at end of file diff --git a/noctalia/plugins/privacy-indicator/i18n/nl.json b/noctalia/plugins/privacy-indicator/i18n/nl.json new file mode 100644 index 0000000..d0b031d --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/nl.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Verberg de pictogrammen voor microfoon, camera en scherm wanneer ze inactief zijn.", + "label": "Inactieve status verbergen" + }, + "iconSpacing": { + "desc": "Stel de afstand tussen de pictogrammen in.", + "label": "Pictogramafstand" + }, + "removeMargins": { + "desc": "Verwijdert alle buitenste marges van de widget.", + "label": "Marges verwijderen" + }, + "activeColor": { + "desc": "Kleur van de pictogrammen wanneer ze actief zijn.", + "label": "Actieve pictogramkleur" + }, + "inactiveColor": { + "desc": "Kleur van de pictogrammen wanneer ze inactief zijn.", + "label": "Inactieve pictogramkleur" + } + }, + "tooltip": { + "cam-on": "Camera: {apps}", + "mic-on": "Microfoon: {apps}", + "screen-on": "Schermdeling: {apps}" + }, + "toast": { + "cam-on": "Camera is actief", + "mic-on": "Microfoon is actief", + "screen-on": "Scherm delen is actief" + }, + "menu": { + "settings": "Widget instellingen" + }, + "history": { + "title": "Toegangsgeschiedenis", + "empty": "Geen recente toegang", + "clear": "Wissen", + "action": { + "started": "Gestart", + "stopped": "Gestopt" + } + } +} \ No newline at end of file diff --git a/noctalia/plugins/privacy-indicator/i18n/pl.json b/noctalia/plugins/privacy-indicator/i18n/pl.json new file mode 100644 index 0000000..9e05d3a --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/pl.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Ukryj ikony mikrofonu, kamery i ekranu, gdy są nieaktywne.", + "label": "Ukryj nieaktywne stany" + }, + "iconSpacing": { + "desc": "Ustaw odstęp między ikonami.", + "label": "Odstępy ikon" + }, + "removeMargins": { + "desc": "Usuń wszystkie zewnętrzne marginesy widżetu.", + "label": "Usuń marginesy" + }, + "activeColor": { + "desc": "Kolor ikon, gdy są aktywne.", + "label": "Kolor aktywnej ikony" + }, + "inactiveColor": { + "desc": "Kolor ikon, gdy są nieaktywne.", + "label": "Kolor nieaktywnej ikony" + } + }, + "tooltip": { + "cam-on": "Kamera: {apps}", + "mic-on": "Mikrofon: {apps}", + "screen-on": "Udostępnianie ekranu: {apps}" + }, + "toast": { + "cam-on": "Kamera jest aktywna", + "mic-on": "Mikrofon jest aktywny", + "screen-on": "Udostępnianie ekranu jest aktywne" + }, + "menu": { + "settings": "Ustawienia widżetu" + }, + "history": { + "title": "Historia dostępu", + "empty": "Brak ostatnich dostępów", + "clear": "Wyczyść", + "action": { + "started": "Rozpoczęto", + "stopped": "Zatrzymano" + } + } +} \ No newline at end of file diff --git a/noctalia/plugins/privacy-indicator/i18n/pt.json b/noctalia/plugins/privacy-indicator/i18n/pt.json new file mode 100644 index 0000000..068a528 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/pt.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Oculta os ícones de microfone, câmera e tela quando estiverem inativos.", + "label": "Ocultar estados inativos" + }, + "iconSpacing": { + "desc": "Define o espaçamento entre os ícones.", + "label": "Espaçamento dos ícones" + }, + "removeMargins": { + "desc": "Remove todas as margens externas do widget.", + "label": "Remover margens" + }, + "activeColor": { + "desc": "Cor dos ícones quando ativos.", + "label": "Cor do ícone ativo" + }, + "inactiveColor": { + "desc": "Cor dos ícones quando inativos.", + "label": "Cor do ícone inativo" + } + }, + "tooltip": { + "cam-on": "Câmera: {apps}", + "mic-on": "Microfone: {apps}", + "screen-on": "Compartilhamento de tela: {apps}" + }, + "toast": { + "cam-on": "A câmera está ativa", + "mic-on": "O microfone está ativo", + "screen-on": "O compartilhamento de tela está ativo" + }, + "menu": { + "settings": "Configurações do widget" + }, + "history": { + "title": "Histórico de acesso", + "empty": "Sem acessos recentes", + "clear": "Limpar", + "action": { + "started": "Iniciado", + "stopped": "Interrompido" + } + } +} \ No newline at end of file diff --git a/noctalia/plugins/privacy-indicator/i18n/ru.json b/noctalia/plugins/privacy-indicator/i18n/ru.json new file mode 100644 index 0000000..152e7d8 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/ru.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Скрывать значки микрофона, камеры и экрана, когда они неактивны.", + "label": "Скрывать неактивные состояния" + }, + "iconSpacing": { + "desc": "Задать расстояние между значками.", + "label": "Интервал между значками" + }, + "removeMargins": { + "desc": "Удаляет все внешние отступы виджета.", + "label": "Убрать отступы" + }, + "activeColor": { + "desc": "Цвет иконок при активации.", + "label": "Цвет активной иконки" + }, + "inactiveColor": { + "desc": "Цвет иконок в спокойном состоянии.", + "label": "Цвет неактивной иконки" + } + }, + "tooltip": { + "cam-on": "Камера: {apps}", + "mic-on": "Микрофон: {apps}", + "screen-on": "Демонстрация экрана: {apps}" + }, + "toast": { + "cam-on": "Камера активна", + "mic-on": "Микрофон активен", + "screen-on": "Демонстрация экрана активна" + }, + "menu": { + "settings": "Настройки виджета" + }, + "history": { + "title": "История доступа", + "empty": "Нет недавних доступов", + "clear": "Очистить", + "action": { + "started": "Начато", + "stopped": "Остановлено" + } + } +} \ No newline at end of file diff --git a/noctalia/plugins/privacy-indicator/i18n/tr.json b/noctalia/plugins/privacy-indicator/i18n/tr.json new file mode 100644 index 0000000..016f1be --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/tr.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Mikrofon, kamera ve ekran simgelerini pasif olduklarında gizle.", + "label": "Pasif durumları gizle" + }, + "iconSpacing": { + "desc": "Simgeler arasındaki boşluğu ayarla.", + "label": "Simge aralığı" + }, + "removeMargins": { + "desc": "Widget’ın tüm dış kenar boşluklarını kaldırır.", + "label": "Kenarlıkları kaldır" + }, + "activeColor": { + "desc": "Aktif olduğunda simgelerin rengi.", + "label": "Aktif simge rengi" + }, + "inactiveColor": { + "desc": "Devre dışı olduğunda simgelerin rengi.", + "label": "Pasif simge rengi" + } + }, + "tooltip": { + "cam-on": "Kamera: {apps}", + "mic-on": "Mikrofon: {apps}", + "screen-on": "Ekran paylaşımı: {apps}" + }, + "toast": { + "cam-on": "Kamera aktif", + "mic-on": "Mikrofon aktif", + "screen-on": "Ekran paylaşımı aktif" + }, + "menu": { + "settings": "Bileşen ayarları" + }, + "history": { + "title": "Erişim Geçmişi", + "empty": "Yakın zamanda erişim yok", + "clear": "Temizle", + "action": { + "started": "Başlatıldı", + "stopped": "Durduruldu" + } + } +} \ No newline at end of file diff --git a/noctalia/plugins/privacy-indicator/i18n/uk-UA.json b/noctalia/plugins/privacy-indicator/i18n/uk-UA.json new file mode 100644 index 0000000..5b872a7 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/uk-UA.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "Приховувати значки мікрофона, камери та екрана, коли вони неактивні.", + "label": "Приховувати неактивні стани" + }, + "iconSpacing": { + "desc": "Встановити відстань між значками.", + "label": "Інтервал між значками" + }, + "removeMargins": { + "desc": "Видаляє всі зовнішні відступи віджета.", + "label": "Прибрати відступи" + }, + "activeColor": { + "desc": "Колір іконок при активації.", + "label": "Колір активної іконки" + }, + "inactiveColor": { + "desc": "Колір іконок у спокійному стані.", + "label": "Колір неактивної іконки" + } + }, + "tooltip": { + "cam-on": "Камера: {apps}", + "mic-on": "Мікрофон: {apps}", + "screen-on": "Демонстрація екрана: {apps}" + }, + "toast": { + "cam-on": "Камера активна", + "mic-on": "Мікрофон активний", + "screen-on": "Демонстрація екрана активна" + }, + "menu": { + "settings": "Налаштування віджета" + }, + "history": { + "title": "Історія доступу", + "empty": "Немає недавніх доступів", + "clear": "Очистити", + "action": { + "started": "Розпочато", + "stopped": "Зупинено" + } + } +} \ No newline at end of file diff --git a/noctalia/plugins/privacy-indicator/i18n/vi.json b/noctalia/plugins/privacy-indicator/i18n/vi.json new file mode 100644 index 0000000..6cd6d76 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/vi.json @@ -0,0 +1,54 @@ +{ + "menu": { + "settings": "Cài đặt tiện ích" + }, + "settings": { + "activeColor": { + "desc": "Màu của biểu tượng khi đang hoạt động.", + "label": "Màu biểu tượng khi hoạt động" + }, + "hideInactive": { + "desc": "Ẩn biểu tượng micro, camera và màn hình khi không hoạt động.", + "label": "Ẩn trạng thái không hoạt động" + }, + "enableToast": { + "desc": "Hiển thị thông báo khi một trạng thái thay đổi.", + "label": "Bật thông báo" + }, + "inactiveColor": { + "desc": "Màu của biểu tượng khi không hoạt động.", + "label": "Màu biểu tượng khi không hoạt động" + }, + "iconSpacing": { + "desc": "Thiết lập khoảng cách giữa các biểu tượng.", + "label": "Khoảng cách biểu tượng" + }, + "removeMargins": { + "desc": "Loại bỏ toàn bộ lề ngoài của widget.", + "label": "Xóa lề" + }, + "micFilterRegex": { + "desc": "Biểu thức chính quy để lọc các ứng dụng sử dụng micro. Các ứng dụng khớp sẽ bị loại khỏi việc phát hiện.", + "label": "Regex lọc micro" + } + }, + "tooltip": { + "cam-on": "Camera: {apps}", + "mic-on": "Micro: {apps}", + "screen-on": "Chia sẻ màn hình: {apps}" + }, + "toast": { + "cam-on": "Camera đang hoạt động", + "mic-on": "Micro đang hoạt động", + "screen-on": "Chia sẻ màn hình đang hoạt động" + }, + "history": { + "title": "Lịch sử truy cập", + "empty": "Không có truy cập gần đây", + "clear": "Xóa", + "action": { + "started": "Bắt đầu", + "stopped": "Dừng" + } + } +} diff --git a/noctalia/plugins/privacy-indicator/i18n/zh-CN.json b/noctalia/plugins/privacy-indicator/i18n/zh-CN.json new file mode 100644 index 0000000..5665d78 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/zh-CN.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "在麦克风、摄像头和屏幕图标处于非活动状态时将其隐藏。", + "label": "隐藏非活动状态" + }, + "iconSpacing": { + "desc": "设置图标之间的间距。", + "label": "图标间距" + }, + "removeMargins": { + "desc": "移除小部件所有外部边距。", + "label": "移除边距" + }, + "activeColor": { + "desc": "激活图标的颜色。", + "label": "激活图标颜色" + }, + "inactiveColor": { + "desc": "未激活图标的颜色。", + "label": "未激活图标颜色" + } + }, + "tooltip": { + "cam-on": "摄像头: {apps}", + "mic-on": "麦克风: {apps}", + "screen-on": "屏幕共享: {apps}" + }, + "toast": { + "cam-on": "摄像头已激活", + "mic-on": "麦克风已激活", + "screen-on": "屏幕共享已激活" + }, + "menu": { + "settings": "小部件设置" + }, + "history": { + "title": "访问历史", + "empty": "无最近访问", + "clear": "清除", + "action": { + "started": "已开始", + "stopped": "已停止" + } + } +} \ No newline at end of file diff --git a/noctalia/plugins/privacy-indicator/i18n/zh-TW.json b/noctalia/plugins/privacy-indicator/i18n/zh-TW.json new file mode 100644 index 0000000..febeadc --- /dev/null +++ b/noctalia/plugins/privacy-indicator/i18n/zh-TW.json @@ -0,0 +1,46 @@ +{ + "settings": { + "hideInactive": { + "desc": "當麥克風, 攝影機及螢幕分享沒有啟動時就直接隱藏", + "label": "隱藏未啟動的狀態" + }, + "iconSpacing": { + "desc": "設定圖示之間的留空", + "label": "圖示間距" + }, + "removeMargins": { + "desc": "移除小工具外面的所有邊距", + "label": "移除邊距" + }, + "activeColor": { + "desc": "圖示啟用時的顏色。", + "label": "啟用圖示顏色" + }, + "inactiveColor": { + "desc": "圖示未啟用時的顏色。", + "label": "未啟用圖示顏色" + } + }, + "tooltip": { + "cam-on": "攝影機: {apps}", + "mic-on": "麥克風: {apps}", + "screen-on": "螢幕分享: {apps}" + }, + "toast": { + "cam-on": "攝影機已啟用", + "mic-on": "麥克風已啟用", + "screen-on": "螢幕分享已啟用" + }, + "menu": { + "settings": "小工具設定" + }, + "history": { + "title": "存取紀錄", + "empty": "無最近存取", + "clear": "清除", + "action": { + "started": "已開始", + "stopped": "已停止" + } + } +} \ No newline at end of file diff --git a/noctalia/plugins/privacy-indicator/manifest.json b/noctalia/plugins/privacy-indicator/manifest.json new file mode 100644 index 0000000..e7b25b1 --- /dev/null +++ b/noctalia/plugins/privacy-indicator/manifest.json @@ -0,0 +1,37 @@ +{ + "id": "privacy-indicator", + "name": "Privacy Indicator", + "version": "1.2.9", + "minNoctaliaVersion": "3.6.0", + "author": "Noctalia Team", + "official": true, + "license": "MIT", + "repository": "https://github.com/noctalia-dev/noctalia-plugins", + "description": "A privacy indicator widget that shows when microphone, camera or screen sharing is active.", + "tags": [ + "Bar", + "Privacy", + "Indicator" + ], + "entryPoints": { + "main": "Main.qml", + "barWidget": "BarWidget.qml", + "panel": "Panel.qml", + "settings": "Settings.qml" + }, + "dependencies": { + "plugins": [] + }, + "metadata": { + "defaultSettings": { + "hideInactive": false, + "enableToast": true, + "removeMargins": false, + "iconSpacing": 4, + "activeColor": "primary", + "inactiveColor": "none", + "micFilterRegex": "", + "camFilterRegex": "" + } + } +} diff --git a/noctalia/plugins/privacy-indicator/preview.png b/noctalia/plugins/privacy-indicator/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..0db7b7c736fcbc63fd1abca3c6df14d60fd51e56 GIT binary patch literal 26449 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 Idle) input type:touchpad { - dwt enabled # Disable while typing - RIATTIVA QUESTA OPZIONE + dwt disabled # Disable while typing - RIATTIVA QUESTA OPZIONE tap enabled # Enable tap-to-click natural_scroll enabled # Natural scrolling middle_emulation enabled # Emulate middle mouse button - RIATTIVA QUESTA OPZIONE @@ -74,7 +47,7 @@ bindsym $mod+Return exec $term # Chiudi finestra focalizzata bindsym $mod+Shift+q kill -set $ipc qs -c noctalia-shell ipc call +set $ipc /usr/sbin/qs -c noctalia-shell ipc call # Avvia il launcher bindsym $mod+d exec $ipc launcher toggle @@ -166,9 +139,9 @@ bindsym --locked XF86MonBrightnessUp exec $ipc brightness increase bindsym --locked XF86MonBrightnessDown exec $ipc brightness decrease # Controlli multimediali -bindsym XF86AudioPlay exec ipc call media playPause -bindsym XF86AudioNext exec ipc call media next -bindsym XF86AudioPrev exec ipc call media previous +bindsym XF86AudioPlay exec $ipc media playPause +bindsym XF86AudioNext exec $ipc media next +bindsym XF86AudioPrev exec $ipc media previous bindsym $mod+Shift+h exec handy --toggle-transcription bindsym $mod+v exec $ipc launcher clipboard @@ -201,7 +174,7 @@ gaps outer 2 gaps inner 3 # Impostazioni dei colori per il focus delle finestre -client.focused $gruvbox_orange $gruvbox_orange $gruvbox_bg_dark $gruvbox_orange +# client.focused $gruvbox_orange $gruvbox_orange $gruvbox_bg_dark $gruvbox_orange hide_edge_borders smart ### Impostazioni GTK per tema scuro @@ -246,16 +219,13 @@ exec dbus-update-activation-environment WAYLAND_DISPLAY DISPLAY XDG_CURRENT_DESK ### Notifiche Desktop e Clip History # exec swaync # exec mako -exec wl-paste --type text --watch cliphist store -exec wl-paste --type image --watch cliphist store - ### Applet di rete exec nm-applet --indicator exec handy --start-hidden # speek to text exec wl-paste --type text --watch cliphist store # Stores only text data exec wl-paste --type image --watch cliphist store # Stores only image data -exec_always pkill qs; qs -d -c noctalia-shell +exec_always sh -c '/usr/bin/pkill -x qs || true; exec /usr/sbin/qs -d -c noctalia-shell' # comando per capire come si chiama l'id della app per le regole delle finestre # swaymsg -t get_tree @@ -280,3 +250,5 @@ for_window [title="Picture in picture"] floating enable, sticky enable for_window [title="Save File"] floating enable for_window [app_id="firefox" title="Firefox — Sharing Indicator"] kill + +include ~/.config/sway/noctalia diff --git a/sway/noctalia b/sway/noctalia new file mode 100644 index 0000000..a79352c --- /dev/null +++ b/sway/noctalia @@ -0,0 +1,18 @@ +set $primary #b8bb26 +set $on_primary #282828 +set $secondary #fabd2f +set $tertiary #83a598 +set $error #fb4934 +set $surface #282828 +set $on_surface #fbf1c7 +set $on_surface_variant #ebdbb2 +set $outline #786f6b + +## Window Colours +# class border backgr. text indicator child_border +client.focused $primary $surface $on_surface $primary $primary +client.focused_inactive $outline $surface $on_surface_variant $outline $outline +client.unfocused $outline $surface $on_surface_variant $outline $outline +client.urgent $error $surface $on_surface $error $error +client.placeholder $surface $surface $on_surface_variant $surface $surface +client.background $surface diff --git a/sway/scripts/lid.sh b/sway/scripts/lid.sh new file mode 100755 index 0000000..2603e0c --- /dev/null +++ b/sway/scripts/lid.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +action="$1" +internal="${2:-eDP-1}" + +active_count="$(swaymsg -t get_outputs | python3 -c 'import json,sys; print(sum(1 for o in json.load(sys.stdin) if o.get("active")))' 2>/dev/null)" + +if [ -z "$active_count" ]; then + active_count=1 +fi + +case "$action" in + on) + if [ "$active_count" -le 1 ]; then + qs -c noctalia-shell ipc call session lock + sleep 1 + systemctl suspend + else + swaymsg "output $internal disable" + fi + ;; + off) + swaymsg "output $internal enable" + ;; +esac diff --git a/zathura/noctaliarc b/zathura/noctaliarc new file mode 100644 index 0000000..13ef7df --- /dev/null +++ b/zathura/noctaliarc @@ -0,0 +1,31 @@ +set default-bg "rgba(40, 40, 40, 0.8)" +set default-fg "#fbf1c7" +set recolor-lightcolor "rgba(0,0,0,0)" +set recolor-darkcolor "#fbf1c7" + +set statusbar-bg "#282828" +set statusbar-fg "#b8bb26" +set inputbar-bg "#282828" +set inputbar-fg "#b8bb26" + +set notification-bg "#282828" +set notification-fg "#b8bb26" +set notification-error-bg "#282828" +set notification-error-fg "#fb4934" +set notification-warning-bg "#f2f4be" +set notification-warning-fg "#7d0d00" + +set index-bg "rgba(0,0,0,0)" +set index-fg "#fbf1c7" +set index-active-bg "#b8bb26" +set index-active-fg "#282828" +set highlight-color "rgba(242, 244, 190, 0.5)" +set highlight-active-color "rgba(232, 233, 149, 0.5)" +set highlight-fg "#282828" + +set completion-bg "#282828" +set completion-fg "#b8bb26" +set completion-group-bg "#282828" +set completion-group-fg "#b8bb26" +set completion-highlight-fg "#282828" +set completion-highlight-bg "#b8bb26"