From ae9a80c1f012686c1f65fce7ce76fdd1b77b0b71 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Wed, 23 Sep 2026 21:08:35 +0200 Subject: ... --- .../shell/quickshell/ActiveWindowDisplay.qml | 172 ++++++++ accounts/gkleen@skadhi/shell/quickshell/Bar.qml | 142 ++++++ .../shell/quickshell/BatteryWidget.qml | 139 ++++++ .../shell/quickshell/BrightnessOSD.qml | 117 +++++ .../shell/quickshell/BrightnessWidget.qml | 84 ++++ accounts/gkleen@skadhi/shell/quickshell/Clock.qml | 295 +++++++++++++ .../shell/quickshell/KeyboardLayout.qml | 112 +++++ .../shell/quickshell/LidSwitchInhibitorWidget.qml | 47 ++ .../gkleen@skadhi/shell/quickshell/LockSurface.qml | 227 ++++++++++ .../gkleen@skadhi/shell/quickshell/Lockscreen.qml | 128 ++++++ .../shell/quickshell/LockscreenInhibitorWidget.qml | 48 ++ .../shell/quickshell/MaterialDesignIcon.qml | 35 ++ .../gkleen@skadhi/shell/quickshell/NiriIdle.qml | 30 ++ .../shell/quickshell/NotificationDisplay.qml | 340 +++++++++++++++ .../quickshell/NotificationInhibitorWidget.qml | 266 ++++++++++++ .../shell/quickshell/PipewireWidget.qml | 483 +++++++++++++++++++++ .../shell/quickshell/PrivacyWidget.qml | 49 +++ .../shell/quickshell/Services/Brightness.qml | 107 +++++ .../shell/quickshell/Services/GpgAgent.qml | 18 + .../shell/quickshell/Services/InhibitorState.qml | 23 + .../shell/quickshell/Services/MprisProxy.qml | 8 + .../shell/quickshell/Services/NiriService.qml | 217 +++++++++ .../quickshell/Services/NotificationManager.qml | 162 +++++++ .../shell/quickshell/Services/Privacy.qml | 63 +++ .../shell/quickshell/Services/ScreenRecord.qml | 63 +++ .../quickshell/Services/WallpaperSelector.qml | 8 + .../shell/quickshell/Services/Worktime.qml | 84 ++++ .../gkleen@skadhi/shell/quickshell/SystemTray.qml | 201 +++++++++ .../gkleen@skadhi/shell/quickshell/UnixIPC.qml | 106 +++++ .../gkleen@skadhi/shell/quickshell/VolumeOSD.qml | 163 +++++++ .../shell/quickshell/WallpaperBackground.qml | 89 ++++ .../shell/quickshell/WaylandInhibitorWidget.qml | 56 +++ .../shell/quickshell/WorkspaceSwitcher.qml | 204 +++++++++ .../shell/quickshell/WorktimeWidget.qml | 212 +++++++++ .../gkleen@skadhi/shell/quickshell/YtDlpWidget.qml | 190 ++++++++ .../shell/quickshell/displaymanager.qml | 115 +++++ accounts/gkleen@skadhi/shell/quickshell/shell.qml | 53 +++ 37 files changed, 4856 insertions(+) create mode 100644 accounts/gkleen@skadhi/shell/quickshell/ActiveWindowDisplay.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Bar.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/BatteryWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/BrightnessOSD.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/BrightnessWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Clock.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/KeyboardLayout.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/LidSwitchInhibitorWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/LockSurface.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Lockscreen.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/LockscreenInhibitorWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/MaterialDesignIcon.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/NiriIdle.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/NotificationDisplay.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/NotificationInhibitorWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/PipewireWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/PrivacyWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/Brightness.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/GpgAgent.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/InhibitorState.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/MprisProxy.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/NiriService.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/NotificationManager.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/Privacy.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/ScreenRecord.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/WallpaperSelector.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/Services/Worktime.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/SystemTray.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/UnixIPC.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/VolumeOSD.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/WallpaperBackground.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/WaylandInhibitorWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/WorkspaceSwitcher.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/WorktimeWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/YtDlpWidget.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/displaymanager.qml create mode 100644 accounts/gkleen@skadhi/shell/quickshell/shell.qml (limited to 'accounts/gkleen@skadhi/shell/quickshell') diff --git a/accounts/gkleen@skadhi/shell/quickshell/ActiveWindowDisplay.qml b/accounts/gkleen@skadhi/shell/quickshell/ActiveWindowDisplay.qml new file mode 100644 index 00000000..ac1cbf3a --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/ActiveWindowDisplay.qml @@ -0,0 +1,172 @@ +import QtQuick +import qs.Services +import Quickshell +import Quickshell.Widgets + +Item { + id: activeWindowDisplay + + required property int maxWidth + required property var screen + + property var activeWindow: { + let currWindowId = Array.from(NiriService.workspaces).find(ws => { + return ws.output === screen?.name && ws.is_active; + })?.active_window_id; + + return currWindowId ? Array.from(NiriService.windows).find(win => win.id == currWindowId) : null; + } + property var windowEntry: activeWindow ? DesktopEntries.heuristicLookup(activeWindow.app_id) : null + + anchors.verticalCenter: parent.verticalCenter + width: activeWindowDisplayContent.width + height: parent.height + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + + hoverEnabled: true + + Item { + anchors.fill: parent + + Row { + id: activeWindowDisplayContent + + width: childrenRect.width + height: parent.height + anchors.verticalCenter: parent.verticalCenter + spacing: 8 + + IconImage { + id: activeWindowIcon + + implicitSize: 14 + + anchors.verticalCenter: parent.verticalCenter + + source: { + let icon = activeWindowDisplay.windowEntry?.icon + if (typeof icon === 'string' || icon instanceof String) { + if (icon.includes("?path=")) { + const split = icon.split("?path=") + if (split.length !== 2) + return icon + const name = split[0] + const path = split[1] + const fileName = name.substring( + name.lastIndexOf("/") + 1) + return `file://${path}/${fileName}` + } else + icon = Quickshell.iconPath(icon); + return icon + } + return "" + } + asynchronous: true + smooth: true + mipmap: true + } + + Text { + id: windowTitle + + width: Math.min(implicitWidth, activeWindowDisplay.maxWidth - activeWindowIcon.width - activeWindowDisplayContent.spacing) + + property var appAliases: { "Firefox": "Mozilla Firefox", "mpv Media Player": "mpv", "Thunderbird": "Mozilla Thunderbird", "Thunderbird (LMU)": "Mozilla Thunderbird" } + + elide: Text.ElideRight + maximumLineCount: 1 + color: "white" + anchors.verticalCenter: parent.verticalCenter + text: { + if (!activeWindowDisplay.activeWindow) + return ""; + + var title = activeWindowDisplay.activeWindow.title; + var appName = activeWindowDisplay.windowEntry?.name; + if (appAliases[appName]) + appName = appAliases[appName]; + if (appName && title.endsWith(appName)) { + const oldTitle = title; + title = title.substring(0, title.length - appName.length); + title = title.replace(/\s*(—|-)\s*$/, ""); + if (!title) + title = oldTitle; + } + return title; + } + } + } + } + } + + Loader { + id: tooltipLoader + + active: false + + Connections { + target: widgetMouseArea + function onContainsMouseChanged() { + if (widgetMouseArea.containsMouse) + tooltipLoader.active = true; + } + } + + PopupWindow { + id: tooltip + + property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse + + anchor { + item: widgetMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: tooltip.visible ? 100 : 500 + onTriggered: { + tooltip.visible = tooltip.nextVisible; + if (!tooltip.visible) + tooltipLoader.active = false; + } + } + + implicitWidth: widgetTooltipText.contentWidth + 16 + implicitHeight: widgetTooltipText.contentHeight + 16 + color: "black" + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + anchors.fill: parent + + Item { + anchors.fill: parent + + Text { + id: widgetTooltipText + + anchors.centerIn: parent + + font.pointSize: 10 + font.family: "Fira Mono" + color: "white" + + text: JSON.stringify(Object.assign({}, activeWindowDisplay.activeWindow), null, 2) + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Bar.qml b/accounts/gkleen@skadhi/shell/quickshell/Bar.qml new file mode 100644 index 00000000..33909884 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Bar.qml @@ -0,0 +1,142 @@ +import Quickshell +import Quickshell.Wayland +import QtQuick +import qs.Services + +PanelWindow { + id: bar + + WlrLayershell.namespace: "bar" + + property bool haveMaximizedWindow: { + let currWindowId = Array.from(NiriService.workspaces).find(ws => { + return ws.output === bar.screen.name && ws.is_active; + })?.active_window_id; + let activeWindowTileSize = Array.from(NiriService.windows).find(win => win.id == currWindowId)?.layout?.tile_size; + + if (!activeWindowTileSize) + return false; + return activeWindowTileSize[0] >= bar.screen.width && activeWindowTileSize[1] >= bar.screen.height - bar.height; + } + + anchors { + top: true + left: true + right: true + } + margins { + left: bar.haveMaximizedWindow ? 0 : 26 + 8 + right: bar.haveMaximizedWindow ? 0 : 26 + 8 + } + + implicitHeight: 21 + color: "transparent" + + property bool haveScreenshare: Array.from(NiriService.casts).some(cast => cast.target.Output?.name == bar.screen.name) + + Rectangle { + color: { + if (bar.haveScreenshare) + return bar.haveMaximizedWindow ? Qt.rgba(0.2, 0, 0, 1) : Qt.rgba(0.2, 0, 0, 0.75); + return bar.haveMaximizedWindow ? "black" : Qt.rgba(0, 0, 0, 0.75); + } + anchors.fill: parent + // bottomLeftRadius: 8 + // bottomRightRadius: 8 + } + + Row { + id: left + + height: parent.height + width: childrenRect.width + anchors.left: parent.left + anchors.leftMargin: 8 + anchors.verticalCenter: parent.verticalCenter + spacing: 8 + + WorkspaceSwitcher { + screen: bar.screen + } + } + + Row { + id: center + + height: parent.height + width: childrenRect.width + anchors.centerIn: parent + spacing: 5 + + ActiveWindowDisplay { + screen: bar.screen + maxWidth: bar.width - 2*Math.max(left.width, right.width) - 2*8 + } + } + + Row { + id: right + + height: parent.height + width: childrenRect.width + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + WorktimeWidget {} + + KeyboardLayout {} + + Item { + visible: privacy.visible || ytDlp.visible + height: parent.height + width: 8 - 4 + } + + PrivacyWidget { + id: privacy + } + + Item { + visible: privacy.visible && ytDlp.visible + height: parent.height + width: 8 - 4 + } + + YtDlpWidget { + id: ytDlp + } + + Item { + visible: privacy.visible || ytDlp.visible + height: parent.height + width: 8 - 4 + } + + SystemTray {} + + PipewireWidget {} + + BrightnessWidget {} + + BatteryWidget {} + + LockscreenInhibitorWidget {} + + WaylandInhibitorWidget { + window: bar + } + + LidSwitchInhibitorWidget {} + + NotificationInhibitorWidget {} + + Item { + height: parent.height + width: 8 - 4 + } + + Clock {} + } +} \ No newline at end of file diff --git a/accounts/gkleen@skadhi/shell/quickshell/BatteryWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/BatteryWidget.qml new file mode 100644 index 00000000..f465fa20 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/BatteryWidget.qml @@ -0,0 +1,139 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.UPower + +Item { + id: root + + height: parent.height + width: batteryIcon.width + 8 + anchors.verticalCenter: parent.verticalCenter + + property var batteryDevice: Array.from(UPower.devices.values).find(dev => dev.isLaptopBattery) + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + + hoverEnabled: true + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: batteryIcon + + implicitSize: 14 + anchors.centerIn: parent + + icon: { + if (!root.batteryDevice?.ready) + return "battery-unknown"; + + if (root.batteryDevice.state == UPowerDeviceState.FullyCharged) + return "power-plug-battery"; + + const perdec = 10 * Math.max(1, Math.ceil(root.batteryDevice.percentage * 10)); + if (root.batteryDevice.state == UPowerDeviceState.Charging) + return `battery-charging-${perdec}`; + if (perdec == 100) + return "battery"; + return `battery-${perdec}`; + } + color: { + if (!root.batteryDevice?.ready) + return "#555"; + + if (root.batteryDevice.state != UPowerDeviceState.FullyCharged && root.batteryDevice.state != UPowerDeviceState.Charging && root.batteryDevice.timeToEmpty < 20 * 60) + return "#f2201f"; + if (root.batteryDevice.state != UPowerDeviceState.FullyCharged && root.batteryDevice.state != UPowerDeviceState.Charging && root.batteryDevice.timeToEmpty < 40 * 60) + return "#f28a21"; + if (root.batteryDevice.state != UPowerDeviceState.FullyCharged) + return "#fff"; + return "#555"; + } + } + } + } + + PopupWindow { + id: tooltip + + property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse + + anchor { + item: widgetMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: tooltip.visible = tooltip.nextVisible + } + + implicitWidth: widgetTooltipText.contentWidth + 16 + implicitHeight: widgetTooltipText.contentHeight + 16 + color: "black" + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + anchors.fill: parent + + Item { + anchors.fill: parent + + Text { + id: widgetTooltipText + + anchors.centerIn: parent + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + text: { + if (!root.batteryDevice?.ready) + return "Not ready"; + + const stateStr = UPowerDeviceState.toString(root.batteryDevice.state); + var outStr = stateStr; + if (root.batteryDevice.state !== UPowerDeviceState.FullyCharged) + outStr += ` ${Math.round(root.batteryDevice.percentage * 100)}%`; + + function formatTime(t) { + var res = ""; + for (const unit of [{ "s": "h", "v": 3600 }, { "s": "m", "v": 60 }, { "s": "s", "v": 1 }]) { + if (t < unit.v) + continue; + res += Math.floor(t / unit.v) + unit.s; + t %= unit.v; + } + return res; + } + if (root.batteryDevice.timeToEmpty != 0) { + const tStr = formatTime(Math.floor(root.batteryDevice.timeToEmpty / 60) * 60); + if (tStr) + outStr += " " + tStr; + } else if (root.batteryDevice.timeToFull != 0) { + const tStr = formatTime(Math.ceil(root.batteryDevice.timeToFull / 60) * 60); + if (tStr) + outStr += " " + tStr; + } + + return outStr; + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/BrightnessOSD.qml b/accounts/gkleen@skadhi/shell/quickshell/BrightnessOSD.qml new file mode 100644 index 00000000..a432179e --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/BrightnessOSD.qml @@ -0,0 +1,117 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import qs.Services + +Scope { + id: root + + property bool show: false + property bool inhibited: true + + Connections { + target: Brightness + + function onCurrBrightnessChanged() { + root.show = true; + hideTimer.restart(); + } + } + + onShowChanged: { + if (show) + hideTimer.restart(); + } + + Timer { + id: hideTimer + interval: 1000 + onTriggered: root.show = false + } + + Timer { + id: startInhibit + interval: 100 + running: true + onTriggered: { + root.show = false; + root.inhibited = false; + } + } + + LazyLoader { + active: root.show && !root.inhibited + + Variants { + model: Quickshell.screens + + delegate: Scope { + id: screenScope + + required property var modelData + + PanelWindow { + id: window + + screen: screenScope.modelData + + anchors.top: true + margins.top: screen.height / 2 - 50 + 3.5 + exclusiveZone: 0 + exclusionMode: ExclusionMode.Ignore + + implicitWidth: 400 + implicitHeight: 50 + + mask: Region {} + + color: "transparent" + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.75) + } + + RowLayout { + id: layout + + anchors.centerIn: parent + + height: 50 - 8*2 + width: 400 - 8*2 + + MaterialDesignIcon { + id: volumeIcon + + implicitWidth: parent.height + implicitHeight: parent.height + + icon: `brightness-${Math.min(7, Math.floor(Brightness.currBrightness * 7) + 1)}` + } + + Rectangle { + Layout.fillWidth: true + + implicitHeight: 10 + + color: "#50ffffff" + + Rectangle { + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + + color: "white" + + implicitWidth: parent.width * Brightness.currBrightness + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/BrightnessWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/BrightnessWidget.qml new file mode 100644 index 00000000..3bb5a80e --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/BrightnessWidget.qml @@ -0,0 +1,84 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Services + +Item { + height: parent.height + width: brightnessIcon.width + 8 + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + + hoverEnabled: true + + property real sensitivity: (1 / 50) / 120 + onWheel: event => Brightness.currBrightness += event.angleDelta.y * sensitivity + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: brightnessIcon + + implicitSize: 14 + anchors.centerIn: parent + + icon: `brightness-${Math.min(7, Math.floor(Brightness.currBrightness * 7) + 1)}` + color: "#555" + } + } + } + + PopupWindow { + id: tooltip + + property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse + + anchor { + item: widgetMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: tooltip.visible = tooltip.nextVisible + } + + implicitWidth: widgetTooltipText.contentWidth + 16 + implicitHeight: widgetTooltipText.contentHeight + 16 + color: "black" + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + anchors.fill: parent + + Item { + anchors.fill: parent + + Text { + id: widgetTooltipText + + anchors.centerIn: parent + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + text: `${Math.round(Brightness.currBrightness * 100)}%` + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Clock.qml b/accounts/gkleen@skadhi/shell/quickshell/Clock.qml new file mode 100644 index 00000000..58a80b8b --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Clock.qml @@ -0,0 +1,295 @@ +import QtQml +import QtQuick +import Quickshell +import Custom as Custom +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Widgets + +Item { + id: clockItem + + property bool calendarPopup: true + + width: clock.contentWidth + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: clockMouseArea + + anchors.fill: parent + hoverEnabled: true + enabled: clockItem.calendarPopup + + Item { + anchors.fill: parent + + Text { + id: clock + color: "white" + + anchors.verticalCenter: parent.verticalCenter + + Custom.Chrono { + id: chrono + + onDateChanged: clock.text = format("W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}") + } + + font.pointSize: 10 + font.family: "Fira Sans" + font.features: { "tnum": 1 } + } + } + } + + Loader { + id: tooltipLoader + + active: false + + Connections { + target: clockMouseArea + function onContainsMouseChanged() { + if (clockMouseArea.containsMouse) + tooltipLoader.active = true; + } + } + + sourceComponent: PopupWindow { + id: tooltip + + property bool nextVisible: clockMouseArea.containsMouse || tooltipMouseArea.containsMouse + + anchor { + item: clockMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: { + tooltip.visible = tooltip.nextVisible; + if (!tooltip.visible) + tooltipLoader.active = false; + } + } + + implicitWidth: tooltipLayout.childrenRect.width + 16 + implicitHeight: tooltipLayout.childrenRect.height + 16 + color: "black" + + onVisibleChanged: { + yearCalendar.year = chrono.date.getFullYear(); + yearCalendar.angleRem = 0; + } + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + onWheel: event => yearCalendar.scrollYear(event) + + anchors.fill: parent + + Item { + id: clockTooltipContent + + anchors.fill: parent + + ColumnLayout { + id: tooltipLayout + + anchors { + left: parent.left + top: parent.top + leftMargin: 8 + topMargin: 8 + } + + Text { + id: yearLabel + + horizontalAlignment: Text.AlignHCenter + + font.pointSize: 14 + font.family: "Fira Sans" + font.features: { "tnum": 1 } + color: "white" + + text: yearCalendar.year + + Layout.fillWidth: true + Layout.bottomMargin: 8 + } + + GridLayout { + property int year: chrono.date.getFullYear() + + id: yearCalendar + + columns: 3 + columnSpacing: 16 + rowSpacing: 16 + + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: false + + property real angleRem: 0 + property real sensitivity: 1 / 120 + + function scrollYear(event) { + angleRem += event.angleDelta.y; + const d = Math.round(angleRem * sensitivity); + yearCalendar.year -= d; + angleRem -= d / sensitivity; + } + + Connections { + target: clockMouseArea + function onWheel(event) { yearCalendar.scrollYear(event); } + } + + Repeater { + model: 12 + + GridLayout { + columns: 2 + + required property int index + property int month: index + + id: monthCalendar + + Layout.alignment: Qt.AlignTop | Qt.AlignRight + Layout.fillWidth: false + + Text { + Layout.column: 1 + Layout.fillWidth: true + + horizontalAlignment: Text.AlignHCenter + + font.pointSize: 10 + font.family: "Fira Sans" + + text: new Date(yearCalendar.year, monthCalendar.month, 1).toLocaleString(Qt.locale("en_DK"), "MMMM") + + color: "#ffead3" + } + + DayOfWeekRow { + locale: grid.locale + + Layout.row: 1 + Layout.column: 1 + Layout.fillWidth: true + + delegate: WrapperItem { + required property string shortName + + width: dowLabel.contentWidth + 6 + + Text { + id: dowLabel + + anchors.fill: parent + + font.pointSize: 10 + font.family: "Fira Sans" + + text: parent.shortName + color: "#ffcc66" + + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + } + + WeekNumberColumn { + month: grid.month + year: grid.year + locale: grid.locale + + Layout.fillHeight: true + + delegate: Text { + required property int weekNumber + + opacity: { + const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7); + const dayOfWeek = simple.getDay(); + const isoWeekStart = simple; + + isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1); + if (dayOfWeek > 4) { + isoWeekStart.setDate(isoWeekStart.getDate() + 7); + } + + for (let i = 0; i < 7; i++) { + const dayInWeek = new Date(isoWeekStart); + dayInWeek.setDate(dayInWeek.getDate() + i); + if (dayInWeek.getMonth() == monthCalendar.month) + return 1; + } + + return 0; + } + + font.pointSize: 10 + font.family: "Fira Sans" + font.features: { "tnum": 1 } + + text: weekNumber + color: "#99ffdd" + + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + } + } + + MonthGrid { + id: grid + + year: yearCalendar.year + month: monthCalendar.month + locale: Qt.locale("en_DK") + + Layout.fillWidth: true + Layout.fillHeight: true + + delegate: Text { + required property var model + + opacity: model.month === monthCalendar.month ? 1 : 0 + + font.pointSize: 10 + font.family: "Fira Sans" + font.features: { "tnum": 1 } + + property bool today: chrono.date.getFullYear() == model.year && chrono.date.getMonth() == model.month && chrono.date.getDate() == model.day + + text: model.day + color: today ? "#ff6699" : "white" + + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + } + } + } + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/KeyboardLayout.qml b/accounts/gkleen@skadhi/shell/quickshell/KeyboardLayout.qml new file mode 100644 index 00000000..46302e54 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/KeyboardLayout.qml @@ -0,0 +1,112 @@ +import Quickshell +import QtQuick +import qs.Services +import Quickshell.Widgets + +Item { + width: kbdLabel.contentWidth + 8 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: kbdMouseArea + + anchors.fill: parent + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: true + onClicked: { + NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": "Next" } } }, _ => {}) + } + onWheel: event => { + NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": event.angleDelta > 0 ? "Next" : "Prev" } } }, _ => {}) + } + + Rectangle { + id: kbdWidget + + property var keyboardAbbrev: { "English (programmer Dvorak)": "dvp", "English (US)": "us" } + + anchors.fill: parent + color: { + if (kbdMouseArea.containsMouse) { + return "#33808080"; + } + return "transparent"; + } + + Text { + id: kbdLabel + + font.pointSize: 10 + font.family: "Fira Sans" + color: { + if (NiriService.keyboardLayouts?.current_idx === 0) + return "#555"; + return "white"; + } + anchors.centerIn: parent + + text: { + const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx]; + if (!currentLayout) + return ""; + return kbdWidget.keyboardAbbrev[currentLayout] ? kbdWidget.keyboardAbbrev[currentLayout] : currentLayout; + } + } + } + } + + PopupWindow { + id: tooltip + + property bool nextVisible: kbdMouseArea.containsMouse || tooltipMouseArea.containsMouse + + anchor { + item: kbdMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: tooltip.visible = tooltip.nextVisible + } + + implicitWidth: kbdTooltipText.contentWidth + 16 + implicitHeight: kbdTooltipText.contentHeight + 16 + color: "black" + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + anchors.fill: parent + + Item { + anchors.fill: parent + + Text { + id: kbdTooltipText + + anchors.centerIn: parent + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + text: { + const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx]; + return currentLayout || ""; + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/LidSwitchInhibitorWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/LidSwitchInhibitorWidget.qml new file mode 100644 index 00000000..8410dcda --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/LidSwitchInhibitorWidget.qml @@ -0,0 +1,47 @@ +import Quickshell +import QtQuick +import Quickshell.Widgets +import qs.Services + +Item { + id: root + + width: icon.width + 8 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: InhibitorState.lidSwitchInhibited = !InhibitorState.lidSwitchInhibited + + Rectangle { + anchors.fill: parent + color: { + if (widgetMouseArea.containsMouse) { + return "#33808080"; + } + return "transparent"; + } + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: icon + + implicitSize: 14 + anchors.centerIn: parent + + icon: InhibitorState.lidSwitchInhibited ? "laptop-off" : "laptop" + color: InhibitorState.lidSwitchInhibited ? "#f28a21" : "#555" + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/LockSurface.qml b/accounts/gkleen@skadhi/shell/quickshell/LockSurface.qml new file mode 100644 index 00000000..f4f8f0cd --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/LockSurface.qml @@ -0,0 +1,227 @@ +import Quickshell.Widgets +import QtQuick.Effects +import QtQuick.Layouts +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Fusion +import qs.Services +import QtQml + +Item { + id: lockSurface + + property var screen + property list messages: [] + property bool responseRequired: false + property bool responseVisible: false + property string currentText: "" + property bool authRunning: false + + signal response(string responseText) + + anchors.fill: parent + + Item { + id: background + + anchors.fill: parent + + property Img current: one + property string source: selector.selected + + WallpaperSelector { + id: selector + seed: lockSurface.screen?.name || "" + } + + onSourceChanged: { + if (!source) + current = null; + else if (current === one) + two.update() + else + one.update() + } + + Img { id: one } + Img { id: two } + + component Img: Item { + id: img + + property string source + + function update() { + source = background.source || "" + } + + anchors.fill: parent + + Image { + id: imageSource + + source: img.source + sourceSize: Qt.size(parent.width, parent.height) + fillMode: Image.PreserveAspectCrop + smooth: true + visible: false + asynchronous: true + cache: false + + onStatusChanged: { + if (status === Image.Ready) { + background.current = img + } + } + } + + MultiEffect { + id: imageEffect + + source: imageSource + anchors.fill: parent + blurEnabled: true + blur: 1 + blurMax: 64 + blurMultiplier: 2 + + opacity: 0 + + states: State { + name: "visible" + when: background.current === img + + PropertyChanges { + imageEffect.opacity: 1 + } + StateChangeScript { + name: "unloadOther" + script: { + if (img === one) + two.source = "" + if (img === two) + one.source = "" + } + } + } + + transitions: Transition { + SequentialAnimation { + NumberAnimation { + target: imageEffect + properties: "opacity" + duration: { + if (img === one && two.source == "" || img === two && one.source == "") + return 0; + return 5000; + } + easing.type: Easing.OutCubic + } + ScriptAction { + scriptName: "unloadOther" + } + } + } + } + } + } + + Item { + anchors { + top: lockSurface.top + left: lockSurface.left + right: lockSurface.right + } + + implicitWidth: lockSurface.width + implicitHeight: 21 + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.75) + } + + Clock { + anchors.centerIn: parent + calendarPopup: false + } + } + + WrapperRectangle { + id: unlockUi + + Keys.onPressed: event => { + if (!lockSurface.authRunning) { + event.accepted = true; + lockSurface.authRunning = true; + } + } + focus: !passwordBox.visible + + visible: lockSurface.authRunning + + color: Qt.rgba(0, 0, 0, 0.75) + margin: 8 + + anchors.centerIn: parent + + ColumnLayout { + spacing: 4 + + BusyIndicator { + visible: running + running: !Array.from(lockSurface.messages).length && !lockSurface.responseRequired + } + + Repeater { + model: lockSurface.messages + + Text { + required property var modelData + + font.pointSize: 10 + font.family: "Fira Sans" + color: modelData.error ? "#f28a21" : "#ffffff" + + text: String(modelData.text).trim() + + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + } + } + + TextField { + id: passwordBox + + visible: lockSurface.responseRequired + echoMode: lockSurface.responseVisible ? TextInput.Normal : TextInput.Password + inputMethodHints: Qt.ImhSensitiveData + + onTextChanged: lockSurface.currentText = passwordBox.text + onAccepted: { + passwordBox.readOnly = true; + lockSurface.response(lockSurface.currentText); + } + + Connections { + target: lockSurface + function onCurrentTextChanged() { + passwordBox.text = lockSurface.currentText + } + } + Connections { + target: lockSurface + function onResponseRequiredChanged() { + if (lockSurface.responseRequired) + passwordBox.readOnly = false; + passwordBox.focus = true; + passwordBox.selectAll(); + } + } + + Layout.topMargin: 4 + Layout.fillWidth: true + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Lockscreen.qml b/accounts/gkleen@skadhi/shell/quickshell/Lockscreen.qml new file mode 100644 index 00000000..0b4a68b2 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Lockscreen.qml @@ -0,0 +1,128 @@ +import Quickshell +import Quickshell.Wayland +import Quickshell.Io +import Quickshell.Services.Pam +import Quickshell.Services.Mpris +import Custom as Custom +import qs.Services +import QtQml + +Scope { + id: lockscreen + + property string currentText: "" + + PamContext { + id: pam + + property list messages: [] + + config: "quickshell" + onCompleted: result => { + if (result === PamResult.Success) { + lock.locked = false; + } + } + onPamMessage: { + messages = Array.from(messages).concat([{ "text": pam.message, "error": pam.messageIsError }]) + } + onActiveChanged: { + messages = []; + } + } + + Connections { + target: Custom.Systemd + function onSleep(before: bool) { + console.log(`received prepare for sleep ${before}`); + if (before) { + lock.locked = true; + if (pam.active) + pam.abort(); + } + } + function onLock() { lock.locked = true; } + function onUnlock() { lock.locked = false; } + } + + IdleMonitor { + id: idleMonitor + enabled: !lock.secure && !InhibitorState.lockscreenInhibited + timeout: 600 + respectInhibitors: true + + onIsIdleChanged: { + if (idleMonitor.isIdle) + lock.locked = true; + } + } + + Custom.SystemdInhibitor { + enabled: !lock.secure + + what: Custom.SystemdInhibitorParams.Sleep + who: "quickshell" + why: "Lock session" + mode: Custom.SystemdInhibitorParams.Delay + } + + Binding { + target: NotificationManager + property: "lockscreenActive" + value: lock.locked + } + + WlSessionLock { + id: lock + + onLockStateChanged: { + if (!locked && pam.active) + pam.abort(); + + if (locked) { + NiriService.sendCommand({ "Action": { "PowerOffMonitors": {} } }, _ => {}); + Custom.KeePassXC.lockAllDatabases(); + Array.from(MprisProxy.players).forEach(player => { + if (player.canPause && player.isPlaying) + player.pause(); + }); + // Custom.Systemd.stopUserUnit("gpg-agent.service", "replace"); + GpgAgent.reloadAgent(); + } + } + Component.onCompleted: { (_ => {})(MprisProxy.players); } + + onSecureStateChanged: Custom.Systemd.lockedHint = lock.secure + + WlSessionLockSurface { + id: lockSurface + + color: "black" + + LockSurface { + id: surfaceContent + + onResponse: responseText => pam.respond(responseText) + onAuthRunningChanged: { + if (authRunning) + pam.start(); + } + Connections { + target: pam + function onMessagesChanged() { surfaceContent.messages = pam.messages; } + function onResponseRequiredChanged() { surfaceContent.responseRequired = pam.responseRequired; } + function onActiveChanged() { surfaceContent.authRunning = pam.active; } + } + onCurrentTextChanged: lockscreen.currentText = currentText + Connections { + target: lockscreen + function onCurrentTextChanged() { surfaceContent.currentText = lockscreen.currentText; } + } + Connections { + target: lockSurface + function onScreenChanged() { surfaceContent.screen = lockSurface.screen; } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/LockscreenInhibitorWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/LockscreenInhibitorWidget.qml new file mode 100644 index 00000000..1cbf3f16 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/LockscreenInhibitorWidget.qml @@ -0,0 +1,48 @@ +import Quickshell +import QtQuick +import Quickshell.Widgets +import qs.Services + +Item { + id: root + + width: icon.width + 8 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + + // enabled: InhibitorState.lockscreenInhibited || !InhibitorState.waylandIdleInhibited + hoverEnabled: true + cursorShape: widgetMouseArea.enabled ? Qt.PointingHandCursor : undefined + + onClicked: InhibitorState.lockscreenInhibited = !InhibitorState.lockscreenInhibited + + Rectangle { + anchors.fill: parent + color: { + if (widgetMouseArea.containsMouse) { + return "#33808080"; + } + return "transparent"; + } + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: icon + + implicitSize: 14 + anchors.centerIn: parent + + icon: InhibitorState.lockscreenInhibited || InhibitorState.waylandIdleInhibited ? "lock-off" : "lock" + color: InhibitorState.lockscreenInhibited ? "#f28a21" : "#555" + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/MaterialDesignIcon.qml b/accounts/gkleen@skadhi/shell/quickshell/MaterialDesignIcon.qml new file mode 100644 index 00000000..155a009e --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/MaterialDesignIcon.qml @@ -0,0 +1,35 @@ +import QtQuick +import QtQuick.Effects + +Item { + id: root + + required property string icon + property color color: "white" + + property real implicitSize: 0 + + readonly property real actualSize: Math.min(root.width, root.height) + + implicitWidth: root.implicitSize + implicitHeight: root.implicitSize + + Image { + id: sourceImage + source: "file://" + @mdi@ + "/svg/" + root.icon + ".svg" + anchors.fill: parent + fillMode: Image.PreserveAspectFit + + sourceSize.width: root.actualSize + sourceSize.height: root.actualSize + + layer.enabled: true + layer.effect: MultiEffect { + id: effect + + brightness: 1 + colorization: 1 + colorizationColor: root.color + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/NiriIdle.qml b/accounts/gkleen@skadhi/shell/quickshell/NiriIdle.qml new file mode 100644 index 00000000..beff205c --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/NiriIdle.qml @@ -0,0 +1,30 @@ +import QtQml +import Quickshell +import Quickshell.Wayland +import qs.Services +import Custom as Custom + +Scope { + IdleMonitor { + id: idleMonitor30 + timeout: 30 + + onIsIdleChanged: Custom.Systemd.setIdleHint(idleMonitor30.isIdle) + } + IdleMonitor { + id: idleMonitor540 + timeout: 540 + + onIsIdleChanged: { + if (idleMonitor540.isIdle) + NiriService.sendCommand({ "Action": { "PowerOffMonitors": {} } }, _ => {}); + } + } + Connections { + target: Custom.Systemd + function onSleep(before: bool) { + if (!before) + NiriService.sendCommand({ "Action": { "PowerOnMonitors": {} } }, _ => {}); + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/NotificationDisplay.qml b/accounts/gkleen@skadhi/shell/quickshell/NotificationDisplay.qml new file mode 100644 index 00000000..cc0e49b1 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/NotificationDisplay.qml @@ -0,0 +1,340 @@ +import QtQml +import QtQml.Models +import QtQuick +import Quickshell +import Quickshell.Widgets +import Quickshell.Wayland +import qs.Services +import QtQuick.Layouts +import Quickshell.Services.Notifications + +Scope { + id: root + + property var activeScreen: Array.from(Quickshell.screens).find(screen => screen.name === Array.from(NiriService.workspaces).find(ws => ws.is_focused)?.output) ?? null + + Instantiator { + id: notifsRepeater + + model: ScriptModel { + values: NotificationManager.groups + } + + delegate: PanelWindow { + id: notifWindow + + visible: NotificationManager.active + + screen: root.activeScreen + + WlrLayershell.namespace: "notifications" + + required property var modelData + required property var index + + property int activeIx: modelData.length - 1 + onModelDataChanged: { + notifWindow.activeIx = modelData.length - 1; + } + + property color textColor: { + if (notifWindow.modelData?.[notifWindow.activeIx]?.urgency == NotificationUrgency.Low) + return "#ff999999"; + return "white"; + } + property color backgroundColor: { + if (notifWindow.modelData?.[notifWindow.activeIx]?.urgency == NotificationUrgency.Critical) + return "#dd900000"; + return "black"; + } + + anchors { + right: true + top: true + } + + readonly property real spaceAbove: { + var res = 0; + for (let i = 0; i < notifWindow.index; i++) { + (_ => {})(notifsRepeater.objectAt(i).modelData); + res += notifsRepeater.objectAt(i).height + 8; + } + return res; + } + + margins { + right: 26 + 8 + top: 8 + spaceAbove + } + + color: "transparent" + + implicitHeight: Math.max(notifCount.visible ? notifCount.contentHeight : 0, notifSummary.contentHeight) + (notifBody.visible ? 8 + notifBody.contentHeight : 0) + (notifActions.visible ? 8 + notifActions.height : 0) + (notifTime.visible ? 8 + notifTime.contentHeight : 0) + 16 + implicitWidth: 400 + + WrapperMouseArea { + enabled: true + + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + + onClicked: { + for (const notif of notifWindow.modelData) + notif.dismiss(); + } + + property real angleRem: 0 + property real sensitivity: 1 / 120 + onWheel: event => { + angleRem += event.angleDelta.y; + const d = Math.round(angleRem * sensitivity); + angleRem -= d / sensitivity; + notifWindow.activeIx = ((notifWindow.modelData?.length ?? 1) + notifWindow.activeIx - d) % (notifWindow.modelData?.length ?? 1); + } + + Rectangle { + color: notifWindow.backgroundColor + anchors.fill: parent + border { + color: Qt.hsla(195/360, 1, 0.45, 1) + width: 2 + } + + GridLayout { + id: notifLayout + + width: 400 - 16 + anchors.fill: parent + anchors.margins: 8 + columnSpacing: 8 + rowSpacing: 8 + + columns: notifImage.visible ? 3 : 2 + rows: { + var res = 1; + if (notifBody.visible) + res += 1; + if (notifActions.visible) + res += 1; + if (notifTime.visible) + res += 1; + return res; + } + + Text { + id: notifCount + + visible: notifWindow.modelData?.length > 1 ?? false + text: `${notifWindow.activeIx + 1}/${notifWindow.modelData?.length ?? ""}` + + font.pointSize: 10 + font.family: "Fira Sans" + font.bold: true + font.features: { "tnum": 1 } + color: notifWindow.textColor + maximumLineCount: 1 + + Layout.fillWidth: false + Layout.row: 0 + Layout.column: notifImage.visible ? 1 : 0 + } + + Text { + id: notifSummary + + text: notifWindow.modelData?.[notifWindow.activeIx]?.summary ?? "" + + font.pointSize: 10 + font.family: "Fira Sans" + font.italic: true + color: notifWindow.textColor + maximumLineCount: 1 + elide: Text.ElideRight + + Layout.fillWidth: true + Layout.row: 0 + Layout.column: (notifCount.visible ? 1 : 0) + (notifImage.visible ? 1 : 0) + Layout.columnSpan: notifCount.visible ? 1 : 2 + } + + Image { + id: notifImage + + visible: (notifWindow.modelData?.[notifWindow.activeIx]?.image || notifWindow.modelData?.[notifWindow.activeIx]?.appIcon) ?? false + + onStatusChanged: { + if (notifImage.status == Image.Error) + notifImage.visible = false; + } + + source: (notifWindow.modelData?.[notifWindow.activeIx]?.image || notifWindow.modelData?.[notifWindow.activeIx]?.appIcon) ?? "" + fillMode: Image.PreserveAspectFit + asynchronous: true + smooth: true + mipmap: true + + Layout.maximumWidth: 50 + Layout.column: 0 + Layout.row: 0 + Layout.fillHeight: true + Layout.rowSpan: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0) + } + + Text { + id: notifBody + + visible: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? false + text: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? "" + textFormat: Text.RichText + wrapMode: Text.Wrap + + font.pointSize: 10 + font.family: "Fira Sans" + color: notifWindow.textColor + + Layout.fillWidth: true + Layout.row: 1 + Layout.column: notifImage.visible ? 1 : 0 + Layout.columnSpan: notifCount.visible ? 2 : 1 + } + + Text { + id: notifTime + + Connections { + target: NotificationManager.clock + function onDateChanged() { + notifTime.text = NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime); + } + } + + visible: notifTime.text && notifTime.text !== "now" + text: NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime) + + font.pointSize: 8 + font.family: "Fira Sans" + font.italic: true + color: "#555" + maximumLineCount: 1 + horizontalAlignment: Text.AlignRight + + Layout.fillWidth: true + Layout.row: notifBody.visible ? 2 : 1 + Layout.column: notifImage.visible ? 1 : 0 + Layout.columnSpan: 2 + } + + RowLayout { + id: notifActions + + visible: notifWindow.modelData?.[notifWindow.activeIx]?.actions.length > 0 ?? false + + spacing: 8 + uniformCellSizes: true + + width: 400 - 16 + Layout.row: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0) + Layout.column: 0 + Layout.columnSpan: 2 + (notifImage.visible ? 1 : 0) + + Repeater { + model: ScriptModel { + values: notifWindow.modelData?.[notifWindow.activeIx]?.actions + } + + delegate: WrapperMouseArea { + id: actionMouseArea + + required property var modelData + + height: actionLabelWrapper.implicitHeight + Layout.fillWidth: true + Layout.horizontalStretchFactor: 1 + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: actionMouseArea.modelData?.invoke() + + Rectangle { + anchors.fill: parent + + color: actionMouseArea.containsMouse ? "#20ffffff" : "transparent" + + border { + width: 2 + color: "#20ffffff" + } + + WrapperItem { + id: actionLabelWrapper + + margin: 8 + anchors.centerIn: parent + + RowLayout { + id: actionLabelLayout + + spacing: 8 + + IconImage { + id: actionIcon + + visible: notifWindow.modelData?.[notifWindow.activeIx]?.hasActionIcons + + onStatusChanged: { + if (actionIcon.status == Image.Error) + actionIcon.visible = false; + } + + implicitSize: 16 + source: { + if (!actionIcon.visible) + return ""; + + let icon = actionMouseArea.modelData?.identifier ?? "" + if (icon.includes("?path=")) { + const split = icon.split("?path=") + if (split.length !== 2) + return icon + const name = split[0] + const path = split[1] + const fileName = name.substring( + name.lastIndexOf("/") + 1) + return `file://${path}/${fileName}` + } + return icon + } + asynchronous: true + smooth: true + mipmap: true + + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + } + + Text { + id: actionLabel + + visible: actionMouseArea.modelData?.text ?? false + + text: actionMouseArea.modelData?.text ?? "" + + font.pointSize: 10 + font.family: "Fira Sans" + color: notifWindow.textColor + maximumLineCount: 1 + elide: Text.ElideRight + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/NotificationInhibitorWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/NotificationInhibitorWidget.qml new file mode 100644 index 00000000..80cbba19 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/NotificationInhibitorWidget.qml @@ -0,0 +1,266 @@ +import Quickshell +import QtQuick +import Quickshell.Widgets +import qs.Services +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Shapes + +Item { + id: root + + width: icon.width + 8 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: NotificationManager.displayInhibited = !NotificationManager.displayInhibited + + Rectangle { + anchors.fill: parent + color: { + if (widgetMouseArea.containsMouse) { + return "#33808080"; + } + return "transparent"; + } + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: icon + + implicitSize: 14 + anchors.centerIn: parent + + icon: NotificationManager.active ? "message" : "message-off" + color: { + if (!NotificationManager.active && !NotificationManager.displayInhibited) + return "#f28a21"; + if (NotificationManager.displayInhibited) + return "white"; + return "#555"; + } + } + } + } + } + + Loader { + id: tooltipLoader + + active: false + + Connections { + target: widgetMouseArea + function onContainsMouseChanged() { + if (widgetMouseArea.containsMouse) + tooltipLoader.active = true; + } + } + + sourceComponent: PopupWindow { + id: tooltip + + property bool nextVisible: NotificationManager.active && NotificationManager.history.length > 0 && (widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse) + + anchor { + item: widgetMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: tooltip.visible ? 100 : 500 + onTriggered: { + tooltip.visible = tooltip.nextVisible; + if (!tooltip.visible) + tooltipLoader.active = false; + } + } + + implicitWidth: 400 + implicitHeight: Math.min(tooltip.screen.height * 0.66, Math.max(100, scroll.contentHeight + 16)) + color: "black" + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + anchors.fill: parent + + WrapperItem { + margin: 8 + + ScrollView { + id: scroll + + contentWidth: availableWidth + // ScrollBar.vertical.policy: ScrollBar.AlwaysOn + + ColumnLayout { + id: historyLayout + anchors { + left: parent.left + right: parent.right + } + + spacing: 8 + + Repeater { + model: ScriptModel { + values: [...NotificationManager.history].reverse().map(o => o.notification) + } + + delegate: GridLayout { + id: notif + + Layout.fillWidth: true + Layout.preferredHeight: notifSummary.contentHeight + (notifBody.visible ? notifBody.contentHeight + 8 : 0) + (notifSep.visible ? notifSep.height + 8 : 0) + notifTime.contentHeight + 8 + + required property var modelData + required property int index + + columnSpacing: 8 + rowSpacing: 8 + + columns: notifImage.visible ? 2 : 1 + rows: { + var res = 2; + if (notifBody.visible) + res += 1; + if (notifSep.visible) + res += 1; + return res; + } + + Shape { + id: notifSep + + visible: notif.index != 0 + + height: 2 + width: 400 - 32 + + ShapePath { + strokeWidth: 2 + strokeColor: "#20ffffff" + startX: 0; startY: 0; + PathLine { x: 400 - 32; y: 0; } + } + + Layout.row: 0 + Layout.column: 0 + Layout.columnSpan: notifImage.visible ? 2 : 1 + Layout.alignment: Qt.AlignHCenter + } + + Text { + id: notifSummary + + text: notif.modelData?.summary ?? "" + + font.pointSize: 10 + font.family: "Fira Sans" + font.italic: true + color: "white" + maximumLineCount: 1 + elide: Text.ElideRight + + Layout.fillWidth: true + Layout.row: notifSep.visible ? 1 : 0 + Layout.column: notifImage.visible ? 1 : 0 + } + + Image { + id: notifImage + + visible: (notif.modelData?.image || notif.modelData?.appIcon) ?? false + + onStatusChanged: { + if (notifImage.status == Image.Error) + notifImage.visible = false; + } + + source: (notif.modelData?.image || notif.modelData?.appIcon) ?? "" + fillMode: Image.PreserveAspectFit + asynchronous: true + smooth: true + mipmap: true + + Layout.maximumWidth: 50 + Layout.column: 0 + Layout.row: notifSep.visible ? 1 : 0 + Layout.fillHeight: true + Layout.rowSpan: notifBody.visible ? 3 : 2 + } + + Text { + id: notifBody + + visible: notif.modelData?.body ?? false + text: notif.modelData?.body ?? "" + textFormat: Text.RichText + wrapMode: Text.Wrap + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + Layout.fillWidth: true + Layout.row: notifSep.visible ? 2 : 1 + Layout.column: notifImage.visible ? 1 : 0 + } + + Text { + id: notifTime + + Connections { + target: NotificationManager.clock + function onDateChanged() { + notifTime.text = NotificationManager.formatTime(notif.modelData?.receivedTime); + } + } + + text: NotificationManager.formatTime(notif.modelData?.receivedTime) + + font.pointSize: 8 + font.family: "Fira Sans" + font.italic: true + color: "#555" + maximumLineCount: 1 + horizontalAlignment: Text.AlignRight + + Layout.fillWidth: true + Layout.row: { + var res = 1; + if (notifSep.visible) + res += 1; + if (notifBody.visible) + res += 1; + return res; + } + Layout.column: notifImage.visible ? 1 : 0 + } + } + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/PipewireWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/PipewireWidget.qml new file mode 100644 index 00000000..9c6b65a4 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/PipewireWidget.qml @@ -0,0 +1,483 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Fusion +import Quickshell +import Quickshell.Services.Pipewire +import Quickshell.Widgets + +Item { + height: parent.height + width: volumeIcon.width + 8 + anchors.verticalCenter: parent.verticalCenter + + PwObjectTracker { + objects: [Pipewire.defaultAudioSink] + } + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (!Pipewire.defaultAudioSink) + return; + Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted; + } + + property real sensitivity: (1 / 40) / 120 + onWheel: event => { + if (!Pipewire.defaultAudioSink) + return; + Pipewire.defaultAudioSink.audio.volume += event.angleDelta.y * sensitivity; + } + + Rectangle { + id: volumeWidget + + anchors.fill: parent + color: { + if (widgetMouseArea.containsMouse) + return "#33808080"; + return "transparent"; + } + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: volumeIcon + + implicitSize: 14 + anchors.centerIn: parent + + icon: { + if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted) + return "volume-off"; + if (Pipewire.defaultAudioSink.audio.volume <= 0.33) + return "volume-low"; + if (Pipewire.defaultAudioSink.audio.volume <= 0.67) + return "volume-medium"; + return "volume-high"; + } + color: "#555" + } + } + } + } + + Loader { + id: tooltipLoader + + active: false + + Connections { + target: widgetMouseArea + function onContainsMouseChanged() { + if (widgetMouseArea.containsMouse) + tooltipLoader.active = true; + } + } + + PwObjectTracker { + objects: Pipewire.devices + } + PwObjectTracker { + objects: Pipewire.nodes + } + + sourceComponent: PopupWindow { + id: tooltip + + property bool openPopup: false + property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse || openPopup + + anchor { + item: widgetMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: { + tooltip.visible = tooltip.nextVisible; + if (!tooltip.visible) + tooltipLoader.active = false; + } + } + + implicitWidth: tooltipContent.width + implicitHeight: tooltipContent.height + color: "transparent" + + Rectangle { + width: tooltip.width + height: tooltipLayout.childrenRect.height + 16 + color: "black" + } + + WrapperItem { + id: tooltipContent + + bottomMargin: Math.max(0, 200 - tooltipLayout.implicitHeight) + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + WrapperItem { + margin: 8 + bottomMargin: 8 + + GridLayout { + id: tooltipLayout + + columns: 4 + + Repeater { + model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device") + + Item { + id: descItem + + required property var modelData + required property int index + + Layout.column: 0 + Layout.row: index + + implicitWidth: descText.contentWidth + implicitHeight: descText.contentHeight + + Text { + id: descText + + color: "white" + font.pointSize: 10 + font.family: "Fira Sans" + + text: descItem.modelData.description + } + } + } + + Repeater { + id: defaultSinkRepeater + + model: { + Array.from(Pipewire.devices.values) + .filter(dev => dev.type == "Audio/Device") + .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSink && node.device?.id == device.id )); + } + + Item { + id: defaultSinkItem + + required property var modelData + required property int index + + visible: Boolean(modelData) + + PwObjectTracker { + objects: [defaultSinkItem.modelData] + } + + Layout.column: 1 + Layout.row: index + + Layout.fillHeight: true + + implicitWidth: 16 + 8 + + WrapperMouseArea { + id: defaultSinkMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + Pipewire.preferredDefaultAudioSink = defaultSinkItem.modelData + } + + onWheel: event => scrollVolume(event); + property real sensitivity: (1 / 40) / 120 + function scrollVolume(event) { + defaultSinkItem.modelData.audio.volume += event.angleDelta.y * sensitivity; + } + + Rectangle { + id: defaultSinkWidget + + anchors.fill: parent + color: { + if (defaultSinkMouseArea.containsMouse) + return "#33808080"; + return "transparent"; + } + + MaterialDesignIcon { + width: 16 + height: 16 + anchors.centerIn: parent + + icon: { + if (defaultSinkItem.modelData?.id == Pipewire.defaultAudioSink?.id) + return "speaker"; + return "speaker-off"; + } + color: icon == "speaker" ? "white" : "#555" + } + } + } + + PopupWindow { + id: volumeTooltip + + property bool nextVisible: defaultSinkMouseArea.containsMouse || volumeTooltipMouseArea.containsMouse + + anchor { + item: defaultSinkMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: volumeHangTimer.restart() + + onVisibleChanged: tooltip.openPopup = volumeTooltip.visible + + Timer { + id: volumeHangTimer + interval: 100 + onTriggered: volumeTooltip.visible = volumeTooltip.nextVisible + } + + implicitWidth: volumeTooltipText.contentWidth + 16 + implicitHeight: volumeTooltipText.contentHeight + 16 + color: "black" + + WrapperMouseArea { + id: volumeTooltipMouseArea + + hoverEnabled: true + enabled: true + + onWheel: event => defaultSinkMouseArea.scrollVolume(event); + + anchors.fill: parent + + Item { + anchors.fill: parent + + Text { + id: volumeTooltipText + + anchors.centerIn: parent + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + text: `${Math.round(defaultSinkItem.modelData?.audio?.volume * 100)}%` + } + } + } + } + } + } + + Repeater { + id: defaultSourceRepeater + + model: { + Array.from(Pipewire.devices.values) + .filter(dev => dev.type == "Audio/Device") + .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSource && node.device?.id == device.id )); + } + + Item { + id: defaultSourceItem + + required property var modelData + required property int index + + visible: Boolean(modelData) + + PwObjectTracker { + objects: [defaultSourceItem.modelData] + } + + Layout.column: 2 + Layout.row: index + + Layout.fillHeight: true + + implicitWidth: 16 + 8 + + WrapperMouseArea { + id: defaultSourceMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + Pipewire.preferredDefaultAudioSource = defaultSourceItem.modelData + } + + onWheel: event => scrollVolume(event); + property real sensitivity: (1 / 40) / 120 + function scrollVolume(event) { + defaultSourceItem.modelData.audio.volume += event.angleDelta.y * sensitivity; + } + + Rectangle { + id: defaultSourceWidget + + anchors.fill: parent + color: { + if (defaultSourceMouseArea.containsMouse) + return "#33808080"; + return "transparent"; + } + + MaterialDesignIcon { + width: 16 + height: 16 + anchors.centerIn: parent + + icon: { + if (defaultSourceItem.modelData?.id == Pipewire.defaultAudioSource?.id) + return "microphone"; + return "microphone-off"; + } + color: icon == "microphone" ? "white" : "#555" + } + } + } + + PopupWindow { + id: volumeTooltip + + property bool nextVisible: defaultSourceMouseArea.containsMouse || volumeTooltipMouseArea.containsMouse + + anchor { + item: defaultSourceMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: volumeHangTimer.restart() + + onVisibleChanged: tooltip.openPopup = volumeTooltip.visible + + Timer { + id: volumeHangTimer + interval: 100 + onTriggered: volumeTooltip.visible = volumeTooltip.nextVisible + } + + implicitWidth: volumeTooltipText.contentWidth + 16 + implicitHeight: volumeTooltipText.contentHeight + 16 + color: "black" + + WrapperMouseArea { + id: volumeTooltipMouseArea + + hoverEnabled: true + enabled: true + + onWheel: event => defaultSourceMouseArea.scrollVolume(event); + + anchors.fill: parent + + Item { + anchors.fill: parent + + Text { + id: volumeTooltipText + + anchors.centerIn: parent + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + text: `${Math.round(defaultSourceItem.modelData?.audio?.volume * 100)}%` + } + } + } + } + } + } + + Repeater { + id: profileRepeater + + model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device") + + Item { + id: profileItem + + required property var modelData + required property int index + + PwObjectTracker { + objects: [profileItem.modelData] + } + + Layout.column: 3 + Layout.row: index + + Layout.fillWidth: true + + implicitWidth: Math.max(profileBox.implicitWidth, 300) + implicitHeight: profileBox.height + + ComboBox { + id: profileBox + + model: profileItem.modelData.profiles + + textRole: "description" + valueRole: "index" + onActivated: profileItem.modelData.setProfile(currentValue) + + anchors.fill: parent + + implicitContentWidthPolicy: ComboBox.WidestText + + Connections { + target: profileItem.modelData + function onCurrentProfileChanged() { + profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile); + } + } + Component.onCompleted: { + profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile); + } + + Connections { + target: profileBox.popup + function onVisibleChanged() { + tooltip.openPopup = profileBox.popup.visible + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/PrivacyWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/PrivacyWidget.qml new file mode 100644 index 00000000..d7ffadfe --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/PrivacyWidget.qml @@ -0,0 +1,49 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import qs.Services + +Item { + height: parent.height + width: layout.childrenRect.width + anchors.verticalCenter: parent.verticalCenter + + visible: Array.from(Privacy.activeItems).length > 0 + + RowLayout { + id: layout + + anchors.fill: parent + + spacing: 8 + + Repeater { + model: Privacy.activeItems + + Item { + id: privacyItem + + required property var modelData; + + height: parent.height + width: icon.width + + MaterialDesignIcon { + id: icon + + implicitSize: 14 + anchors.centerIn: parent + + icon: { + if (privacyItem.modelData == Privacy.Item.Microphone) + return "microphone"; + if (privacyItem.modelData == Privacy.Item.Screensharing) + return "monitor-share"; + } + color: "#f2201f" + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/Brightness.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/Brightness.qml new file mode 100644 index 00000000..f3ae6804 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/Brightness.qml @@ -0,0 +1,107 @@ +pragma Singleton + +import QtQml +import Quickshell +import Quickshell.Io +import Custom as Custom +import qs.Services + +Singleton { + id: root + + property string subsystem: "backlight" + property string device: "intel_backlight" + + property real currBrightness + property real exponent: 4 + + function calcCurrBrightness() { + if (!currFile.loaded || !maxFile.loaded) + return undefined; + const curr = Number(currFile.text()); + const max = Number(maxFile.text()); + const val = Math.pow(curr / max, 1 / root.exponent); + return val; + } + + Connections { + target: currFile + function onLoaded() { + const b = root.calcCurrBrightness(); + if (typeof b !== 'undefined') + root.currBrightness = b; + } + } + Connections { + target: maxFile + function onLoaded() { + const b = root.calcCurrBrightness(); + if (typeof b !== 'undefined') + root.currBrightness = b; + } + } + + onCurrBrightnessChanged: { + root.currBrightness = Math.max(0, Math.min(1, root.currBrightness)); + + const prev = root.calcCurrBrightness(); + if (typeof prev === 'undefined' || Math.abs(root.currBrightness - prev) < 0.01) + return; + + const max = Number(maxFile.text()); + const actual = Number(currFile.text()); + let curr = Math.max(0, Math.min(max, Math.pow(root.currBrightness, root.exponent) * max)); + if (Math.round(curr) == actual && curr < actual) + curr = Math.max(0, actual - 1); + else if (Math.round(curr) == actual && curr > actual) + curr = Math.min(max, actual + 1); + // root.currBrightness = Math.pow(curr / max, 1 / root.exponent); + Custom.Systemd.setBrightness(root.subsystem, root.device, Math.round(curr)); + } + + FileView { + id: currFile + path: `/sys/class/${root.subsystem}/${root.device}/brightness` + blockAllReads: true + watchChanges: true + onFileChanged: reload() + } + FileView { + id: maxFile + path: `/sys/class/${root.subsystem}/${root.device}/max_brightness` + blockAllReads: true + watchChanges: true + onFileChanged: reload() + } + + + Timer { + id: startupDelay + interval: 500 + running: true + repeat: false + } + property bool onlyInternal: true + Connections { + target: NiriService + function onOutputsChanged() { + root.onlyInternal = Object.keys(NiriService.outputs).every(oname => oname == "eDP-1"); + } + } + onOnlyInternalChanged: { + if (startupDelay.running) + return; + + if (!root.onlyInternal) + root.currBrightness = 1 + } + + + IpcHandler { + target: "Brightness" + + function set(brightness: real): void { + root.currBrightness = brightness; + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/GpgAgent.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/GpgAgent.qml new file mode 100644 index 00000000..3de69535 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/GpgAgent.qml @@ -0,0 +1,18 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + Socket { + id: agentSocket + connected: true + path: `${Quickshell.env("XDG_RUNTIME_DIR")}/gnupg/S.gpg-agent` + } + + function reloadAgent() { + agentSocket.write("RELOADAGENT\n") + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/InhibitorState.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/InhibitorState.qml new file mode 100644 index 00000000..60202a29 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/InhibitorState.qml @@ -0,0 +1,23 @@ +pragma Singleton + +import Quickshell +import Custom as Custom + +Singleton { + id: inhibitorState + + property bool waylandIdleInhibited: false + property alias lidSwitchInhibited: lidSwitchInhibitor.enabled + property bool lockscreenInhibited: false + + Custom.SystemdInhibitor { + id: lidSwitchInhibitor + + enabled: false + + what: Custom.SystemdInhibitorParams.HandleLidSwitch + who: "quickshell" + why: "User request" + mode: Custom.SystemdInhibitorParams.BlockWeak + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/MprisProxy.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/MprisProxy.qml new file mode 100644 index 00000000..e3ab9755 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/MprisProxy.qml @@ -0,0 +1,8 @@ +pragma Singleton + +import Quickshell +import Quickshell.Services.Mpris + +Scope { + property list players: Mpris.players.values +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/NiriService.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/NiriService.qml new file mode 100644 index 00000000..e9e86f95 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/NiriService.qml @@ -0,0 +1,217 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + property var workspaces: [] + property var outputs: {} + property var keyboardLayouts: {} + property var windows: [] + property var casts: [] + readonly property string socketPath: Quickshell.env("NIRI_SOCKET") + + function refreshOutputs() { + commandSocket.sendCommand("Outputs", data => { + outputs = data.Ok.Outputs; + }); + } + + function sendCommand(command, callback) { + commandSocket.sendCommand(command, callback); + } + + Socket { + id: eventStreamSocket + path: root.socketPath + connected: true + + property bool acked: false + + onConnectionStateChanged: { + if (connected) { + acked = false; + write('"EventStream"\n'); + } + } + + parser: SplitParser { + onRead: line => { + try { + const event = JSON.parse(line) + + // console.log(JSON.stringify(event)) + + if (event.WorkspacesChanged) { + root.workspaces = event.WorkspacesChanged.workspaces + root.refreshOutputs(); + } else if (event.WorkspaceActivated) + eventWorkspaceActivated(event.WorkspaceActivated); + else if (event.WorkspaceUrgencyChanged) + eventWorkspaceUrgencyChanged(event.WorkspaceUrgencyChanged); + else if (event.WorkspaceActiveWindowChanged) + eventWorkspaceActiveWindowChanged(event.WorkspaceActiveWindowChanged); + else if (event.KeyboardLayoutsChanged) + root.keyboardLayouts = event.KeyboardLayoutsChanged.keyboard_layouts; + else if (event.KeyboardLayoutSwitched) + root.keyboardLayouts = Object.assign({}, root.keyboardLayouts, {"current_idx": event.KeyboardLayoutSwitched.idx }); + else if (event.WindowsChanged) + root.windows = event.WindowsChanged.windows + else if (event.WindowOpenedOrChanged) + eventWindowOpenedOrChanged(event.WindowOpenedOrChanged); + else if (event.WindowClosed) + eventWindowClosed(event.WindowClosed); + else if (event.WindowFocusChanged) + eventWindowFocusChanged(event.WindowFocusChanged); + else if (event.WindowUrgencyChanged) + eventWindowUrgencyChanged(event.WindowUrgencyChanged); + else if (event.WindowLayoutsChanged) + eventWindowLayoutsChanged(event.WindowLayoutsChanged); + else if (event.WindowFocusTimestampChanged) + eventWindowFocusTimestampChanged(event.WindowFocusTimestampChanged); + else if (event.CastsChanged) + root.casts = event.CastsChanged.casts + else if (event.CastStartedOrChanged) + eventCastStartedOrChanged(event.CastStartedOrChanged); + else if (event.CastStopped) + eventCastStopped(event.CastStopped); + else if (event.Ok && !eventStreamSocket.acked) { eventStreamSocket.acked = true; } + else if (event.OverviewOpenedOrClosed) {} + else if (event.ScreenshotCaptured) {} + else if (event.ConfigLoaded) {} + else + console.log(JSON.stringify(event)); + } catch (e) { + console.warn("NiriService: Failed to parse event:", line, e) + } + } + } + } + + Socket { + id: commandSocket + path: root.socketPath + connected: true + + property var awaitingAnswer: null + property var cmdQueue: [] + + parser: SplitParser { + onRead: line => { + if (commandSocket.awaitingAnswer === null) + return; + + try { + const response = JSON.parse(line); + commandSocket.awaitingAnswer.callback(response); + commandSocket.awaitingAnswer = null; + } catch (e) { + console.warn("NiriService: Failed to parse response:", line, e) + } + commandSocket._handleQueue(); + } + } + + onCmdQueueChanged: { + _handleQueue(); + } + onAwaitingAnswerChanged: { + _handleQueue(); + } + + function _handleQueue() { + if (cmdQueue.length <= 0 || awaitingAnswer !== null) + return; + + let localQueue = Array.from(cmdQueue); + awaitingAnswer = localQueue.shift(); + cmdQueue = localQueue; + write(JSON.stringify(awaitingAnswer.command) + '\n'); + } + + function sendCommand(command, callback) { + cmdQueue = Array.from(cmdQueue).concat([{ "command": command, "callback": callback }]) + } + } + + function eventWorkspaceActivated(data) { + let relevant_output = null; + Array.from(root.workspaces).forEach(ws => { + if (data.id === ws.id) + relevant_output = ws.output; + }); + root.workspaces = Array.from(root.workspaces).map(ws => { + if (data.focused) + ws.is_focused = false; + if (ws.output === relevant_output) + ws.is_active = false; + if (data.id === ws.id) { + ws.is_active = true; + ws.is_focused = data.focused; + } + return ws; + }); + } + function eventWorkspaceUrgencyChanged(data) { + root.workspaces = Array.from(root.workspaces).map(ws => { + if (data.id == ws.id) + ws.is_urgent = data.urgent; + return ws; + }); + } + function eventWorkspaceActiveWindowChanged(data) { + root.workspaces = Array.from(root.workspaces).map(ws => { + if (data.workspace_id === ws.id) + ws.active_window_id = data.active_window_id; + return ws; + }); + } + function eventWindowOpenedOrChanged(data) { + root.windows = Array.from(root.windows).map(win => { + if (data.window.is_focused) + win.is_focused = false; + return win; + }).filter(win => win.id !== data.window.id).concat([data.window]); + } + function eventWindowClosed(data) { + root.windows = Array.from(root.windows).filter(win => win.id !== data.id); + } + function eventWindowFocusChanged(data) { + root.windows = Array.from(root.windows).map(win => { + win.is_focused = win.id === data.id; + return win; + }); + } + function eventWindowUrgencyChanged(data) { + root.windows = Array.from(root.windows).map(win => { + if (win.id === data.id) + win.is_urgent = data.urgent; + return win; + }); + } + function eventWindowLayoutsChanged(data) { + root.windows = Array.from(root.windows).map(win => { + Array.from(data.changes).forEach(change => { + if (win.id === change[0]) + win.layout = change[1]; + }); + return win; + }); + } + function eventWindowFocusTimestampChanged(data) { + root.windows = Array.from(root.windows).map(win => { + if (win.id === data.id) + win.focus_timestamp = data.focus_timestamp; + return win; + }); + } + function eventCastStartedOrChanged(data) { + root.casts = [...Array.from(root.casts).filter(cast => cast.stream_id !== data.cast.stream_id), data.cast]; + } + function eventCastStopped(data) { + root.casts = Array.from(root.casts).filter(cast => cast.stream_id !== data.stream_id); + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/NotificationManager.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/NotificationManager.qml new file mode 100644 index 00000000..5f8ff419 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/NotificationManager.qml @@ -0,0 +1,162 @@ +pragma Singleton + +import QtQml +import Quickshell +import Quickshell.Services.Notifications + +Singleton { + id: root + + readonly property bool active: !root.lockscreenActive && !root.displayInhibited + property bool lockscreenActive: false + property bool displayInhibited: false + property alias trackedNotifications: server.trackedNotifications + readonly property var groups: { + function matchesGroupKey(notif, groupKey) { + var matches = true; + for (const prop in groupKey.test) { + if (notif[prop] !== groupKey.test[prop]) { + matches = false; + break; + } + } + return matches; + } + + var groups = new Map(); + var notifs = new Array(); + for (const [ix, notif] of server.trackedNotifications.values.entries()) { + var didGroup = false; + for (const groupKey of root.groupKeys) { + if (!matchesGroupKey(notif, groupKey)) + continue; + + const key = JSON.stringify({ + "key": groupKey, + "values": Object.assign({}, ...(Array.from(groupKey["group-by"]).map(prop => { + var res = {}; + res[prop] = notif[prop]; + return res; + }))) + }); + if (!groups.has(key)) + groups.set(key, new Array()); + groups.get(key).push({ "ix": ix, "notif": notif }); + didGroup = true; + break; + } + + if (!didGroup) + notifs.push([{ "ix": ix, "notif": notif }]); + } + notifs.push(...groups.values()); + notifs.sort((as, bs) => Math.min(...(as.map(o => o.ix))) - Math.min(...(bs.map(o => o.ix)))); + return notifs.map(ns => ns.map(n => n.notif)); + } + + property var groupKeys: [ + { "test": { "appName": "Element" }, "group-by": [ "summary" ] } + ]; + + property int historyLimit: 100 + property var history: [] + + Component { + id: expirationTimer + + QtObject { + id: timer + + required property QtObject parent + required property int expirationTime + + property list data: [ + Timer { + running: root.active && !timer.expired + interval: timer.expirationTime + onTriggered: { + timer.parent.expirationTimer.destroy(); + timer.parent.expirationTimer = null; + timer.parent.expire(); + } + } + ] + } + } + + Component { + id: notificationLock + + RetainableLock {} + } + + readonly property SystemClock clock: SystemClock { + precision: SystemClock.Minutes + } + + function formatTime(time) { + const now = root.clock.date; + const diff = now - time; + const minutes = Math.ceil(diff / 60000); + const hours = Math.floor(minutes / 60); + + if (hours < 1) { + if (minutes < 1) + return "now"; + if (minutes == 1) + return "1 minute"; + return `${minutes} minutes`; + } + + const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()) + const timeDate = new Date(time.getFullYear(), time.getMonth(), time.getDate()) + const days = Math.floor((nowDate - timeDate) / (1000 * 86400)) + + const timeStr = time.toLocaleTimeString(Qt.locale(), "HH:mm"); + + if (days === 0) + return timeStr; + if (days === 1) + return `yesterday ${timeStr}`; + + const dateStr = time.toLocaleDateString(Qt.locale(), "YYYY-MM-DD"); + return `${dateStr} ${timeStr}`; + } + + NotificationServer { + id: server + + bodySupported: true + actionsSupported: true + actionIconsSupported: true + imageSupported: true + bodyMarkupSupported: true + bodyImagesSupported: true + + onNotification: notification => { + var timeout = notification.expireTimeout * 1000; + if (notification.appName == "poweralertd") + timeout = 2000; + if (timeout > 0) { + Object.defineProperty(notification, "expirationTimer", { configurable: true, enumerable: true, writable: true }); + notification.expirationTimer = expirationTimer.createObject(notification, { parent: notification, expirationTime: timeout }); + } + Object.defineProperty(notification, "receivedTime", { configurable: true, enumerable: true, writable: true }); + notification.receivedTime = root.clock.date; + notification.closed.connect((reason) => server.onNotificationClosed(notification, reason)); + notification.tracked = true; + } + + function onNotificationClosed(notification, reason) { + while (root.history.length >= root.historyLimit) { + root.history[0].lock.locked = false; + root.history.shift(); + } + + root.history.push({ + lock: notificationLock.createObject(root, { locked: true, object: notification }), + notification: notification + }); + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/Privacy.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/Privacy.qml new file mode 100644 index 00000000..9c813e49 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/Privacy.qml @@ -0,0 +1,63 @@ +pragma Singleton + +import QtQml +import Quickshell +import Quickshell.Services.Pipewire + +Singleton { + id: root + + PwObjectTracker { + objects: Pipewire.nodes.values + } + + enum Item { + Microphone, + Screensharing + } + + readonly property list activeItems: { + var items = []; + if (microphoneActive) + items.push(Privacy.Item.Microphone); + if (screensharingActive) + items.push(Privacy.Item.Screensharing); + return items; + } + + readonly property bool microphoneActive: { + if (!Pipewire.ready || !Pipewire.nodes?.values) { + return false + } + + for (const node of Pipewire.nodes.values) { + if (!node || (node.type & PwNodeType.AudioInStream) != PwNodeType.AudioInStream) + continue; + + if (node.properties?.["stream.monitor"] === "true") + continue; + + if (node.audio?.muted) + continue; + + return true; + } + + return false; + } + + readonly property bool screensharingActive: { + if (!Pipewire.ready || !Pipewire.nodes?.values) { + return false + } + + for (const node of Pipewire.nodes.values) { + if (!node || (node.type & PwNodeType.VideoInStream) != PwNodeType.VideoInStream) + continue; + + return true; + } + + return false; + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/ScreenRecord.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/ScreenRecord.qml new file mode 100644 index 00000000..eb415452 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/ScreenRecord.qml @@ -0,0 +1,63 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io + +Singleton { + id: root + property bool active: false + property bool slurpSuccess: false + + onActiveChanged: { + if (!active) { + slurp.running = false; + screenRecorder.running = false; + } + if (active) + slurp.running = true; + } + + Process { + id: screenRecorder + running: false + onRunningChanged: { + console.log("wf-recorder running: ", screenRecorder.running); + + if (!screenRecorder.running && !slurp.running) + root.active = false; + } + stderr: SplitParser { + onRead: line => console.log("wf-recorder stderr: ", line) + } + stdout: SplitParser { + onRead: line => console.log("wf-recorder stdout: ", line) + } + } + + Process { + id: slurp + running: false + command: [ @slurp@, "-o", "-d" ] + stdout: StdioCollector {} + stderr: SplitParser { + onRead: line => console.log("slurp stderr: ", line) + } + onExited: exitCode => { + if (exitCode !== 0) { + console.log("slurp failed: ", exitCode); + root.active = false; + return; + } + console.log("slurp succeeded: ", slurp.stdout.text); + + const nowDate = new Date(); + + screenRecorder.command = [ + @wf-recorder@, + "-g", slurp.stdout.text, + "-f", `${Quickshell.env("HOME")}/screenshots/${nowDate.toLocaleString(Qt.locale(), "yyyy-MM-ddThh:mm:ss")}.mkv`, + ]; + screenRecorder.running = true; + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/WallpaperSelector.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/WallpaperSelector.qml new file mode 100644 index 00000000..c71a9cca --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/WallpaperSelector.qml @@ -0,0 +1,8 @@ +import Custom as Custom + +Custom.FileSelector { + id: root + + directory: @wallpapers@ + epoch: 79200000 +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/Worktime.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/Worktime.qml new file mode 100644 index 00000000..d98378f1 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/Services/Worktime.qml @@ -0,0 +1,84 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import QtQml + +Singleton { + id: root + + property alias time: timeState + property alias today: todayState + + CommandState { + id: timeState + command: "time" + } + CommandState { + id: todayState + command: "today" + } + + IpcHandler { + target: "Worktime" + + function refresh(): void { + time.running = true; + today.running = true; + } + } + + component CommandState : Scope { + id: commandState + + required property string command + property var state: null + + property bool strikeout: !strikeoutTimer.running + property alias running: process.running + property alias updating: updateTimer.running + + Process { + id: process + running: true + command: [ @worktime@, commandState.command, "--waybar" ] + stdout: StdioCollector { + id: processCollector + onStreamFinished: { + try { + commandState.state = JSON.parse(processCollector.text); + strikeoutTimer.restart(); + } catch (e) { + console.warn("Worktime: Failed to parse output:", processCollector.text, e); + } + } + } + } + + Timer { + id: updateTimer + running: commandState.state?.class == "running" || commandState.state?.class == "over" + interval: 60000 + repeat: true + onTriggered: process.running = true + } + + Timer { + id: strikeoutTimer + running: false + interval: 5 * updateTimer.interval + repeat: false + } + } + + Timer { + running: Boolean(timeState.state) && Boolean(todayState.state) && timeState.strikeout && todayState.strikeout + interval: 1000 + repeat: false + onTriggered: { + timeState.state = null; + todayState.state = null; + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/SystemTray.qml b/accounts/gkleen@skadhi/shell/quickshell/SystemTray.qml new file mode 100644 index 00000000..f0c50632 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/SystemTray.qml @@ -0,0 +1,201 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.SystemTray + +Item { + anchors.verticalCenter: parent.verticalCenter + width: systemTrayRow.childrenRect.width + height: parent.height + clip: true + + Row { + id: systemTrayRow + anchors.centerIn: parent + width: childrenRect.width + height: parent.height + spacing: 0 + + Repeater { + model: ScriptModel { + values: { + var trayItems = Array.from(SystemTray.items.values).filter(item => item.status !== Status.Passive && !(["Fcitx"].includes(item.id))); + trayItems.sort((a, b) => a.category !== b.category ? b.category - a.category : a.id.localeCompare(b.id)) + return trayItems; + } + } + + delegate: Item { + id: trayItemWrapper + + required property var modelData + required property int index + + property var trayItem: modelData + property string iconSource: { + let icon = trayItem && trayItem.icon + if (typeof icon === 'string' || icon instanceof String) { + if (icon.includes("?path=")) { + const split = icon.split("?path=") + if (split.length !== 2) + return icon + const name = split[0] + const path = split[1] + const fileName = name.substring( + name.lastIndexOf("/") + 1) + return `file://${path}/${fileName}` + } + return icon + } + return "" + } + + width: icon.width + 6 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: trayItemArea + + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: true + cursorShape: trayItem.onlyMenu ? Qt.ArrowCursor : Qt.PointingHandCursor + onClicked: mouse => { + if (!trayItem) + return + + if (mouse.button === Qt.LeftButton + && !trayItem.onlyMenu) { + trayItem.activate() + return + } + + if (trayItem.hasMenu) { + var globalPos = mapToGlobal(0, 0) + var currentScreen = screen || Screen + var screenX = currentScreen.x || 0 + var relativeX = globalPos.x - screenX + menuAnchor.menu = trayItem.menu + menuAnchor.anchor.window = bar + menuAnchor.anchor.rect = Qt.rect( + relativeX, + 21, + parent.width, 1) + menuAnchor.open() + } + } + + Rectangle { + anchors.fill: parent + color: { + if (trayItemArea.containsMouse) + return "#33808080"; + return "transparent"; + } + + Item { + anchors.fill: parent + + layer.enabled: true + layer.effect: MultiEffect { + colorization: 1 + colorizationColor: "#555" + } + + IconImage { + id: icon + + anchors.centerIn: parent + implicitSize: 16 + source: trayItemWrapper.iconSource + asynchronous: true + smooth: true + mipmap: true + + layer.enabled: true + layer.effect: MultiEffect { + id: effect + + brightness: 1 + } + } + } + } + } + + PopupWindow { + id: tooltip + + property bool nextVisible: (trayItem.tooltipTitle || trayItem.tooltipDescription) && (trayItemArea.containsMouse || tooltipMouseArea.containsMouse) && !menuAnchor.visible + + anchor { + item: trayItemArea + edges: Edges.Bottom + } + + visible: false + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: tooltip.visible = tooltip.nextVisible + } + + color: "black" + + implicitWidth: Math.max(tooltipTitle.contentWidth, tooltipDescription.contentWidth) + 16 + implicitHeight: (trayItem.tooltipTitle ? tooltipTitle.contentHeight : 0) + (trayItem.tooltipDescription ? tooltipDescription.contentHeight : 0) + 16 + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + margin: 4 + + anchors.fill: parent + + Item { + anchors.fill: parent + + Column { + anchors.centerIn: parent + Text { + id: tooltipTitle + + enabled: trayItem.tooltipTitle + + font.pointSize: 10 + font.family: "Fira Sans" + font.bold: true + color: "white" + + text: trayItem.tooltipTitle + } + + Text { + id: tooltipDescription + + enabled: trayItem.tooltipDescription + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + text: trayItem.tooltipDescription + } + } + } + } + } + } + } + } + QsMenuAnchor { + id: menuAnchor + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/UnixIPC.qml b/accounts/gkleen@skadhi/shell/quickshell/UnixIPC.qml new file mode 100644 index 00000000..3d950031 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/UnixIPC.qml @@ -0,0 +1,106 @@ +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pipewire +import Quickshell.Services.Mpris +import qs.Services +import Custom as Custom +import QtQml + +Scope { + id: root + + SocketServer { + active: true + path: `${Quickshell.env("XDG_RUNTIME_DIR")}/shell.sock` + handler: Socket { + parser: SplitParser { + onRead: line => { + const command = (() => { + try { + return JSON.parse(line); + } catch (e) { + console.warn("UnixIPC: Failed to parse command:", line, e); + } + })(); + if (!command) + return; + + if (command.Volume) + root.onCommandVolume(command.Volume); + else if (command.Brightness) + root.onCommandBrightness(command.Brightness); + else if (command.LockSession) + Custom.Systemd.lockSession(); + else if (command.Suspend) + Custom.Systemd.suspend(); + else if (command.Hibernate) + Custom.Systemd.hibernate(); + else if (command.Mpris) + root.onCommandMpris(command.Mpris); + else if (command.Notifications) + root.onCommandNotifications(command.Notifications); + else if (command.ScreenRecord) + root.onCommandScreenRecord(command.ScreenRecord); + else + console.warn("UnixIPC: Command not handled:", JSON.stringify(command)); + } + } + + onError: e => { + if (e == 1) + return; + console.warn("QLocalSocket::LocalSocketError", e); + } + } + } + + PwObjectTracker { + objects: [ Pipewire.defaultAudioSink, Pipewire.defaultAudioSource ] + } + function onCommandVolume(command) { + if (command.muted === "toggle") + Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted; + if (command.volume === "up") + Pipewire.defaultAudioSink.audio.volume += 0.02; + if (command.volume === "down") + Pipewire.defaultAudioSink.audio.volume -= 0.02; + + if (command["mic-muted"] === "toggle") + Pipewire.defaultAudioSource.audio.muted = !Pipewire.defaultAudioSource.audio.muted; + } + + function onCommandBrightness(command) { + if (command === "up") + Brightness.currBrightness += 0.02 + if (command === "down") + Brightness.currBrightness -= 0.02 + } + + function onCommandMpris(command) { + if (command.PauseAll) + Array.from(MprisProxy.players).forEach(player => { + if (player.canPause && player.isPlaying) + player.pause(); + }); + } + Component.onCompleted: { (_ => {})(MprisProxy.players); } + + function onCommandNotifications(command) { + if (command.DismissGroup && NotificationManager.active) { + if (NotificationManager.groups.length > 0) + for (const notif of [...NotificationManager.groups[0]]) + notif.dismiss(); + } + if (command.DismissAll && NotificationManager.active) { + for (const notif of [...NotificationManager.trackedNotifications.values]) + notif.dismiss(); + } + } + + function onCommandScreenRecord(command) { + if (command.Toggle) + ScreenRecord.active = !ScreenRecord.active; + else + console.warn("UnixIPC.ScreenRecord: Command not handled:", JSON.stringify(command)); + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/VolumeOSD.qml b/accounts/gkleen@skadhi/shell/quickshell/VolumeOSD.qml new file mode 100644 index 00000000..91dc9591 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/VolumeOSD.qml @@ -0,0 +1,163 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Pipewire +import Quickshell.Widgets + +Scope { + id: root + + property string show: "" + property bool inhibited: true + + PwObjectTracker { + objects: [ Pipewire.defaultAudioSink, Pipewire.defaultAudioSource ] + } + + Connections { + enabled: Pipewire.defaultAudioSink + target: Pipewire.defaultAudioSink?.audio || null + + function onVolumeChanged() { + root.show = "sink"; + hideTimer.restart(); + } + function onMutedChanged() { + root.show = "sink"; + hideTimer.restart(); + } + } + + Connections { + enabled: Pipewire.defaultAudioSource + target: Pipewire.defaultAudioSource?.audio || null + + function onVolumeChanged() { + root.show = "source"; + hideTimer.restart(); + } + function onMutedChanged() { + root.show = "source"; + hideTimer.restart(); + } + } + + onShowChanged: { + if (show) + hideTimer.restart(); + } + + Timer { + id: hideTimer + interval: 1000 + onTriggered: root.show = "" + } + + Timer { + id: startInhibit + interval: 100 + running: true + onTriggered: { + root.show = ""; + root.inhibited = false; + } + } + + LazyLoader { + active: root.show && !root.inhibited + + Variants { + model: Quickshell.screens + + delegate: Scope { + id: screenScope + + required property var modelData + + PanelWindow { + id: window + + screen: screenScope.modelData + + anchors.top: true + margins.top: screen.height / 2 - 50 + 3.5 + exclusiveZone: 0 + exclusionMode: ExclusionMode.Ignore + + implicitWidth: 400 + implicitHeight: 50 + + mask: Region {} + + color: "transparent" + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.75) + } + + RowLayout { + id: layout + + anchors.centerIn: parent + + height: 50 - 8*2 + width: 400 - 8*2 + + MaterialDesignIcon { + id: volumeIcon + + implicitWidth: parent.height + implicitHeight: parent.height + + icon: { + if (root.show == "sink") { + if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted) + return "volume-off"; + if (Pipewire.defaultAudioSink.audio.volume <= 0.33) + return "volume-low"; + if (Pipewire.defaultAudioSink.audio.volume <= 0.67) + return "volume-medium"; + return "volume-high"; + } else if (root.show == "source") { + if (!Pipewire.defaultAudioSource || Pipewire.defaultAudioSource.audio.muted) + return "microphone-off"; + if (Pipewire.defaultAudioSource.audio.volume > 1) + return "microphone-plus"; + return "microphone"; + } + return "volume-high"; + } + } + + Rectangle { + Layout.fillWidth: true + + implicitHeight: 10 + + color: "#50ffffff" + + Rectangle { + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + + color: Pipewire.defaultAudioSink?.audio.muted ? "#70ffffff" : "white" + + implicitWidth: { + if (root.show == "sink") + return parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0); + else if (root.show == "source") + return parent.width * Math.min(1, (Pipewire.defaultAudioSource?.audio.volume ?? 0)); + return 0; + } + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/WallpaperBackground.qml b/accounts/gkleen@skadhi/shell/quickshell/WallpaperBackground.qml new file mode 100644 index 00000000..4f85a900 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/WallpaperBackground.qml @@ -0,0 +1,89 @@ +import QtQuick +import Quickshell +import qs.Services + +Item { + id: root + + anchors.fill: parent + + required property string screen + + property Img current: one + property string source: selector.selected + + WallpaperSelector { + id: selector + seed: screen + } + + onSourceChanged: { + if (!source) + current = null; + else if (current === one) + two.update() + else + one.update() + } + + Img { id: one } + Img { id: two } + + component Img: Image { + id: img + + function update() { + source = root.source || "" + } + + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + smooth: true + asynchronous: true + cache: false + + opacity: 0 + + onStatusChanged: { + if (status === Image.Ready) { + root.current = this + } + } + + states: State { + name: "visible" + when: root.current === img + + PropertyChanges { + img.opacity: 1 + } + StateChangeScript { + name: "unloadOther" + script: { + if (img === one) + two.source = "" + if (img === two) + one.source = "" + } + } + } + + transitions: Transition { + SequentialAnimation { + NumberAnimation { + target: img + properties: "opacity" + duration: { + if (img === one && two.source == "" || img === two && one.source == "") + return 0; + return 5000; + } + easing.type: Easing.OutCubic + } + ScriptAction { + scriptName: "unloadOther" + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/WaylandInhibitorWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/WaylandInhibitorWidget.qml new file mode 100644 index 00000000..0512ff51 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/WaylandInhibitorWidget.qml @@ -0,0 +1,56 @@ +import Quickshell +import QtQuick +import Quickshell.Widgets +import Quickshell.Wayland +import qs.Services + +Item { + id: root + + required property var window + + width: icon.width + 8 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + IdleInhibitor { + id: inhibitor + enabled: InhibitorState.waylandIdleInhibited + window: root.window + } + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: InhibitorState.waylandIdleInhibited = !InhibitorState.waylandIdleInhibited + + Rectangle { + anchors.fill: parent + color: { + if (widgetMouseArea.containsMouse) { + return "#33808080"; + } + return "transparent"; + } + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: icon + + implicitSize: 14 + anchors.centerIn: parent + + icon: inhibitor.enabled ? "eye" : "eye-off" + color: inhibitor.enabled ? "white" : "#555" + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/WorkspaceSwitcher.qml b/accounts/gkleen@skadhi/shell/quickshell/WorkspaceSwitcher.qml new file mode 100644 index 00000000..a893937a --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/WorkspaceSwitcher.qml @@ -0,0 +1,204 @@ +import Quickshell +import QtQuick +import qs.Services +import Quickshell.Widgets +import QtQuick.Layouts + +Row { + id: workspaces + + required property var screen + + property var ignoreWorkspaces: @ignore_workspaces@ + + height: parent.height + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + Repeater { + model: ScriptModel { + values: { + let currWorkspaces = NiriService.workspaces; + const ignoreWorkspaces = Array.from(workspaces.ignoreWorkspaces); + currWorkspaces = currWorkspaces.filter(ws => ws.output == workspaces.screen?.name).filter(ws => ws.is_active || ignoreWorkspaces.every(iws => iws !== ws.name)); + currWorkspaces.sort((a, b) => { + if (NiriService.outputs?.[a.output]?.logical?.x !== NiriService.outputs?.[b.output]?.logical?.x) + return NiriService.outputs?.[a.output]?.logical?.x - NiriService.outputs?.[b.output]?.logical?.x + if (NiriService.outputs?.[a.output]?.logical?.y !== NiriService.outputs?.[b.output]?.logical?.y) + return NiriService.outputs?.[a.output]?.logical?.y - NiriService.outputs?.[b.output]?.logical?.y + return a.idx - b.idx; + }); + return currWorkspaces.map(o => Object.assign({}, o)); + } + } + + Item { + id: wsItem + + property var workspaceData: modelData + + width: wsLabel.contentWidth + 8 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + WrapperMouseArea { + id: mouseArea + + anchors.fill: parent + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: true + onClicked: { + NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {}); + } + + Rectangle { + anchors.fill: parent + + color: { + if (mouseArea.containsMouse) { + return "#33808080"; + } + return "transparent"; + } + + Text { + id: wsLabel + + anchors.centerIn: parent + + font.pointSize: 10 + font.family: "Fira Sans" + color: { + if (workspaceData.is_active) + return "#23fd00"; + if (workspaceData.active_window_id === null) + return "#555"; + return "white"; + } + + text: workspaceData.name ? workspaceData.name : workspaceData.idx + } + } + } + + PopupWindow { + id: tooltip + + property bool nextVisible: (mouseArea.containsMouse || tooltipMouseArea.containsMouse) && [...windowsModel.values].length > 0 + + anchor { + item: mouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: tooltip.visible = tooltip.nextVisible + } + + implicitWidth: tooltipContent.implicitWidth + implicitHeight: tooltipContent.implicitHeight + color: "black" + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + anchors.fill: parent + + WrapperItem { + id: tooltipContent + + margin: 0 + + ColumnLayout { + spacing: 0 + + Repeater { + model: ScriptModel { + id: windowsModel + + values: { + let currWindows = NiriService.windows.filter(win => win.workspace_id == wsItem.workspaceData.id); + currWindows.sort((a, b) => { + if (a.is_floating !== b.is_floating) + return b.is_floating - a.is_floating; + if (a.layout.tile_pos_in_workspace_view?.[0] !== b.layout.tile_pos_in_workspace_view?.[0]) + return a.layout.tile_pos_in_workspace_view?.[0] - b.layout.tile_pos_in_workspace_view?.[0] + if (a.layout.tile_pos_in_workspace_view?.[1] !== b.layout.tile_pos_in_workspace_view?.[1]) + return a.layout.tile_pos_in_workspace_view?.[1] - b.layout.tile_pos_in_workspace_view?.[1] + if (a.layout.pos_in_scrolling_layout?.[0] !== b.layout.pos_in_scrolling_layout?.[0]) + return a.layout.pos_in_scrolling_layout?.[0] - b.layout.pos_in_scrolling_layout?.[0] + if (a.layout.pos_in_scrolling_layout?.[1] !== b.layout.pos_in_scrolling_layout?.[1]) + return a.layout.pos_in_scrolling_layout?.[1] - b.layout.pos_in_scrolling_layout?.[1] + if (a.app_id !== b.app_id) + return a.app_id?.localeCompare(b.app_id); + + return a.title.localeCompare(b.title); + }); + return currWindows.map(o => Object.assign({}, o)); + } + } + + WrapperMouseArea { + id: windowMouseArea + + required property int index + required property var modelData + property var windowData: modelData + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: true + + Layout.fillWidth: true + + onClicked: { + NiriService.sendCommand({ "Action": { "FocusWindow": { "id": windowData.id } } }, _ => {}) + } + + WrapperRectangle { + color: windowMouseArea.containsMouse ? "#33808080" : "transparent"; + + WrapperItem { + rightMargin: 8 + leftMargin: 8 + topMargin: windowMouseArea.index == 0 ? 8 : 4 + bottomMargin: windowMouseArea.index == windowsModel.values.length - 1 ? 8 : 4 + + Text { + id: windowLabel + + font.pointSize: 10 + font.family: "Fira Sans" + color: { + if (windowData.is_focused) + return "#23fd00"; + if (NiriService.workspaces.find(ws => ws.id == windowData.workspace_id)?.active_window_id == windowData.id) + return "white"; + return "#555"; + } + + text: windowData.title + + horizontalAlignment: Text.AlignLeft + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/WorktimeWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/WorktimeWidget.qml new file mode 100644 index 00000000..b7baee34 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/WorktimeWidget.qml @@ -0,0 +1,212 @@ +import QtQml +import Quickshell +import QtQuick +import Quickshell.Widgets +import qs.Services + +Item { + id: root + + height: parent.height + width: (timeWidget.visible ? timeWidget.label.contentWidth + 8 : 0) + (todayWidget.visible ? todayWidget.label.contentWidth + 8 : 0) + (icon.visible ? icon.implicitWidth + 8 : 0) + anchors.verticalCenter: parent.verticalCenter + + component TextWidget : Item { + id: textWidget + + visible: textWidget.state.state?.text ?? false + + required property var state + property alias label: label + property alias mouseArea: mouseArea + + anchors.verticalCenter: parent.verticalCenter + implicitWidth: label.contentWidth + 8 + height: parent.height + + WrapperMouseArea { + id: mouseArea + + anchors.fill: parent + + enabled: true + hoverEnabled: true + acceptedButtons: Qt.NoButton + cursorShape: Qt.PointingHandCursor + + Item { + anchors.fill: parent + + Text { + id: label + + anchors.centerIn: parent + + text: textWidget.state.state?.text ?? "" + + font.pointSize: 10 + font.family: "Fira Sans" + font.strikeout: textWidget.state.strikeout + color: { + if (textWidget.state.state?.class == "running") + return "white"; + if (textWidget.state.state?.class == "over") + return "#f28a21"; + return "#555"; + } + } + } + } + } + + WrapperMouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: true + + cursorShape: Qt.PointingHandCursor + onClicked: { + Worktime.time.running = true; + Worktime.today.running = true; + } + + Rectangle { + anchors.fill: parent + color: { + if (mouseArea.containsMouse) + return "#33808080"; + return "transparent"; + } + + Row { + height: parent.height + anchors.centerIn: parent + spacing: 0 + + TextWidget { + id: timeWidget + state: Worktime.time + } + + TextWidget { + id: todayWidget + state: Worktime.today + } + + MaterialDesignIcon { + id: icon + + anchors.verticalCenter: parent.verticalCenter + + implicitSize: 14 + + visible: !timeWidget.visible && !todayWidget.visible + + icon: (Worktime.time.running || Worktime.today.running) ? "update" : "timer-off" + color: "#555" + } + } + } + } + + component WorktimePopup : PopupWindow { + id: tooltip + + required property var state + required property var mouseArea + + property bool iconVisible: tooltip.state.running || !tooltip.state.updating + property bool tooltipVisible: tooltip.state.state?.tooltip ?? false + + property bool nextVisible: (tooltip.tooltipVisible || tooltip.iconVisible) && (tooltip.mouseArea.containsMouse || tooltipMouseArea.containsMouse) + + anchor { + item: tooltip.mouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: tooltip.visible = tooltip.nextVisible + } + + implicitWidth: (tooltipIcon.visible ? tooltipIcon.implicitWidth : 0) + (tooltipIcon.visible && tooltipText.visible ? 8 : 0) + (tooltipText.visible ? tooltipText.implicitWidth : 0) + 16 + implicitHeight: tooltipText.implicitHeight + 16 + color: "black" + + WrapperMouseArea { + id: tooltipMouseArea + + enabled: true + hoverEnabled: true + + anchors.fill: parent + + Item { + anchors.fill: parent + + Row { + id: tooltipLayout + + anchors { + left: parent.left + top: parent.top + leftMargin: 8 + topMargin: 8 + verticalCenter: parent.verticalCenter + } + + height: parent.height + width: childrenRect.width + + spacing: 0 + + MaterialDesignIcon { + id: tooltipIcon + + implicitSize: 14 + anchors.verticalCenter: parent.verticalCenter + + visible: tooltip.iconVisible + + icon: tooltip.state.running ? "update" : "timer-off" + } + + Item { + visible: tooltipIcon.visible && tooltipText.visible + height: parent.height + width: 8 + } + + Text { + id: tooltipText + + visible: tooltip.tooltipVisible + + anchors.verticalCenter: parent.verticalCenter + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + text: tooltip.state.state?.tooltip ?? "" + } + } + } + } + } + + WorktimePopup { + state: Worktime.time + mouseArea: timeWidget.mouseArea + } + WorktimePopup { + state: Worktime.today + mouseArea: todayWidget.mouseArea + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/YtDlpWidget.qml b/accounts/gkleen@skadhi/shell/quickshell/YtDlpWidget.qml new file mode 100644 index 00000000..ff338d49 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/YtDlpWidget.qml @@ -0,0 +1,190 @@ +import QtQml.Models +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Material +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import Custom as Custom + +Item { + id: root + + height: parent.height + width: icon.width + anchors.verticalCenter: parent.verticalCenter + + visible: Boolean(unitsModel.values.length) + + WrapperMouseArea { + id: widgetMouseArea + + anchors.fill: parent + hoverEnabled: true + + Item { + anchors.fill: parent + + MaterialDesignIcon { + id: icon + + implicitSize: 14 + anchors.centerIn: parent + + icon: "tray-arrow-down" + color: "white" + } + } + } + + PopupWindow { + id: tooltip + + property bool openPopup: false + property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse || openPopup + + anchor { + item: widgetMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: 100 + onTriggered: tooltip.visible = tooltip.nextVisible + } + + implicitWidth: tooltipContent.width + implicitHeight: tooltipContent.height + color: "transparent" + + Rectangle { + width: tooltip.width + height: tooltipLayout.childrenRect.height + 16 + color: "black" + } + + WrapperItem { + id: tooltipContent + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + WrapperItem { + margin: 8 + + child: GridLayout { + id: tooltipLayout + + columns: 2 + } + + Repeater { + model: ScriptModel { + id: unitsModel + + values: [...Custom.Systemd.userUnits.values].filter(unit => unit.id.match(/^yt-dlp@.+\.service$/) && unit.activeState === "active" && unit.runtimeDirectory) + } + + Item { + id: unitDelegate + + Component.onCompleted: { + while (children.length) { children[0].parent = tooltipLayout; } + } + + required property var modelData + required property int index + + property var state: null + + Socket { + id: socket + + path: Quickshell.env("XDG_RUNTIME_DIR") + "/" + unitDelegate.modelData.runtimeDirectory[0] + "/status.sock" + connected: true + onError: e => { + console.warn("YtDlpWidget: Error in Socket:", socket.path, e); + } + parser: SplitParser { + onRead: line => { + try { + unitDelegate.state = JSON.parse(line) + } catch (e) { + console.warn("YtDlpWidget: Failed to parse state:", line, e) + } + } + } + } + + WrapperItem { + Layout.column: 0 + Layout.row: unitDelegate.index + + implicitWidth: descText.contentWidth + implicitHeight: descText.contentHeight + + Text { + id: descText + + color: "white" + font.pointSize: 10 + font.family: "Fira Sans" + + text: unitDelegate.state?.title || unitDelegate.modelData.id + } + } + + /* + WrapperItem { + Layout.column: 1 + Layout.row: unitDelegate.index + + implicitWidth: fragText.contentWidth + implicitHeight: fragText.contentHeight + + visible: Boolean(unitDelegate.state) + + Text { + id: fragText + + color: "white" + font.pointSize: 10 + font.family: "Fira Sans" + + text: `${unitDelegate.state?.fragment_index}/${unitDelegate.state?.fragment_count}` + } + } + */ + + WrapperItem { + Layout.column: 1 + Layout.row: unitDelegate.index + + visible: Boolean(unitDelegate.state) + + ProgressBar { + id: progress + + Material.background: "black" + Material.accent: "white" + + from: 0 + to: unitDelegate.state?.total_bytes || unitDelegate.state?.total_bytes_estimate || 0 + value: unitDelegate.state?.downloaded_bytes || 0 + indeterminate: !Boolean(unitDelegate.state?.total_bytes || unitDelegate.state?.total_bytes_estimate) || unitDelegate.state?.status !== "downloading" + } + } + } + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/displaymanager.qml b/accounts/gkleen@skadhi/shell/quickshell/displaymanager.qml new file mode 100644 index 00000000..b452c03d --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/displaymanager.qml @@ -0,0 +1,115 @@ +//@ pragma UseQApplication + +import Quickshell +import Quickshell.Wayland +import Quickshell.Io +import Quickshell.Services.Greetd +import QtQml + + +ShellRoot { + id: displaymanager + + settings.watchFiles: false + + property string currentText: "" + property string username: @username@ + property list command: @niri_session@ + property list messages: [] + property bool responseRequired: false + property bool responseVisible: false + + signal startAuth() + + onStartAuth: { + if (Greetd.state !== GreetdState.Inactive) + Greetd.cancelSession(); + displaymanager.messages = []; + Greetd.createSession(displaymanager.username); + } + + Connections { + target: Greetd + function onStateChanged() { + console.log("greetd state: ", GreetdState.toString(Greetd.state)); + if (Greetd.state === GreetdState.ReadyToLaunch) + Greetd.launch(displaymanager.command); + } + function onAuthMessage(message: string, error: bool, responseRequired: bool, echoResponse: bool) { + displaymanager.responseVisible = echoResponse; + displaymanager.responseRequired = responseRequired; + displaymanager.messages = Array.from(displaymanager.messages).concat([{ "text": message, "error": error }]); + } + function onAuthFailure(message: string) { + displaymanager.responseRequired = false; + displaymanager.messages = Array.from(displaymanager.messages).concat([{ "text": message, "error": true }]); + } + } + + Component.onCompleted: { + if (Greetd.state !== GreetdState.Inactive) + Greetd.cancelSession(); + } + + Variants { + model: Quickshell.screens + + delegate: Scope { + id: screenScope + + required property var modelData + + PanelWindow { + color: "black" + + screen: screenScope.modelData + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + + anchors.top: true + anchors.bottom: true + anchors.left: true + anchors.right: true + + LockSurface { + id: surfaceContent + + screen: screenScope.modelData + + onCurrentTextChanged: displaymanager.currentText = currentText + Connections { + target: displaymanager + function onCurrentTextChanged() { surfaceContent.currentText = displaymanager.currentText; } + function onMessagesChanged() { surfaceContent.messages = Array.from(displaymanager.messages); } + function onResponseRequiredChanged() { surfaceContent.responseRequired = displaymanager.responseRequired; } + function onResponseVisibleChanged() { surfaceContent.responseVisible = displaymanager.responseVisible; } + } + + onResponse: responseText => Greetd.respond(responseText); + Connections { + target: Greetd + function onStateChanged() { + if (Greetd.state === GreetdState.Authenticating) { + surfaceContent.authRunning = true; + } else { + surfaceContent.authRunning = false; + } + } + } + + onAuthRunningChanged: { + if (surfaceContent.authRunning && Greetd.state !== GreetdState.Authenticating) + displaymanager.startAuth(); + } + Component.onCompleted: { + surfaceContent.authRunning = Greetd.state === GreetdState.Authenticating + surfaceContent.messages = Array.from(displaymanager.messages); + surfaceContent.responseVisible = displaymanager.responseVisible; + surfaceContent.responseRequired = displaymanager.responseRequired; + surfaceContent.currentText = displaymanager.currentText; + } + } + } + } + } +} diff --git a/accounts/gkleen@skadhi/shell/quickshell/shell.qml b/accounts/gkleen@skadhi/shell/quickshell/shell.qml new file mode 100644 index 00000000..1a5875f0 --- /dev/null +++ b/accounts/gkleen@skadhi/shell/quickshell/shell.qml @@ -0,0 +1,53 @@ +//@ pragma UseQApplication + +import Quickshell +import Quickshell.Wayland + +ShellRoot { + settings.watchFiles: false + + Variants { + model: Quickshell.screens + + delegate: Scope { + id: screenScope + + required property var modelData + + PanelWindow { + id: bgWindow + + screen: screenScope.modelData + + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.namespace: "background" + exclusionMode: ExclusionMode.Ignore + + anchors.top: true + anchors.bottom: true + anchors.left: true + anchors.right: true + + color: "black" + + WallpaperBackground { + screen: bgWindow.screen.name + } + } + + Bar { + screen: screenScope.modelData + } + } + } + + Lockscreen {} + NiriIdle {} + + VolumeOSD {} + BrightnessOSD {} + + NotificationDisplay {} + + UnixIPC {} +} -- cgit v1.2.3