diff options
Diffstat (limited to 'accounts/gkleen@sif/shell/quickshell')
33 files changed, 4290 insertions, 0 deletions
diff --git a/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml b/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml new file mode 100644 index 00000000..dcc23279 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml | |||
| @@ -0,0 +1,172 @@ | |||
| 1 | import QtQuick | ||
| 2 | import qs.Services | ||
| 3 | import Quickshell | ||
| 4 | import Quickshell.Widgets | ||
| 5 | |||
| 6 | Item { | ||
| 7 | id: activeWindowDisplay | ||
| 8 | |||
| 9 | required property int maxWidth | ||
| 10 | required property var screen | ||
| 11 | |||
| 12 | property var activeWindow: { | ||
| 13 | let currWindowId = Array.from(NiriService.workspaces).find(ws => { | ||
| 14 | return ws.output === screen.name && ws.is_active; | ||
| 15 | })?.active_window_id; | ||
| 16 | |||
| 17 | return currWindowId ? Array.from(NiriService.windows).find(win => win.id == currWindowId) : null; | ||
| 18 | } | ||
| 19 | property var windowEntry: activeWindow ? DesktopEntries.heuristicLookup(activeWindow.app_id) : null | ||
| 20 | |||
| 21 | anchors.verticalCenter: parent.verticalCenter | ||
| 22 | width: activeWindowDisplayContent.width | ||
| 23 | height: parent.height | ||
| 24 | |||
| 25 | WrapperMouseArea { | ||
| 26 | id: widgetMouseArea | ||
| 27 | |||
| 28 | anchors.fill: parent | ||
| 29 | |||
| 30 | hoverEnabled: true | ||
| 31 | |||
| 32 | Item { | ||
| 33 | anchors.fill: parent | ||
| 34 | |||
| 35 | Row { | ||
| 36 | id: activeWindowDisplayContent | ||
| 37 | |||
| 38 | width: childrenRect.width | ||
| 39 | height: parent.height | ||
| 40 | anchors.verticalCenter: parent.verticalCenter | ||
| 41 | spacing: 8 | ||
| 42 | |||
| 43 | IconImage { | ||
| 44 | id: activeWindowIcon | ||
| 45 | |||
| 46 | implicitSize: 14 | ||
| 47 | |||
| 48 | anchors.verticalCenter: parent.verticalCenter | ||
| 49 | |||
| 50 | source: { | ||
| 51 | let icon = activeWindowDisplay.windowEntry?.icon | ||
| 52 | if (typeof icon === 'string' || icon instanceof String) { | ||
| 53 | if (icon.includes("?path=")) { | ||
| 54 | const split = icon.split("?path=") | ||
| 55 | if (split.length !== 2) | ||
| 56 | return icon | ||
| 57 | const name = split[0] | ||
| 58 | const path = split[1] | ||
| 59 | const fileName = name.substring( | ||
| 60 | name.lastIndexOf("/") + 1) | ||
| 61 | return `file://${path}/${fileName}` | ||
| 62 | } else | ||
| 63 | icon = Quickshell.iconPath(icon); | ||
| 64 | return icon | ||
| 65 | } | ||
| 66 | return "" | ||
| 67 | } | ||
| 68 | asynchronous: true | ||
| 69 | smooth: true | ||
| 70 | mipmap: true | ||
| 71 | } | ||
| 72 | |||
| 73 | Text { | ||
| 74 | id: windowTitle | ||
| 75 | |||
| 76 | width: Math.min(implicitWidth, activeWindowDisplay.maxWidth - activeWindowIcon.width - activeWindowDisplayContent.spacing) | ||
| 77 | |||
| 78 | property var appAliases: { "Firefox": "Mozilla Firefox", "mpv Media Player": "mpv", "Thunderbird": "Mozilla Thunderbird", "Thunderbird (LMU)": "Mozilla Thunderbird" } | ||
| 79 | |||
| 80 | elide: Text.ElideRight | ||
| 81 | maximumLineCount: 1 | ||
| 82 | color: "white" | ||
| 83 | anchors.verticalCenter: parent.verticalCenter | ||
| 84 | text: { | ||
| 85 | if (!activeWindowDisplay.activeWindow) | ||
| 86 | return ""; | ||
| 87 | |||
| 88 | var title = activeWindowDisplay.activeWindow.title; | ||
| 89 | var appName = activeWindowDisplay.windowEntry?.name; | ||
| 90 | if (appAliases[appName]) | ||
| 91 | appName = appAliases[appName]; | ||
| 92 | if (appName && title.endsWith(appName)) { | ||
| 93 | const oldTitle = title; | ||
| 94 | title = title.substring(0, title.length - appName.length); | ||
| 95 | title = title.replace(/\s*(—|-)\s*$/, ""); | ||
| 96 | if (!title) | ||
| 97 | title = oldTitle; | ||
| 98 | } | ||
| 99 | return title; | ||
| 100 | } | ||
| 101 | } | ||
| 102 | } | ||
| 103 | } | ||
| 104 | } | ||
| 105 | |||
| 106 | Loader { | ||
| 107 | id: tooltipLoader | ||
| 108 | |||
| 109 | active: false | ||
| 110 | |||
| 111 | Connections { | ||
| 112 | target: widgetMouseArea | ||
| 113 | function onContainsMouseChanged() { | ||
| 114 | if (widgetMouseArea.containsMouse) | ||
| 115 | tooltipLoader.active = true; | ||
| 116 | } | ||
| 117 | } | ||
| 118 | |||
| 119 | PopupWindow { | ||
| 120 | id: tooltip | ||
| 121 | |||
| 122 | property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse | ||
| 123 | |||
| 124 | anchor { | ||
| 125 | item: widgetMouseArea | ||
| 126 | edges: Edges.Bottom | Edges.Left | ||
| 127 | } | ||
| 128 | visible: false | ||
| 129 | |||
| 130 | onNextVisibleChanged: hangTimer.restart() | ||
| 131 | |||
| 132 | Timer { | ||
| 133 | id: hangTimer | ||
| 134 | interval: tooltip.visible ? 100 : 500 | ||
| 135 | onTriggered: { | ||
| 136 | tooltip.visible = tooltip.nextVisible; | ||
| 137 | if (!tooltip.visible) | ||
| 138 | tooltipLoader.active = false; | ||
| 139 | } | ||
| 140 | } | ||
| 141 | |||
| 142 | implicitWidth: widgetTooltipText.contentWidth + 16 | ||
| 143 | implicitHeight: widgetTooltipText.contentHeight + 16 | ||
| 144 | color: "black" | ||
| 145 | |||
| 146 | WrapperMouseArea { | ||
| 147 | id: tooltipMouseArea | ||
| 148 | |||
| 149 | hoverEnabled: true | ||
| 150 | enabled: true | ||
| 151 | |||
| 152 | anchors.fill: parent | ||
| 153 | |||
| 154 | Item { | ||
| 155 | anchors.fill: parent | ||
| 156 | |||
| 157 | Text { | ||
| 158 | id: widgetTooltipText | ||
| 159 | |||
| 160 | anchors.centerIn: parent | ||
| 161 | |||
| 162 | font.pointSize: 10 | ||
| 163 | font.family: "Fira Mono" | ||
| 164 | color: "white" | ||
| 165 | |||
| 166 | text: JSON.stringify(Object.assign({}, activeWindowDisplay.activeWindow), null, 2) | ||
| 167 | } | ||
| 168 | } | ||
| 169 | } | ||
| 170 | } | ||
| 171 | } | ||
| 172 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/Bar.qml b/accounts/gkleen@sif/shell/quickshell/Bar.qml new file mode 100644 index 00000000..9210066c --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml | |||
| @@ -0,0 +1,117 @@ | |||
| 1 | import Quickshell | ||
| 2 | import Quickshell.Wayland | ||
| 3 | import QtQuick | ||
| 4 | |||
| 5 | PanelWindow { | ||
| 6 | id: bar | ||
| 7 | |||
| 8 | required property var modelData | ||
| 9 | screen: modelData | ||
| 10 | |||
| 11 | WlrLayershell.namespace: "bar" | ||
| 12 | |||
| 13 | anchors { | ||
| 14 | top: true | ||
| 15 | left: true | ||
| 16 | right: true | ||
| 17 | } | ||
| 18 | margins { | ||
| 19 | left: 26 + 8 | ||
| 20 | right: 26 + 8 | ||
| 21 | } | ||
| 22 | |||
| 23 | implicitHeight: 21 | ||
| 24 | color: "transparent" | ||
| 25 | |||
| 26 | Rectangle { | ||
| 27 | color: Qt.rgba(0, 0, 0, 0.75) | ||
| 28 | anchors.fill: parent | ||
| 29 | // bottomLeftRadius: 8 | ||
| 30 | // bottomRightRadius: 8 | ||
| 31 | } | ||
| 32 | |||
| 33 | Row { | ||
| 34 | id: left | ||
| 35 | |||
| 36 | height: parent.height | ||
| 37 | width: childrenRect.width | ||
| 38 | anchors.left: parent.left | ||
| 39 | anchors.leftMargin: 8 | ||
| 40 | anchors.verticalCenter: parent.verticalCenter | ||
| 41 | spacing: 8 | ||
| 42 | |||
| 43 | WorkspaceSwitcher { | ||
| 44 | screen: bar.screen | ||
| 45 | } | ||
| 46 | } | ||
| 47 | |||
| 48 | Row { | ||
| 49 | id: center | ||
| 50 | |||
| 51 | height: parent.height | ||
| 52 | width: childrenRect.width | ||
| 53 | anchors.centerIn: parent | ||
| 54 | spacing: 5 | ||
| 55 | |||
| 56 | ActiveWindowDisplay { | ||
| 57 | screen: bar.screen | ||
| 58 | maxWidth: bar.width - 2*Math.max(left.width, right.width) - 2*8 | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | Row { | ||
| 63 | id: right | ||
| 64 | |||
| 65 | height: parent.height | ||
| 66 | width: childrenRect.width | ||
| 67 | anchors.right: parent.right | ||
| 68 | anchors.rightMargin: 8 | ||
| 69 | anchors.verticalCenter: parent.verticalCenter | ||
| 70 | spacing: 0 | ||
| 71 | |||
| 72 | // WorktimeWidget { command: "time"; } | ||
| 73 | |||
| 74 | // WorktimeWidget { command: "today"; } | ||
| 75 | |||
| 76 | KeyboardLayout {} | ||
| 77 | |||
| 78 | Item { | ||
| 79 | visible: privacy.visible | ||
| 80 | height: parent.height | ||
| 81 | width: 8 - 4 | ||
| 82 | } | ||
| 83 | |||
| 84 | PrivacyWidget { | ||
| 85 | id: privacy | ||
| 86 | } | ||
| 87 | |||
| 88 | Item { | ||
| 89 | visible: privacy.visible | ||
| 90 | height: parent.height | ||
| 91 | width: 8 - 4 | ||
| 92 | } | ||
| 93 | |||
| 94 | SystemTray {} | ||
| 95 | |||
| 96 | PipewireWidget {} | ||
| 97 | |||
| 98 | BrightnessWidget {} | ||
| 99 | |||
| 100 | BatteryWidget {} | ||
| 101 | |||
| 102 | WaylandInhibitorWidget { | ||
| 103 | window: bar | ||
| 104 | } | ||
| 105 | |||
| 106 | NotificationInhibitorWidget {} | ||
| 107 | |||
| 108 | LidSwitchInhibitorWidget {} | ||
| 109 | |||
| 110 | Item { | ||
| 111 | height: parent.height | ||
| 112 | width: 8 - 4 | ||
| 113 | } | ||
| 114 | |||
| 115 | Clock {} | ||
| 116 | } | ||
| 117 | } \ No newline at end of file | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml b/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml new file mode 100644 index 00000000..da17df2a --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml | |||
| @@ -0,0 +1,136 @@ | |||
| 1 | import QtQuick | ||
| 2 | import Quickshell | ||
| 3 | import Quickshell.Widgets | ||
| 4 | import Quickshell.Services.UPower | ||
| 5 | |||
| 6 | Item { | ||
| 7 | id: root | ||
| 8 | |||
| 9 | height: parent.height | ||
| 10 | width: batteryIcon.width + 8 | ||
| 11 | anchors.verticalCenter: parent.verticalCenter | ||
| 12 | |||
| 13 | property var batteryDevice: Array.from(UPower.devices.values).find(dev => dev.isLaptopBattery) | ||
| 14 | |||
| 15 | WrapperMouseArea { | ||
| 16 | id: widgetMouseArea | ||
| 17 | |||
| 18 | anchors.fill: parent | ||
| 19 | |||
| 20 | hoverEnabled: true | ||
| 21 | |||
| 22 | Item { | ||
| 23 | anchors.fill: parent | ||
| 24 | |||
| 25 | MaterialDesignIcon { | ||
| 26 | id: batteryIcon | ||
| 27 | |||
| 28 | implicitSize: 14 | ||
| 29 | anchors.centerIn: parent | ||
| 30 | |||
| 31 | icon: { | ||
| 32 | if (!root.batteryDevice?.ready) | ||
| 33 | return "battery-unknown"; | ||
| 34 | |||
| 35 | if (root.batteryDevice.state == UPowerDeviceState.FullyCharged) | ||
| 36 | return "power-plug-battery"; | ||
| 37 | |||
| 38 | const perdec = 10 * Math.max(1, Math.ceil(root.batteryDevice.percentage * 10)); | ||
| 39 | if (root.batteryDevice.state == UPowerDeviceState.Charging) | ||
| 40 | return `battery-charging-${perdec}`; | ||
| 41 | if (perdec == 100) | ||
| 42 | return "battery"; | ||
| 43 | return `battery-${perdec}`; | ||
| 44 | } | ||
| 45 | color: { | ||
| 46 | if (!root.batteryDevice?.ready) | ||
| 47 | return "#555"; | ||
| 48 | |||
| 49 | if (root.batteryDevice.state != UPowerDeviceState.FullyCharged && root.batteryDevice.state != UPowerDeviceState.Charging && root.batteryDevice.timeToEmpty < 20 * 60) | ||
| 50 | return "#f2201f"; | ||
| 51 | if (root.batteryDevice.state != UPowerDeviceState.FullyCharged && root.batteryDevice.state != UPowerDeviceState.Charging && root.batteryDevice.timeToEmpty < 40 * 60) | ||
| 52 | return "#f28a21"; | ||
| 53 | if (root.batteryDevice.state != UPowerDeviceState.FullyCharged) | ||
| 54 | return "#fff"; | ||
| 55 | return "#555"; | ||
| 56 | } | ||
| 57 | } | ||
| 58 | } | ||
| 59 | } | ||
| 60 | |||
| 61 | PopupWindow { | ||
| 62 | id: tooltip | ||
| 63 | |||
| 64 | property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse | ||
| 65 | |||
| 66 | anchor { | ||
| 67 | item: widgetMouseArea | ||
| 68 | edges: Edges.Bottom | Edges.Left | ||
| 69 | } | ||
| 70 | visible: false | ||
| 71 | |||
| 72 | onNextVisibleChanged: hangTimer.restart() | ||
| 73 | |||
| 74 | Timer { | ||
| 75 | id: hangTimer | ||
| 76 | interval: 100 | ||
| 77 | onTriggered: tooltip.visible = tooltip.nextVisible | ||
| 78 | } | ||
| 79 | |||
| 80 | implicitWidth: widgetTooltipText.contentWidth + 16 | ||
| 81 | implicitHeight: widgetTooltipText.contentHeight + 16 | ||
| 82 | color: "black" | ||
| 83 | |||
| 84 | WrapperMouseArea { | ||
| 85 | id: tooltipMouseArea | ||
| 86 | |||
| 87 | hoverEnabled: true | ||
| 88 | enabled: true | ||
| 89 | |||
| 90 | anchors.fill: parent | ||
| 91 | |||
| 92 | Item { | ||
| 93 | anchors.fill: parent | ||
| 94 | |||
| 95 | Text { | ||
| 96 | id: widgetTooltipText | ||
| 97 | |||
| 98 | anchors.centerIn: parent | ||
| 99 | |||
| 100 | font.pointSize: 10 | ||
| 101 | font.family: "Fira Sans" | ||
| 102 | color: "white" | ||
| 103 | |||
| 104 | text: { | ||
| 105 | const stateStr = UPowerDeviceState.toString(root.batteryDevice.state); | ||
| 106 | var outStr = stateStr; | ||
| 107 | if (root.batteryDevice.state != UPowerDeviceState.FullyCharged) | ||
| 108 | outStr += ` ${Math.round(root.batteryDevice.percentage * 100)}%`; | ||
| 109 | |||
| 110 | function formatTime(t) { | ||
| 111 | var res = ""; | ||
| 112 | for (const unit of [{ "s": "h", "v": 3600 }, { "s": "m", "v": 60 }, { "s": "s", "v": 1 }]) { | ||
| 113 | if (t < unit.v) | ||
| 114 | continue; | ||
| 115 | res += Math.floor(t / unit.v) + unit.s; | ||
| 116 | t %= unit.v; | ||
| 117 | } | ||
| 118 | return res; | ||
| 119 | } | ||
| 120 | if (root.batteryDevice.timeToEmpty != 0) { | ||
| 121 | const tStr = formatTime(Math.floor(root.batteryDevice.timeToEmpty / 60) * 60); | ||
| 122 | if (tStr) | ||
| 123 | outStr += " " + tStr; | ||
| 124 | } else if (root.batteryDevice.timeToFull != 0) { | ||
| 125 | const tStr = formatTime(Math.ceil(root.batteryDevice.timeToFull / 60) * 60); | ||
| 126 | if (tStr) | ||
| 127 | outStr += " " + tStr; | ||
| 128 | } | ||
| 129 | |||
| 130 | return outStr; | ||
| 131 | } | ||
| 132 | } | ||
| 133 | } | ||
| 134 | } | ||
| 135 | } | ||
| 136 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/BrightnessOSD.qml b/accounts/gkleen@sif/shell/quickshell/BrightnessOSD.qml new file mode 100644 index 00000000..a432179e --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/BrightnessOSD.qml | |||
| @@ -0,0 +1,117 @@ | |||
| 1 | import QtQuick | ||
| 2 | import QtQuick.Layouts | ||
| 3 | import Quickshell | ||
| 4 | import Quickshell.Widgets | ||
| 5 | import qs.Services | ||
| 6 | |||
| 7 | Scope { | ||
| 8 | id: root | ||
| 9 | |||
| 10 | property bool show: false | ||
| 11 | property bool inhibited: true | ||
| 12 | |||
| 13 | Connections { | ||
| 14 | target: Brightness | ||
| 15 | |||
| 16 | function onCurrBrightnessChanged() { | ||
| 17 | root.show = true; | ||
| 18 | hideTimer.restart(); | ||
| 19 | } | ||
| 20 | } | ||
| 21 | |||
| 22 | onShowChanged: { | ||
| 23 | if (show) | ||
| 24 | hideTimer.restart(); | ||
| 25 | } | ||
| 26 | |||
| 27 | Timer { | ||
| 28 | id: hideTimer | ||
| 29 | interval: 1000 | ||
| 30 | onTriggered: root.show = false | ||
| 31 | } | ||
| 32 | |||
| 33 | Timer { | ||
| 34 | id: startInhibit | ||
| 35 | interval: 100 | ||
| 36 | running: true | ||
| 37 | onTriggered: { | ||
| 38 | root.show = false; | ||
| 39 | root.inhibited = false; | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | LazyLoader { | ||
| 44 | active: root.show && !root.inhibited | ||
| 45 | |||
| 46 | Variants { | ||
| 47 | model: Quickshell.screens | ||
| 48 | |||
| 49 | delegate: Scope { | ||
| 50 | id: screenScope | ||
| 51 | |||
| 52 | required property var modelData | ||
| 53 | |||
| 54 | PanelWindow { | ||
| 55 | id: window | ||
| 56 | |||
| 57 | screen: screenScope.modelData | ||
| 58 | |||
| 59 | anchors.top: true | ||
| 60 | margins.top: screen.height / 2 - 50 + 3.5 | ||
| 61 | exclusiveZone: 0 | ||
| 62 | exclusionMode: ExclusionMode.Ignore | ||
| 63 | |||
| 64 | implicitWidth: 400 | ||
| 65 | implicitHeight: 50 | ||
| 66 | |||
| 67 | mask: Region {} | ||
| 68 | |||
| 69 | color: "transparent" | ||
| 70 | |||
| 71 | Rectangle { | ||
| 72 | anchors.fill: parent | ||
| 73 | color: Qt.rgba(0, 0, 0, 0.75) | ||
| 74 | } | ||
| 75 | |||
| 76 | RowLayout { | ||
| 77 | id: layout | ||
| 78 | |||
| 79 | anchors.centerIn: parent | ||
| 80 | |||
| 81 | height: 50 - 8*2 | ||
| 82 | width: 400 - 8*2 | ||
| 83 | |||
| 84 | MaterialDesignIcon { | ||
| 85 | id: volumeIcon | ||
| 86 | |||
| 87 | implicitWidth: parent.height | ||
| 88 | implicitHeight: parent.height | ||
| 89 | |||
| 90 | icon: `brightness-${Math.min(7, Math.floor(Brightness.currBrightness * 7) + 1)}` | ||
| 91 | } | ||
| 92 | |||
| 93 | Rectangle { | ||
| 94 | Layout.fillWidth: true | ||
| 95 | |||
| 96 | implicitHeight: 10 | ||
| 97 | |||
| 98 | color: "#50ffffff" | ||
| 99 | |||
| 100 | Rectangle { | ||
| 101 | anchors { | ||
| 102 | left: parent.left | ||
| 103 | top: parent.top | ||
| 104 | bottom: parent.bottom | ||
| 105 | } | ||
| 106 | |||
| 107 | color: "white" | ||
| 108 | |||
| 109 | implicitWidth: parent.width * Brightness.currBrightness | ||
| 110 | } | ||
| 111 | } | ||
| 112 | } | ||
| 113 | } | ||
| 114 | } | ||
| 115 | } | ||
| 116 | } | ||
| 117 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml b/accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml new file mode 100644 index 00000000..3bb5a80e --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml | |||
| @@ -0,0 +1,84 @@ | |||
| 1 | import QtQuick | ||
| 2 | import Quickshell | ||
| 3 | import Quickshell.Widgets | ||
| 4 | import qs.Services | ||
| 5 | |||
| 6 | Item { | ||
| 7 | height: parent.height | ||
| 8 | width: brightnessIcon.width + 8 | ||
| 9 | anchors.verticalCenter: parent.verticalCenter | ||
| 10 | |||
| 11 | WrapperMouseArea { | ||
| 12 | id: widgetMouseArea | ||
| 13 | |||
| 14 | anchors.fill: parent | ||
| 15 | |||
| 16 | hoverEnabled: true | ||
| 17 | |||
| 18 | property real sensitivity: (1 / 50) / 120 | ||
| 19 | onWheel: event => Brightness.currBrightness += event.angleDelta.y * sensitivity | ||
| 20 | |||
| 21 | Item { | ||
| 22 | anchors.fill: parent | ||
| 23 | |||
| 24 | MaterialDesignIcon { | ||
| 25 | id: brightnessIcon | ||
| 26 | |||
| 27 | implicitSize: 14 | ||
| 28 | anchors.centerIn: parent | ||
| 29 | |||
| 30 | icon: `brightness-${Math.min(7, Math.floor(Brightness.currBrightness * 7) + 1)}` | ||
| 31 | color: "#555" | ||
| 32 | } | ||
| 33 | } | ||
| 34 | } | ||
| 35 | |||
| 36 | PopupWindow { | ||
| 37 | id: tooltip | ||
| 38 | |||
| 39 | property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse | ||
| 40 | |||
| 41 | anchor { | ||
| 42 | item: widgetMouseArea | ||
| 43 | edges: Edges.Bottom | Edges.Left | ||
| 44 | } | ||
| 45 | visible: false | ||
| 46 | |||
| 47 | onNextVisibleChanged: hangTimer.restart() | ||
| 48 | |||
| 49 | Timer { | ||
| 50 | id: hangTimer | ||
| 51 | interval: 100 | ||
| 52 | onTriggered: tooltip.visible = tooltip.nextVisible | ||
| 53 | } | ||
| 54 | |||
| 55 | implicitWidth: widgetTooltipText.contentWidth + 16 | ||
| 56 | implicitHeight: widgetTooltipText.contentHeight + 16 | ||
| 57 | color: "black" | ||
| 58 | |||
| 59 | WrapperMouseArea { | ||
| 60 | id: tooltipMouseArea | ||
| 61 | |||
| 62 | hoverEnabled: true | ||
| 63 | enabled: true | ||
| 64 | |||
| 65 | anchors.fill: parent | ||
| 66 | |||
| 67 | Item { | ||
| 68 | anchors.fill: parent | ||
| 69 | |||
| 70 | Text { | ||
| 71 | id: widgetTooltipText | ||
| 72 | |||
| 73 | anchors.centerIn: parent | ||
| 74 | |||
| 75 | font.pointSize: 10 | ||
| 76 | font.family: "Fira Sans" | ||
| 77 | color: "white" | ||
| 78 | |||
| 79 | text: `${Math.round(Brightness.currBrightness * 100)}%` | ||
| 80 | } | ||
| 81 | } | ||
| 82 | } | ||
| 83 | } | ||
| 84 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/Clock.qml b/accounts/gkleen@sif/shell/quickshell/Clock.qml new file mode 100644 index 00000000..b7004528 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/Clock.qml | |||
| @@ -0,0 +1,295 @@ | |||
| 1 | import QtQml | ||
| 2 | import QtQuick | ||
| 3 | import Quickshell | ||
| 4 | import Custom as Custom | ||
| 5 | import QtQuick.Controls | ||
| 6 | import QtQuick.Layouts | ||
| 7 | import Quickshell.Widgets | ||
| 8 | |||
| 9 | Item { | ||
| 10 | id: clockItem | ||
| 11 | |||
| 12 | property bool calendarPopup: true | ||
| 13 | |||
| 14 | width: clock.contentWidth | ||
| 15 | height: parent.height | ||
| 16 | anchors.verticalCenter: parent.verticalCenter | ||
| 17 | |||
| 18 | WrapperMouseArea { | ||
| 19 | id: clockMouseArea | ||
| 20 | |||
| 21 | anchors.fill: parent | ||
| 22 | hoverEnabled: true | ||
| 23 | enabled: clockItem.calendarPopup | ||
| 24 | |||
| 25 | Item { | ||
| 26 | anchors.fill: parent | ||
| 27 | |||
| 28 | Text { | ||
| 29 | id: clock | ||
| 30 | color: "white" | ||
| 31 | |||
| 32 | anchors.verticalCenter: parent.verticalCenter | ||
| 33 | |||
| 34 | Custom.Chrono { | ||
| 35 | id: chrono | ||
| 36 | |||
| 37 | onDateChanged: clock.text = format("W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}") | ||
| 38 | } | ||
| 39 | |||
| 40 | font.pointSize: 10 | ||
| 41 | font.family: "Fira Sans" | ||
| 42 | font.features: { "tnum": 1 } | ||
| 43 | } | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | Loader { | ||
| 48 | id: tooltipLoader | ||
| 49 | |||
| 50 | active: false | ||
| 51 | |||
| 52 | Connections { | ||
| 53 | target: clockMouseArea | ||
| 54 | function onContainsMouseChanged() { | ||
| 55 | if (clockMouseArea.containsMouse) | ||
| 56 | tooltipLoader.active = true; | ||
| 57 | } | ||
| 58 | } | ||
| 59 | |||
| 60 | sourceComponent: PopupWindow { | ||
| 61 | id: tooltip | ||
| 62 | |||
| 63 | property bool nextVisible: clockMouseArea.containsMouse || tooltipMouseArea.containsMouse | ||
| 64 | |||
| 65 | anchor { | ||
| 66 | item: clockMouseArea | ||
| 67 | edges: Edges.Bottom | Edges.Left | ||
| 68 | } | ||
| 69 | visible: false | ||
| 70 | |||
| 71 | onNextVisibleChanged: hangTimer.restart() | ||
| 72 | |||
| 73 | Timer { | ||
| 74 | id: hangTimer | ||
| 75 | interval: 100 | ||
| 76 | onTriggered: { | ||
| 77 | tooltip.visible = tooltip.nextVisible; | ||
| 78 | if (!tooltip.visible) | ||
| 79 | tooltipLoader.active = false; | ||
| 80 | } | ||
| 81 | } | ||
| 82 | |||
| 83 | implicitWidth: tooltipLayout.childrenRect.width + 16 | ||
| 84 | implicitHeight: tooltipLayout.childrenRect.height + 16 | ||
| 85 | color: "black" | ||
| 86 | |||
| 87 | onVisibleChanged: { | ||
| 88 | yearCalendar.year = chrono.date.getFullYear(); | ||
| 89 | yearCalendar.angleRem = 0; | ||
| 90 | } | ||
| 91 | |||
| 92 | WrapperMouseArea { | ||
| 93 | id: tooltipMouseArea | ||
| 94 | |||
| 95 | hoverEnabled: true | ||
| 96 | enabled: true | ||
| 97 | |||
| 98 | onWheel: event => yearCalendar.scrollYear(event) | ||
| 99 | |||
| 100 | anchors.fill: parent | ||
| 101 | |||
| 102 | Item { | ||
| 103 | id: clockTooltipContent | ||
| 104 | |||
| 105 | anchors.fill: parent | ||
| 106 | |||
| 107 | ColumnLayout { | ||
| 108 | id: tooltipLayout | ||
| 109 | |||
| 110 | anchors { | ||
| 111 | left: parent.left | ||
| 112 | top: parent.top | ||
| 113 | leftMargin: 8 | ||
| 114 | topMargin: 8 | ||
| 115 | } | ||
| 116 | |||
| 117 | Text { | ||
| 118 | id: yearLabel | ||
| 119 | |||
| 120 | horizontalAlignment: Text.AlignHCenter | ||
| 121 | |||
| 122 | font.pointSize: 14 | ||
| 123 | font.family: "Fira Sans" | ||
| 124 | font.features: { "tnum": 1 } | ||
| 125 | color: "white" | ||
| 126 | |||
| 127 | text: yearCalendar.year | ||
| 128 | |||
| 129 | Layout.fillWidth: true | ||
| 130 | Layout.bottomMargin: 8 | ||
| 131 | } | ||
| 132 | |||
| 133 | GridLayout { | ||
| 134 | property int year: chrono.date.getFullYear() | ||
| 135 | |||
| 136 | id: yearCalendar | ||
| 137 | |||
| 138 | columns: 3 | ||
| 139 | columnSpacing: 16 | ||
| 140 | rowSpacing: 16 | ||
| 141 | |||
| 142 | Layout.alignment: Qt.AlignHCenter | ||
| 143 | Layout.fillWidth: false | ||
| 144 | |||
| 145 | property real angleRem: 0 | ||
| 146 | property real sensitivity: 1 / 120 | ||
| 147 | |||
| 148 | function scrollYear(event) { | ||
| 149 | angleRem += event.angleDelta.y; | ||
| 150 | const d = Math.round(angleRem * sensitivity); | ||
| 151 | yearCalendar.year += d; | ||
| 152 | angleRem -= d / sensitivity; | ||
| 153 | } | ||
| 154 | |||
| 155 | Connections { | ||
| 156 | target: clockMouseArea | ||
| 157 | function onWheel(event) { yearCalendar.scrollYear(event); } | ||
| 158 | } | ||
| 159 | |||
| 160 | Repeater { | ||
| 161 | model: 12 | ||
| 162 | |||
| 163 | GridLayout { | ||
| 164 | columns: 2 | ||
| 165 | |||
| 166 | required property int index | ||
| 167 | property int month: index | ||
| 168 | |||
| 169 | id: monthCalendar | ||
| 170 | |||
| 171 | Layout.alignment: Qt.AlignTop | Qt.AlignRight | ||
| 172 | Layout.fillWidth: false | ||
| 173 | |||
| 174 | Text { | ||
| 175 | Layout.column: 1 | ||
| 176 | Layout.fillWidth: true | ||
| 177 | |||
| 178 | horizontalAlignment: Text.AlignHCenter | ||
| 179 | |||
| 180 | font.pointSize: 10 | ||
| 181 | font.family: "Fira Sans" | ||
| 182 | |||
| 183 | text: new Date(yearCalendar.year, monthCalendar.month, 1).toLocaleString(Qt.locale("en_DK"), "MMMM") | ||
| 184 | |||
| 185 | color: "#ffead3" | ||
| 186 | } | ||
| 187 | |||
| 188 | DayOfWeekRow { | ||
| 189 | locale: grid.locale | ||
| 190 | |||
| 191 | Layout.row: 1 | ||
| 192 | Layout.column: 1 | ||
| 193 | Layout.fillWidth: true | ||
| 194 | |||
| 195 | delegate: WrapperItem { | ||
| 196 | required property string shortName | ||
| 197 | |||
| 198 | width: dowLabel.contentWidth + 6 | ||
| 199 | |||
| 200 | Text { | ||
| 201 | id: dowLabel | ||
| 202 | |||
| 203 | anchors.fill: parent | ||
| 204 | |||
| 205 | font.pointSize: 10 | ||
| 206 | font.family: "Fira Sans" | ||
| 207 | |||
| 208 | text: parent.shortName | ||
| 209 | color: "#ffcc66" | ||
| 210 | |||
| 211 | horizontalAlignment: Text.AlignHCenter | ||
| 212 | verticalAlignment: Text.AlignVCenter | ||
| 213 | } | ||
| 214 | } | ||
| 215 | } | ||
| 216 | |||
| 217 | WeekNumberColumn { | ||
| 218 | month: grid.month | ||
| 219 | year: grid.year | ||
| 220 | locale: grid.locale | ||
| 221 | |||
| 222 | Layout.fillHeight: true | ||
| 223 | |||
| 224 | delegate: Text { | ||
| 225 | required property int weekNumber | ||
| 226 | |||
| 227 | opacity: { | ||
| 228 | const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7); | ||
| 229 | const dayOfWeek = simple.getDay(); | ||
| 230 | const isoWeekStart = simple; | ||
| 231 | |||
| 232 | isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1); | ||
| 233 | if (dayOfWeek > 4) { | ||
| 234 | isoWeekStart.setDate(isoWeekStart.getDate() + 7); | ||
| 235 | } | ||
| 236 | |||
| 237 | for (let i = 0; i < 7; i++) { | ||
| 238 | const dayInWeek = new Date(isoWeekStart); | ||
| 239 | dayInWeek.setDate(dayInWeek.getDate() + i); | ||
| 240 | if (dayInWeek.getMonth() == monthCalendar.month) | ||
| 241 | return 1; | ||
| 242 | } | ||
| 243 | |||
| 244 | return 0; | ||
| 245 | } | ||
| 246 | |||
| 247 | font.pointSize: 10 | ||
| 248 | font.family: "Fira Sans" | ||
| 249 | font.features: { "tnum": 1 } | ||
| 250 | |||
| 251 | text: weekNumber | ||
| 252 | color: "#99ffdd" | ||
| 253 | |||
| 254 | horizontalAlignment: Text.AlignRight | ||
| 255 | verticalAlignment: Text.AlignVCenter | ||
| 256 | } | ||
| 257 | } | ||
| 258 | |||
| 259 | MonthGrid { | ||
| 260 | id: grid | ||
| 261 | |||
| 262 | year: yearCalendar.year | ||
| 263 | month: monthCalendar.month | ||
| 264 | locale: Qt.locale("en_DK") | ||
| 265 | |||
| 266 | Layout.fillWidth: true | ||
| 267 | Layout.fillHeight: true | ||
| 268 | |||
| 269 | delegate: Text { | ||
| 270 | required property var model | ||
| 271 | |||
| 272 | opacity: model.month === monthCalendar.month ? 1 : 0 | ||
| 273 | |||
| 274 | font.pointSize: 10 | ||
| 275 | font.family: "Fira Sans" | ||
| 276 | font.features: { "tnum": 1 } | ||
| 277 | |||
| 278 | property bool today: chrono.date.getFullYear() == model.year && chrono.date.getMonth() == model.month && chrono.date.getDate() == model.day | ||
| 279 | |||
| 280 | text: model.day | ||
| 281 | color: today ? "#ff6699" : "white" | ||
| 282 | |||
| 283 | horizontalAlignment: Text.AlignRight | ||
| 284 | verticalAlignment: Text.AlignVCenter | ||
| 285 | } | ||
| 286 | } | ||
| 287 | } | ||
| 288 | } | ||
| 289 | } | ||
| 290 | } | ||
| 291 | } | ||
| 292 | } | ||
| 293 | } | ||
| 294 | } | ||
| 295 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml b/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml new file mode 100644 index 00000000..46302e54 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml | |||
| @@ -0,0 +1,112 @@ | |||
| 1 | import Quickshell | ||
| 2 | import QtQuick | ||
| 3 | import qs.Services | ||
| 4 | import Quickshell.Widgets | ||
| 5 | |||
| 6 | Item { | ||
| 7 | width: kbdLabel.contentWidth + 8 | ||
| 8 | height: parent.height | ||
| 9 | anchors.verticalCenter: parent.verticalCenter | ||
| 10 | |||
| 11 | WrapperMouseArea { | ||
| 12 | id: kbdMouseArea | ||
| 13 | |||
| 14 | anchors.fill: parent | ||
| 15 | |||
| 16 | hoverEnabled: true | ||
| 17 | cursorShape: Qt.PointingHandCursor | ||
| 18 | enabled: true | ||
| 19 | onClicked: { | ||
| 20 | NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": "Next" } } }, _ => {}) | ||
| 21 | } | ||
| 22 | onWheel: event => { | ||
| 23 | NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": event.angleDelta > 0 ? "Next" : "Prev" } } }, _ => {}) | ||
| 24 | } | ||
| 25 | |||
| 26 | Rectangle { | ||
| 27 | id: kbdWidget | ||
| 28 | |||
| 29 | property var keyboardAbbrev: { "English (programmer Dvorak)": "dvp", "English (US)": "us" } | ||
| 30 | |||
| 31 | anchors.fill: parent | ||
| 32 | color: { | ||
| 33 | if (kbdMouseArea.containsMouse) { | ||
| 34 | return "#33808080"; | ||
| 35 | } | ||
| 36 | return "transparent"; | ||
| 37 | } | ||
| 38 | |||
| 39 | Text { | ||
| 40 | id: kbdLabel | ||
| 41 | |||
| 42 | font.pointSize: 10 | ||
| 43 | font.family: "Fira Sans" | ||
| 44 | color: { | ||
| 45 | if (NiriService.keyboardLayouts?.current_idx === 0) | ||
| 46 | return "#555"; | ||
| 47 | return "white"; | ||
| 48 | } | ||
| 49 | anchors.centerIn: parent | ||
| 50 | |||
| 51 | text: { | ||
| 52 | const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx]; | ||
| 53 | if (!currentLayout) | ||
| 54 | return ""; | ||
| 55 | return kbdWidget.keyboardAbbrev[currentLayout] ? kbdWidget.keyboardAbbrev[currentLayout] : currentLayout; | ||
| 56 | } | ||
| 57 | } | ||
| 58 | } | ||
| 59 | } | ||
| 60 | |||
| 61 | PopupWindow { | ||
| 62 | id: tooltip | ||
| 63 | |||
| 64 | property bool nextVisible: kbdMouseArea.containsMouse || tooltipMouseArea.containsMouse | ||
| 65 | |||
| 66 | anchor { | ||
| 67 | item: kbdMouseArea | ||
| 68 | edges: Edges.Bottom | Edges.Left | ||
| 69 | } | ||
| 70 | visible: false | ||
| 71 | |||
| 72 | onNextVisibleChanged: hangTimer.restart() | ||
| 73 | |||
| 74 | Timer { | ||
| 75 | id: hangTimer | ||
| 76 | interval: 100 | ||
| 77 | onTriggered: tooltip.visible = tooltip.nextVisible | ||
| 78 | } | ||
| 79 | |||
| 80 | implicitWidth: kbdTooltipText.contentWidth + 16 | ||
| 81 | implicitHeight: kbdTooltipText.contentHeight + 16 | ||
| 82 | color: "black" | ||
| 83 | |||
| 84 | WrapperMouseArea { | ||
| 85 | id: tooltipMouseArea | ||
| 86 | |||
| 87 | hoverEnabled: true | ||
| 88 | enabled: true | ||
| 89 | |||
| 90 | anchors.fill: parent | ||
| 91 | |||
| 92 | Item { | ||
| 93 | anchors.fill: parent | ||
| 94 | |||
| 95 | Text { | ||
| 96 | id: kbdTooltipText | ||
| 97 | |||
| 98 | anchors.centerIn: parent | ||
| 99 | |||
| 100 | font.pointSize: 10 | ||
| 101 | font.family: "Fira Sans" | ||
| 102 | color: "white" | ||
| 103 | |||
| 104 | text: { | ||
| 105 | const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx]; | ||
| 106 | return currentLayout || ""; | ||
| 107 | } | ||
| 108 | } | ||
| 109 | } | ||
| 110 | } | ||
| 111 | } | ||
| 112 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/LidSwitchInhibitorWidget.qml b/accounts/gkleen@sif/shell/quickshell/LidSwitchInhibitorWidget.qml new file mode 100644 index 00000000..8410dcda --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/LidSwitchInhibitorWidget.qml | |||
| @@ -0,0 +1,47 @@ | |||
| 1 | import Quickshell | ||
| 2 | import QtQuick | ||
| 3 | import Quickshell.Widgets | ||
| 4 | import qs.Services | ||
| 5 | |||
| 6 | Item { | ||
| 7 | id: root | ||
| 8 | |||
| 9 | width: icon.width + 8 | ||
| 10 | height: parent.height | ||
| 11 | anchors.verticalCenter: parent.verticalCenter | ||
| 12 | |||
| 13 | WrapperMouseArea { | ||
| 14 | id: widgetMouseArea | ||
| 15 | |||
| 16 | anchors.fill: parent | ||
| 17 | |||
| 18 | hoverEnabled: true | ||
| 19 | cursorShape: Qt.PointingHandCursor | ||
| 20 | |||
| 21 | onClicked: InhibitorState.lidSwitchInhibited = !InhibitorState.lidSwitchInhibited | ||
| 22 | |||
| 23 | Rectangle { | ||
| 24 | anchors.fill: parent | ||
| 25 | color: { | ||
| 26 | if (widgetMouseArea.containsMouse) { | ||
| 27 | return "#33808080"; | ||
| 28 | } | ||
| 29 | return "transparent"; | ||
| 30 | } | ||
| 31 | |||
| 32 | Item { | ||
| 33 | anchors.fill: parent | ||
| 34 | |||
| 35 | MaterialDesignIcon { | ||
| 36 | id: icon | ||
| 37 | |||
| 38 | implicitSize: 14 | ||
| 39 | anchors.centerIn: parent | ||
| 40 | |||
| 41 | icon: InhibitorState.lidSwitchInhibited ? "laptop-off" : "laptop" | ||
| 42 | color: InhibitorState.lidSwitchInhibited ? "#f28a21" : "#555" | ||
| 43 | } | ||
| 44 | } | ||
| 45 | } | ||
| 46 | } | ||
| 47 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/LockSurface.qml b/accounts/gkleen@sif/shell/quickshell/LockSurface.qml new file mode 100644 index 00000000..f4f8f0cd --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/LockSurface.qml | |||
| @@ -0,0 +1,227 @@ | |||
| 1 | import Quickshell.Widgets | ||
| 2 | import QtQuick.Effects | ||
| 3 | import QtQuick.Layouts | ||
| 4 | import QtQuick | ||
| 5 | import QtQuick.Controls | ||
| 6 | import QtQuick.Controls.Fusion | ||
| 7 | import qs.Services | ||
| 8 | import QtQml | ||
| 9 | |||
| 10 | Item { | ||
| 11 | id: lockSurface | ||
| 12 | |||
| 13 | property var screen | ||
| 14 | property list<var> messages: [] | ||
| 15 | property bool responseRequired: false | ||
| 16 | property bool responseVisible: false | ||
| 17 | property string currentText: "" | ||
| 18 | property bool authRunning: false | ||
| 19 | |||
| 20 | signal response(string responseText) | ||
| 21 | |||
| 22 | anchors.fill: parent | ||
| 23 | |||
| 24 | Item { | ||
| 25 | id: background | ||
| 26 | |||
| 27 | anchors.fill: parent | ||
| 28 | |||
| 29 | property Img current: one | ||
| 30 | property string source: selector.selected | ||
| 31 | |||
| 32 | WallpaperSelector { | ||
| 33 | id: selector | ||
| 34 | seed: lockSurface.screen?.name || "" | ||
| 35 | } | ||
| 36 | |||
| 37 | onSourceChanged: { | ||
| 38 | if (!source) | ||
| 39 | current = null; | ||
| 40 | else if (current === one) | ||
| 41 | two.update() | ||
| 42 | else | ||
| 43 | one.update() | ||
| 44 | } | ||
| 45 | |||
| 46 | Img { id: one } | ||
| 47 | Img { id: two } | ||
| 48 | |||
| 49 | component Img: Item { | ||
| 50 | id: img | ||
| 51 | |||
| 52 | property string source | ||
| 53 | |||
| 54 | function update() { | ||
| 55 | source = background.source || "" | ||
| 56 | } | ||
| 57 | |||
| 58 | anchors.fill: parent | ||
| 59 | |||
| 60 | Image { | ||
| 61 | id: imageSource | ||
| 62 | |||
| 63 | source: img.source | ||
| 64 | sourceSize: Qt.size(parent.width, parent.height) | ||
| 65 | fillMode: Image.PreserveAspectCrop | ||
| 66 | smooth: true | ||
| 67 | visible: false | ||
| 68 | asynchronous: true | ||
| 69 | cache: false | ||
| 70 | |||
| 71 | onStatusChanged: { | ||
| 72 | if (status === Image.Ready) { | ||
| 73 | background.current = img | ||
| 74 | } | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | MultiEffect { | ||
| 79 | id: imageEffect | ||
| 80 | |||
| 81 | source: imageSource | ||
| 82 | anchors.fill: parent | ||
| 83 | blurEnabled: true | ||
| 84 | blur: 1 | ||
| 85 | blurMax: 64 | ||
| 86 | blurMultiplier: 2 | ||
| 87 | |||
| 88 | opacity: 0 | ||
| 89 | |||
| 90 | states: State { | ||
| 91 | name: "visible" | ||
| 92 | when: background.current === img | ||
| 93 | |||
| 94 | PropertyChanges { | ||
| 95 | imageEffect.opacity: 1 | ||
| 96 | } | ||
| 97 | StateChangeScript { | ||
| 98 | name: "unloadOther" | ||
| 99 | script: { | ||
| 100 | if (img === one) | ||
| 101 | two.source = "" | ||
| 102 | if (img === two) | ||
| 103 | one.source = "" | ||
| 104 | } | ||
| 105 | } | ||
| 106 | } | ||
| 107 | |||
| 108 | transitions: Transition { | ||
| 109 | SequentialAnimation { | ||
| 110 | NumberAnimation { | ||
| 111 | target: imageEffect | ||
| 112 | properties: "opacity" | ||
| 113 | duration: { | ||
| 114 | if (img === one && two.source == "" || img === two && one.source == "") | ||
| 115 | return 0; | ||
| 116 | return 5000; | ||
| 117 | } | ||
| 118 | easing.type: Easing.OutCubic | ||
| 119 | } | ||
| 120 | ScriptAction { | ||
| 121 | scriptName: "unloadOther" | ||
| 122 | } | ||
| 123 | } | ||
| 124 | } | ||
| 125 | } | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | Item { | ||
| 130 | anchors { | ||
| 131 | top: lockSurface.top | ||
| 132 | left: lockSurface.left | ||
| 133 | right: lockSurface.right | ||
| 134 | } | ||
| 135 | |||
| 136 | implicitWidth: lockSurface.width | ||
| 137 | implicitHeight: 21 | ||
| 138 | |||
| 139 | Rectangle { | ||
| 140 | anchors.fill: parent | ||
| 141 | color: Qt.rgba(0, 0, 0, 0.75) | ||
| 142 | } | ||
| 143 | |||
| 144 | Clock { | ||
| 145 | anchors.centerIn: parent | ||
| 146 | calendarPopup: false | ||
| 147 | } | ||
| 148 | } | ||
| 149 | |||
| 150 | WrapperRectangle { | ||
| 151 | id: unlockUi | ||
| 152 | |||
| 153 | Keys.onPressed: event => { | ||
| 154 | if (!lockSurface.authRunning) { | ||
| 155 | event.accepted = true; | ||
| 156 | lockSurface.authRunning = true; | ||
| 157 | } | ||
| 158 | } | ||
| 159 | focus: !passwordBox.visible | ||
| 160 | |||
| 161 | visible: lockSurface.authRunning | ||
| 162 | |||
| 163 | color: Qt.rgba(0, 0, 0, 0.75) | ||
| 164 | margin: 8 | ||
| 165 | |||
| 166 | anchors.centerIn: parent | ||
| 167 | |||
| 168 | ColumnLayout { | ||
| 169 | spacing: 4 | ||
| 170 | |||
| 171 | BusyIndicator { | ||
| 172 | visible: running | ||
| 173 | running: !Array.from(lockSurface.messages).length && !lockSurface.responseRequired | ||
| 174 | } | ||
| 175 | |||
| 176 | Repeater { | ||
| 177 | model: lockSurface.messages | ||
| 178 | |||
| 179 | Text { | ||
| 180 | required property var modelData | ||
| 181 | |||
| 182 | font.pointSize: 10 | ||
| 183 | font.family: "Fira Sans" | ||
| 184 | color: modelData.error ? "#f28a21" : "#ffffff" | ||
| 185 | |||
| 186 | text: String(modelData.text).trim() | ||
| 187 | |||
| 188 | Layout.fillWidth: true | ||
| 189 | horizontalAlignment: Text.AlignHCenter | ||
| 190 | } | ||
| 191 | } | ||
| 192 | |||
| 193 | TextField { | ||
| 194 | id: passwordBox | ||
| 195 | |||
| 196 | visible: lockSurface.responseRequired | ||
| 197 | echoMode: lockSurface.responseVisible ? TextInput.Normal : TextInput.Password | ||
| 198 | inputMethodHints: Qt.ImhSensitiveData | ||
| 199 | |||
| 200 | onTextChanged: lockSurface.currentText = passwordBox.text | ||
| 201 | onAccepted: { | ||
| 202 | passwordBox.readOnly = true; | ||
| 203 | lockSurface.response(lockSurface.currentText); | ||
| 204 | } | ||
| 205 | |||
| 206 | Connections { | ||
| 207 | target: lockSurface | ||
| 208 | function onCurrentTextChanged() { | ||
| 209 | passwordBox.text = lockSurface.currentText | ||
| 210 | } | ||
| 211 | } | ||
| 212 | Connections { | ||
| 213 | target: lockSurface | ||
| 214 | function onResponseRequiredChanged() { | ||
| 215 | if (lockSurface.responseRequired) | ||
| 216 | passwordBox.readOnly = false; | ||
| 217 | passwordBox.focus = true; | ||
| 218 | passwordBox.selectAll(); | ||
| 219 | } | ||
| 220 | } | ||
| 221 | |||
| 222 | Layout.topMargin: 4 | ||
| 223 | Layout.fillWidth: true | ||
| 224 | } | ||
| 225 | } | ||
| 226 | } | ||
| 227 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml new file mode 100644 index 00000000..996fd41b --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml | |||
| @@ -0,0 +1,132 @@ | |||
| 1 | import Quickshell | ||
| 2 | import Quickshell.Wayland | ||
| 3 | import Quickshell.Io | ||
| 4 | import Quickshell.Services.Pam | ||
| 5 | import Quickshell.Services.Mpris | ||
| 6 | import Custom as Custom | ||
| 7 | import qs.Services | ||
| 8 | import QtQml | ||
| 9 | |||
| 10 | Scope { | ||
| 11 | id: lockscreen | ||
| 12 | |||
| 13 | property string currentText: "" | ||
| 14 | |||
| 15 | PamContext { | ||
| 16 | id: pam | ||
| 17 | |||
| 18 | property list<var> messages: [] | ||
| 19 | |||
| 20 | config: "quickshell" | ||
| 21 | onCompleted: result => { | ||
| 22 | if (result === PamResult.Success) { | ||
| 23 | lock.locked = false; | ||
| 24 | } | ||
| 25 | } | ||
| 26 | onPamMessage: { | ||
| 27 | messages = Array.from(messages).concat([{ "text": pam.message, "error": pam.messageIsError }]) | ||
| 28 | } | ||
| 29 | onActiveChanged: { | ||
| 30 | messages = []; | ||
| 31 | } | ||
| 32 | } | ||
| 33 | |||
| 34 | IpcHandler { | ||
| 35 | target: "Lockscreen" | ||
| 36 | |||
| 37 | function setLocked(locked: bool): void { lock.locked = locked; } | ||
| 38 | function getLocked(): bool { return lock.locked; } | ||
| 39 | } | ||
| 40 | |||
| 41 | Connections { | ||
| 42 | target: Custom.Systemd | ||
| 43 | function onSleep(before: bool) { | ||
| 44 | console.log(`received prepare for sleep ${before}`); | ||
| 45 | if (before) | ||
| 46 | lock.locked = true; | ||
| 47 | } | ||
| 48 | function onLock() { lock.locked = true; } | ||
| 49 | function onUnlock() { lock.locked = false; } | ||
| 50 | } | ||
| 51 | |||
| 52 | IdleMonitor { | ||
| 53 | id: idleMonitor | ||
| 54 | enabled: !lock.secure | ||
| 55 | timeout: 600 | ||
| 56 | respectInhibitors: true | ||
| 57 | |||
| 58 | onIsIdleChanged: { | ||
| 59 | if (idleMonitor.isIdle) | ||
| 60 | lock.locked = true; | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | Custom.SystemdInhibitor { | ||
| 65 | enabled: !lock.secure | ||
| 66 | |||
| 67 | what: Custom.SystemdInhibitorParams.Sleep | ||
| 68 | who: "quickshell" | ||
| 69 | why: "Lock session" | ||
| 70 | mode: Custom.SystemdInhibitorParams.Delay | ||
| 71 | } | ||
| 72 | |||
| 73 | Binding { | ||
| 74 | target: NotificationManager | ||
| 75 | property: "lockscreenActive" | ||
| 76 | value: lock.locked | ||
| 77 | } | ||
| 78 | |||
| 79 | WlSessionLock { | ||
| 80 | id: lock | ||
| 81 | |||
| 82 | onLockStateChanged: { | ||
| 83 | if (!locked && pam.active) | ||
| 84 | pam.abort(); | ||
| 85 | |||
| 86 | if (locked) { | ||
| 87 | NiriService.sendCommand({ "Action": { "PowerOffMonitors": {} } }, _ => {}); | ||
| 88 | Custom.KeePassXC.lockAllDatabases(); | ||
| 89 | Array.from(MprisProxy.players).forEach(player => { | ||
| 90 | if (player.canPause && player.isPlaying) | ||
| 91 | player.pause(); | ||
| 92 | }); | ||
| 93 | // Custom.Systemd.stopUserUnit("gpg-agent.service", "replace"); | ||
| 94 | GpgAgent.reloadAgent(); | ||
| 95 | } | ||
| 96 | } | ||
| 97 | Component.onCompleted: { (_ => {})(MprisProxy.players); } | ||
| 98 | |||
| 99 | onSecureStateChanged: Custom.Systemd.lockedHint = lock.secure | ||
| 100 | |||
| 101 | WlSessionLockSurface { | ||
| 102 | id: lockSurface | ||
| 103 | |||
| 104 | color: "black" | ||
| 105 | |||
| 106 | LockSurface { | ||
| 107 | id: surfaceContent | ||
| 108 | |||
| 109 | onResponse: responseText => pam.respond(responseText) | ||
| 110 | onAuthRunningChanged: { | ||
| 111 | if (authRunning) | ||
| 112 | pam.start(); | ||
| 113 | } | ||
| 114 | Connections { | ||
| 115 | target: pam | ||
| 116 | function onMessagesChanged() { surfaceContent.messages = pam.messages; } | ||
| 117 | function onResponseRequiredChanged() { surfaceContent.responseRequired = pam.responseRequired; } | ||
| 118 | function onActiveChanged() { surfaceContent.authRunning = pam.active; } | ||
| 119 | } | ||
| 120 | onCurrentTextChanged: lockscreen.currentText = currentText | ||
| 121 | Connections { | ||
| 122 | target: lockscreen | ||
| 123 | function onCurrentTextChanged() { surfaceContent.currentText = lockscreen.currentText; } | ||
| 124 | } | ||
| 125 | Connections { | ||
| 126 | target: lockSurface | ||
| 127 | function onScreenChanged() { surfaceContent.screen = lockSurface.screen; } | ||
| 128 | } | ||
| 129 | } | ||
| 130 | } | ||
| 131 | } | ||
| 132 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml b/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml new file mode 100644 index 00000000..155a009e --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml | |||
| @@ -0,0 +1,35 @@ | |||
| 1 | import QtQuick | ||
| 2 | import QtQuick.Effects | ||
| 3 | |||
| 4 | Item { | ||
| 5 | id: root | ||
| 6 | |||
| 7 | required property string icon | ||
| 8 | property color color: "white" | ||
| 9 | |||
| 10 | property real implicitSize: 0 | ||
| 11 | |||
| 12 | readonly property real actualSize: Math.min(root.width, root.height) | ||
| 13 | |||
| 14 | implicitWidth: root.implicitSize | ||
| 15 | implicitHeight: root.implicitSize | ||
| 16 | |||
| 17 | Image { | ||
| 18 | id: sourceImage | ||
| 19 | source: "file://" + @mdi@ + "/svg/" + root.icon + ".svg" | ||
| 20 | anchors.fill: parent | ||
| 21 | fillMode: Image.PreserveAspectFit | ||
| 22 | |||
| 23 | sourceSize.width: root.actualSize | ||
| 24 | sourceSize.height: root.actualSize | ||
| 25 | |||
| 26 | layer.enabled: true | ||
| 27 | layer.effect: MultiEffect { | ||
| 28 | id: effect | ||
| 29 | |||
| 30 | brightness: 1 | ||
| 31 | colorization: 1 | ||
| 32 | colorizationColor: root.color | ||
| 33 | } | ||
| 34 | } | ||
| 35 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/NiriIdle.qml b/accounts/gkleen@sif/shell/quickshell/NiriIdle.qml new file mode 100644 index 00000000..beff205c --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/NiriIdle.qml | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | import QtQml | ||
| 2 | import Quickshell | ||
| 3 | import Quickshell.Wayland | ||
| 4 | import qs.Services | ||
| 5 | import Custom as Custom | ||
| 6 | |||
| 7 | Scope { | ||
| 8 | IdleMonitor { | ||
| 9 | id: idleMonitor30 | ||
| 10 | timeout: 30 | ||
| 11 | |||
| 12 | onIsIdleChanged: Custom.Systemd.setIdleHint(idleMonitor30.isIdle) | ||
| 13 | } | ||
| 14 | IdleMonitor { | ||
| 15 | id: idleMonitor540 | ||
| 16 | timeout: 540 | ||
| 17 | |||
| 18 | onIsIdleChanged: { | ||
| 19 | if (idleMonitor540.isIdle) | ||
| 20 | NiriService.sendCommand({ "Action": { "PowerOffMonitors": {} } }, _ => {}); | ||
| 21 | } | ||
| 22 | } | ||
| 23 | Connections { | ||
| 24 | target: Custom.Systemd | ||
| 25 | function onSleep(before: bool) { | ||
| 26 | if (!before) | ||
| 27 | NiriService.sendCommand({ "Action": { "PowerOnMonitors": {} } }, _ => {}); | ||
| 28 | } | ||
| 29 | } | ||
| 30 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml b/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml new file mode 100644 index 00000000..cc0e49b1 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml | |||
| @@ -0,0 +1,340 @@ | |||
| 1 | import QtQml | ||
| 2 | import QtQml.Models | ||
| 3 | import QtQuick | ||
| 4 | import Quickshell | ||
| 5 | import Quickshell.Widgets | ||
| 6 | import Quickshell.Wayland | ||
| 7 | import qs.Services | ||
| 8 | import QtQuick.Layouts | ||
| 9 | import Quickshell.Services.Notifications | ||
| 10 | |||
| 11 | Scope { | ||
| 12 | id: root | ||
| 13 | |||
| 14 | property var activeScreen: Array.from(Quickshell.screens).find(screen => screen.name === Array.from(NiriService.workspaces).find(ws => ws.is_focused)?.output) ?? null | ||
| 15 | |||
| 16 | Instantiator { | ||
| 17 | id: notifsRepeater | ||
| 18 | |||
| 19 | model: ScriptModel { | ||
| 20 | values: NotificationManager.groups | ||
| 21 | } | ||
| 22 | |||
| 23 | delegate: PanelWindow { | ||
| 24 | id: notifWindow | ||
| 25 | |||
| 26 | visible: NotificationManager.active | ||
| 27 | |||
| 28 | screen: root.activeScreen | ||
| 29 | |||
| 30 | WlrLayershell.namespace: "notifications" | ||
| 31 | |||
| 32 | required property var modelData | ||
| 33 | required property var index | ||
| 34 | |||
| 35 | property int activeIx: modelData.length - 1 | ||
| 36 | onModelDataChanged: { | ||
| 37 | notifWindow.activeIx = modelData.length - 1; | ||
| 38 | } | ||
| 39 | |||
| 40 | property color textColor: { | ||
| 41 | if (notifWindow.modelData?.[notifWindow.activeIx]?.urgency == NotificationUrgency.Low) | ||
| 42 | return "#ff999999"; | ||
| 43 | return "white"; | ||
| 44 | } | ||
| 45 | property color backgroundColor: { | ||
| 46 | if (notifWindow.modelData?.[notifWindow.activeIx]?.urgency == NotificationUrgency.Critical) | ||
| 47 | return "#dd900000"; | ||
| 48 | return "black"; | ||
| 49 | } | ||
| 50 | |||
| 51 | anchors { | ||
| 52 | right: true | ||
| 53 | top: true | ||
| 54 | } | ||
| 55 | |||
| 56 | readonly property real spaceAbove: { | ||
| 57 | var res = 0; | ||
| 58 | for (let i = 0; i < notifWindow.index; i++) { | ||
| 59 | (_ => {})(notifsRepeater.objectAt(i).modelData); | ||
| 60 | res += notifsRepeater.objectAt(i).height + 8; | ||
| 61 | } | ||
| 62 | return res; | ||
| 63 | } | ||
| 64 | |||
| 65 | margins { | ||
| 66 | right: 26 + 8 | ||
| 67 | top: 8 + spaceAbove | ||
| 68 | } | ||
| 69 | |||
| 70 | color: "transparent" | ||
| 71 | |||
| 72 | 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 | ||
| 73 | implicitWidth: 400 | ||
| 74 | |||
| 75 | WrapperMouseArea { | ||
| 76 | enabled: true | ||
| 77 | |||
| 78 | anchors.fill: parent | ||
| 79 | |||
| 80 | cursorShape: Qt.PointingHandCursor | ||
| 81 | |||
| 82 | onClicked: { | ||
| 83 | for (const notif of notifWindow.modelData) | ||
| 84 | notif.dismiss(); | ||
| 85 | } | ||
| 86 | |||
| 87 | property real angleRem: 0 | ||
| 88 | property real sensitivity: 1 / 120 | ||
| 89 | onWheel: event => { | ||
| 90 | angleRem += event.angleDelta.y; | ||
| 91 | const d = Math.round(angleRem * sensitivity); | ||
| 92 | angleRem -= d / sensitivity; | ||
| 93 | notifWindow.activeIx = ((notifWindow.modelData?.length ?? 1) + notifWindow.activeIx - d) % (notifWindow.modelData?.length ?? 1); | ||
| 94 | } | ||
| 95 | |||
| 96 | Rectangle { | ||
| 97 | color: notifWindow.backgroundColor | ||
| 98 | anchors.fill: parent | ||
| 99 | border { | ||
| 100 | color: Qt.hsla(195/360, 1, 0.45, 1) | ||
| 101 | width: 2 | ||
| 102 | } | ||
| 103 | |||
| 104 | GridLayout { | ||
| 105 | id: notifLayout | ||
| 106 | |||
| 107 | width: 400 - 16 | ||
| 108 | anchors.fill: parent | ||
| 109 | anchors.margins: 8 | ||
| 110 | columnSpacing: 8 | ||
| 111 | rowSpacing: 8 | ||
| 112 | |||
| 113 | columns: notifImage.visible ? 3 : 2 | ||
| 114 | rows: { | ||
| 115 | var res = 1; | ||
| 116 | if (notifBody.visible) | ||
| 117 | res += 1; | ||
| 118 | if (notifActions.visible) | ||
| 119 | res += 1; | ||
| 120 | if (notifTime.visible) | ||
| 121 | res += 1; | ||
| 122 | return res; | ||
| 123 | } | ||
| 124 | |||
| 125 | Text { | ||
| 126 | id: notifCount | ||
| 127 | |||
| 128 | visible: notifWindow.modelData?.length > 1 ?? false | ||
| 129 | text: `${notifWindow.activeIx + 1}/${notifWindow.modelData?.length ?? ""}` | ||
| 130 | |||
| 131 | font.pointSize: 10 | ||
| 132 | font.family: "Fira Sans" | ||
| 133 | font.bold: true | ||
| 134 | font.features: { "tnum": 1 } | ||
| 135 | color: notifWindow.textColor | ||
| 136 | maximumLineCount: 1 | ||
| 137 | |||
| 138 | Layout.fillWidth: false | ||
| 139 | Layout.row: 0 | ||
| 140 | Layout.column: notifImage.visible ? 1 : 0 | ||
| 141 | } | ||
| 142 | |||
| 143 | Text { | ||
| 144 | id: notifSummary | ||
| 145 | |||
| 146 | text: notifWindow.modelData?.[notifWindow.activeIx]?.summary ?? "" | ||
| 147 | |||
| 148 | font.pointSize: 10 | ||
| 149 | font.family: "Fira Sans" | ||
| 150 | font.italic: true | ||
| 151 | color: notifWindow.textColor | ||
| 152 | maximumLineCount: 1 | ||
| 153 | elide: Text.ElideRight | ||
| 154 | |||
| 155 | Layout.fillWidth: true | ||
| 156 | Layout.row: 0 | ||
| 157 | Layout.column: (notifCount.visible ? 1 : 0) + (notifImage.visible ? 1 : 0) | ||
| 158 | Layout.columnSpan: notifCount.visible ? 1 : 2 | ||
| 159 | } | ||
| 160 | |||
| 161 | Image { | ||
| 162 | id: notifImage | ||
| 163 | |||
| 164 | visible: (notifWindow.modelData?.[notifWindow.activeIx]?.image || notifWindow.modelData?.[notifWindow.activeIx]?.appIcon) ?? false | ||
| 165 | |||
| 166 | onStatusChanged: { | ||
| 167 | if (notifImage.status == Image.Error) | ||
| 168 | notifImage.visible = false; | ||
| 169 | } | ||
| 170 | |||
| 171 | source: (notifWindow.modelData?.[notifWindow.activeIx]?.image || notifWindow.modelData?.[notifWindow.activeIx]?.appIcon) ?? "" | ||
| 172 | fillMode: Image.PreserveAspectFit | ||
| 173 | asynchronous: true | ||
| 174 | smooth: true | ||
| 175 | mipmap: true | ||
| 176 | |||
| 177 | Layout.maximumWidth: 50 | ||
| 178 | Layout.column: 0 | ||
| 179 | Layout.row: 0 | ||
| 180 | Layout.fillHeight: true | ||
| 181 | Layout.rowSpan: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0) | ||
| 182 | } | ||
| 183 | |||
| 184 | Text { | ||
| 185 | id: notifBody | ||
| 186 | |||
| 187 | visible: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? false | ||
| 188 | text: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? "" | ||
| 189 | textFormat: Text.RichText | ||
| 190 | wrapMode: Text.Wrap | ||
| 191 | |||
| 192 | font.pointSize: 10 | ||
| 193 | font.family: "Fira Sans" | ||
| 194 | color: notifWindow.textColor | ||
| 195 | |||
| 196 | Layout.fillWidth: true | ||
| 197 | Layout.row: 1 | ||
| 198 | Layout.column: notifImage.visible ? 1 : 0 | ||
| 199 | Layout.columnSpan: notifCount.visible ? 2 : 1 | ||
| 200 | } | ||
| 201 | |||
| 202 | Text { | ||
| 203 | id: notifTime | ||
| 204 | |||
| 205 | Connections { | ||
| 206 | target: NotificationManager.clock | ||
| 207 | function onDateChanged() { | ||
| 208 | notifTime.text = NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime); | ||
| 209 | } | ||
| 210 | } | ||
| 211 | |||
| 212 | visible: notifTime.text && notifTime.text !== "now" | ||
| 213 | text: NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime) | ||
| 214 | |||
| 215 | font.pointSize: 8 | ||
| 216 | font.family: "Fira Sans" | ||
| 217 | font.italic: true | ||
| 218 | color: "#555" | ||
| 219 | maximumLineCount: 1 | ||
| 220 | horizontalAlignment: Text.AlignRight | ||
| 221 | |||
| 222 | Layout.fillWidth: true | ||
| 223 | Layout.row: notifBody.visible ? 2 : 1 | ||
| 224 | Layout.column: notifImage.visible ? 1 : 0 | ||
| 225 | Layout.columnSpan: 2 | ||
| 226 | } | ||
| 227 | |||
| 228 | RowLayout { | ||
| 229 | id: notifActions | ||
| 230 | |||
| 231 | visible: notifWindow.modelData?.[notifWindow.activeIx]?.actions.length > 0 ?? false | ||
| 232 | |||
| 233 | spacing: 8 | ||
| 234 | uniformCellSizes: true | ||
| 235 | |||
| 236 | width: 400 - 16 | ||
| 237 | Layout.row: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0) | ||
| 238 | Layout.column: 0 | ||
| 239 | Layout.columnSpan: 2 + (notifImage.visible ? 1 : 0) | ||
| 240 | |||
| 241 | Repeater { | ||
| 242 | model: ScriptModel { | ||
| 243 | values: notifWindow.modelData?.[notifWindow.activeIx]?.actions | ||
| 244 | } | ||
| 245 | |||
| 246 | delegate: WrapperMouseArea { | ||
| 247 | id: actionMouseArea | ||
| 248 | |||
| 249 | required property var modelData | ||
| 250 | |||
| 251 | height: actionLabelWrapper.implicitHeight | ||
| 252 | Layout.fillWidth: true | ||
| 253 | Layout.horizontalStretchFactor: 1 | ||
| 254 | |||
| 255 | hoverEnabled: true | ||
| 256 | cursorShape: Qt.PointingHandCursor | ||
| 257 | |||
| 258 | onClicked: actionMouseArea.modelData?.invoke() | ||
| 259 | |||
| 260 | Rectangle { | ||
| 261 | anchors.fill: parent | ||
| 262 | |||
| 263 | color: actionMouseArea.containsMouse ? "#20ffffff" : "transparent" | ||
| 264 | |||
| 265 | border { | ||
| 266 | width: 2 | ||
| 267 | color: "#20ffffff" | ||
| 268 | } | ||
| 269 | |||
| 270 | WrapperItem { | ||
| 271 | id: actionLabelWrapper | ||
| 272 | |||
| 273 | margin: 8 | ||
| 274 | anchors.centerIn: parent | ||
| 275 | |||
| 276 | RowLayout { | ||
| 277 | id: actionLabelLayout | ||
| 278 | |||
| 279 | spacing: 8 | ||
| 280 | |||
| 281 | IconImage { | ||
| 282 | id: actionIcon | ||
| 283 | |||
| 284 | visible: notifWindow.modelData?.[notifWindow.activeIx]?.hasActionIcons | ||
| 285 | |||
| 286 | onStatusChanged: { | ||
| 287 | if (actionIcon.status == Image.Error) | ||
| 288 | actionIcon.visible = false; | ||
| 289 | } | ||
| 290 | |||
| 291 | implicitSize: 16 | ||
| 292 | source: { | ||
| 293 | if (!actionIcon.visible) | ||
| 294 | return ""; | ||
| 295 | |||
| 296 | let icon = actionMouseArea.modelData?.identifier ?? "" | ||
| 297 | if (icon.includes("?path=")) { | ||
| 298 | const split = icon.split("?path=") | ||
| 299 | if (split.length !== 2) | ||
| 300 | return icon | ||
| 301 | const name = split[0] | ||
| 302 | const path = split[1] | ||
| 303 | const fileName = name.substring( | ||
| 304 | name.lastIndexOf("/") + 1) | ||
| 305 | return `file://${path}/${fileName}` | ||
| 306 | } | ||
| 307 | return icon | ||
| 308 | } | ||
| 309 | asynchronous: true | ||
| 310 | smooth: true | ||
| 311 | mipmap: true | ||
| 312 | |||
| 313 | Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter | ||
| 314 | } | ||
| 315 | |||
| 316 | Text { | ||
| 317 | id: actionLabel | ||
| 318 | |||
| 319 | visible: actionMouseArea.modelData?.text ?? false | ||
| 320 | |||
| 321 | text: actionMouseArea.modelData?.text ?? "" | ||
| 322 | |||
| 323 | font.pointSize: 10 | ||
| 324 | font.family: "Fira Sans" | ||
| 325 | color: notifWindow.textColor | ||
| 326 | maximumLineCount: 1 | ||
| 327 | elide: Text.ElideRight | ||
| 328 | } | ||
| 329 | } | ||
| 330 | } | ||
| 331 | } | ||
| 332 | } | ||
| 333 | } | ||
| 334 | } | ||
| 335 | } | ||
| 336 | } | ||
| 337 | } | ||
| 338 | } | ||
| 339 | } | ||
| 340 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml b/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml new file mode 100644 index 00000000..b58467b3 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml | |||
| @@ -0,0 +1,266 @@ | |||
| 1 | import Quickshell | ||
| 2 | import QtQuick | ||
| 3 | import Quickshell.Widgets | ||
| 4 | import qs.Services | ||
| 5 | import QtQuick.Controls | ||
| 6 | import QtQuick.Layouts | ||
| 7 | import QtQuick.Shapes | ||
| 8 | |||
| 9 | Item { | ||
| 10 | id: root | ||
| 11 | |||
| 12 | width: icon.width + 8 | ||
| 13 | height: parent.height | ||
| 14 | anchors.verticalCenter: parent.verticalCenter | ||
| 15 | |||
| 16 | WrapperMouseArea { | ||
| 17 | id: widgetMouseArea | ||
| 18 | |||
| 19 | anchors.fill: parent | ||
| 20 | |||
| 21 | hoverEnabled: true | ||
| 22 | cursorShape: Qt.PointingHandCursor | ||
| 23 | |||
| 24 | onClicked: NotificationManager.displayInhibited = !NotificationManager.displayInhibited | ||
| 25 | |||
| 26 | Rectangle { | ||
| 27 | anchors.fill: parent | ||
| 28 | color: { | ||
| 29 | if (widgetMouseArea.containsMouse) { | ||
| 30 | return "#33808080"; | ||
| 31 | } | ||
| 32 | return "transparent"; | ||
| 33 | } | ||
| 34 | |||
| 35 | Item { | ||
| 36 | anchors.fill: parent | ||
| 37 | |||
| 38 | MaterialDesignIcon { | ||
| 39 | id: icon | ||
| 40 | |||
| 41 | implicitSize: 14 | ||
| 42 | anchors.centerIn: parent | ||
| 43 | |||
| 44 | icon: NotificationManager.active ? "message" : "message-off" | ||
| 45 | color: { | ||
| 46 | if (!NotificationManager.active && !NotificationManager.displayInhibited) | ||
| 47 | return "#f28a21"; | ||
| 48 | if (NotificationManager.displayInhibited) | ||
| 49 | return "white"; | ||
| 50 | return "#555"; | ||
| 51 | } | ||
| 52 | } | ||
| 53 | } | ||
| 54 | } | ||
| 55 | } | ||
| 56 | |||
| 57 | Loader { | ||
| 58 | id: tooltipLoader | ||
| 59 | |||
| 60 | active: false | ||
| 61 | |||
| 62 | Connections { | ||
| 63 | target: widgetMouseArea | ||
| 64 | function onContainsMouseChanged() { | ||
| 65 | if (widgetMouseArea.containsMouse) | ||
| 66 | tooltipLoader.active = true; | ||
| 67 | } | ||
| 68 | } | ||
| 69 | |||
| 70 | sourceComponent: PopupWindow { | ||
| 71 | id: tooltip | ||
| 72 | |||
| 73 | property bool nextVisible: NotificationManager.active && (widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse) | ||
| 74 | |||
| 75 | anchor { | ||
| 76 | item: widgetMouseArea | ||
| 77 | edges: Edges.Bottom | Edges.Left | ||
| 78 | } | ||
| 79 | visible: false | ||
| 80 | |||
| 81 | onNextVisibleChanged: hangTimer.restart() | ||
| 82 | |||
| 83 | Timer { | ||
| 84 | id: hangTimer | ||
| 85 | interval: tooltip.visible ? 100 : 500 | ||
| 86 | onTriggered: { | ||
| 87 | tooltip.visible = tooltip.nextVisible; | ||
| 88 | if (!tooltip.visible) | ||
| 89 | tooltipLoader.active = false; | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | implicitWidth: 400 | ||
| 94 | implicitHeight: Math.min(tooltip.screen.height * 0.66, Math.max(100, scroll.contentHeight + 16)) | ||
| 95 | color: "black" | ||
| 96 | |||
| 97 | WrapperMouseArea { | ||
| 98 | id: tooltipMouseArea | ||
| 99 | |||
| 100 | hoverEnabled: true | ||
| 101 | enabled: true | ||
| 102 | |||
| 103 | anchors.fill: parent | ||
| 104 | |||
| 105 | WrapperItem { | ||
| 106 | margin: 8 | ||
| 107 | |||
| 108 | ScrollView { | ||
| 109 | id: scroll | ||
| 110 | |||
| 111 | contentWidth: availableWidth | ||
| 112 | // ScrollBar.vertical.policy: ScrollBar.AlwaysOn | ||
| 113 | |||
| 114 | ColumnLayout { | ||
| 115 | id: historyLayout | ||
| 116 | anchors { | ||
| 117 | left: parent.left | ||
| 118 | right: parent.right | ||
| 119 | } | ||
| 120 | |||
| 121 | spacing: 8 | ||
| 122 | |||
| 123 | Repeater { | ||
| 124 | model: ScriptModel { | ||
| 125 | values: [...NotificationManager.history].reverse().map(o => o.notification) | ||
| 126 | } | ||
| 127 | |||
| 128 | delegate: GridLayout { | ||
| 129 | id: notif | ||
| 130 | |||
| 131 | Layout.fillWidth: true | ||
| 132 | Layout.preferredHeight: notifSummary.contentHeight + (notifBody.visible ? notifBody.contentHeight + 8 : 0) + (notifSep.visible ? notifSep.height + 8 : 0) + notifTime.contentHeight + 8 | ||
| 133 | |||
| 134 | required property var modelData | ||
| 135 | required property int index | ||
| 136 | |||
| 137 | columnSpacing: 8 | ||
| 138 | rowSpacing: 8 | ||
| 139 | |||
| 140 | columns: notifImage.visible ? 2 : 1 | ||
| 141 | rows: { | ||
| 142 | var res = 2; | ||
| 143 | if (notifBody.visible) | ||
| 144 | res += 1; | ||
| 145 | if (notifSep.visible) | ||
| 146 | res += 1; | ||
| 147 | return res; | ||
| 148 | } | ||
| 149 | |||
| 150 | Shape { | ||
| 151 | id: notifSep | ||
| 152 | |||
| 153 | visible: notif.index != 0 | ||
| 154 | |||
| 155 | height: 2 | ||
| 156 | width: 400 - 32 | ||
| 157 | |||
| 158 | ShapePath { | ||
| 159 | strokeWidth: 2 | ||
| 160 | strokeColor: "#20ffffff" | ||
| 161 | startX: 0; startY: 0; | ||
| 162 | PathLine { x: 400 - 32; y: 0; } | ||
| 163 | } | ||
| 164 | |||
| 165 | Layout.row: 0 | ||
| 166 | Layout.column: 0 | ||
| 167 | Layout.columnSpan: notifImage.visible ? 2 : 1 | ||
| 168 | Layout.alignment: Qt.AlignHCenter | ||
| 169 | } | ||
| 170 | |||
| 171 | Text { | ||
| 172 | id: notifSummary | ||
| 173 | |||
| 174 | text: notif.modelData?.summary ?? "" | ||
| 175 | |||
| 176 | font.pointSize: 10 | ||
| 177 | font.family: "Fira Sans" | ||
| 178 | font.italic: true | ||
| 179 | color: "white" | ||
| 180 | maximumLineCount: 1 | ||
| 181 | elide: Text.ElideRight | ||
| 182 | |||
| 183 | Layout.fillWidth: true | ||
| 184 | Layout.row: notifSep.visible ? 1 : 0 | ||
| 185 | Layout.column: notifImage.visible ? 1 : 0 | ||
| 186 | } | ||
| 187 | |||
| 188 | Image { | ||
| 189 | id: notifImage | ||
| 190 | |||
| 191 | visible: (notif.modelData?.image || notif.modelData?.appIcon) ?? false | ||
| 192 | |||
| 193 | onStatusChanged: { | ||
| 194 | if (notifImage.status == Image.Error) | ||
| 195 | notifImage.visible = false; | ||
| 196 | } | ||
| 197 | |||
| 198 | source: (notif.modelData?.image || notif.modelData?.appIcon) ?? "" | ||
| 199 | fillMode: Image.PreserveAspectFit | ||
| 200 | asynchronous: true | ||
| 201 | smooth: true | ||
| 202 | mipmap: true | ||
| 203 | |||
| 204 | Layout.maximumWidth: 50 | ||
| 205 | Layout.column: 0 | ||
| 206 | Layout.row: notifSep.visible ? 1 : 0 | ||
| 207 | Layout.fillHeight: true | ||
| 208 | Layout.rowSpan: notifBody.visible ? 3 : 2 | ||
| 209 | } | ||
| 210 | |||
| 211 | Text { | ||
| 212 | id: notifBody | ||
| 213 | |||
| 214 | visible: notif.modelData?.body ?? false | ||
| 215 | text: notif.modelData?.body ?? "" | ||
| 216 | textFormat: Text.RichText | ||
| 217 | wrapMode: Text.Wrap | ||
| 218 | |||
| 219 | font.pointSize: 10 | ||
| 220 | font.family: "Fira Sans" | ||
| 221 | color: "white" | ||
| 222 | |||
| 223 | Layout.fillWidth: true | ||
| 224 | Layout.row: notifSep.visible ? 2 : 1 | ||
| 225 | Layout.column: notifImage.visible ? 1 : 0 | ||
| 226 | } | ||
| 227 | |||
| 228 | Text { | ||
| 229 | id: notifTime | ||
| 230 | |||
| 231 | Connections { | ||
| 232 | target: NotificationManager.clock | ||
| 233 | function onDateChanged() { | ||
| 234 | notifTime.text = NotificationManager.formatTime(notif.modelData?.receivedTime); | ||
| 235 | } | ||
| 236 | } | ||
| 237 | |||
| 238 | text: NotificationManager.formatTime(notif.modelData?.receivedTime) | ||
| 239 | |||
| 240 | font.pointSize: 8 | ||
| 241 | font.family: "Fira Sans" | ||
| 242 | font.italic: true | ||
| 243 | color: "#555" | ||
| 244 | maximumLineCount: 1 | ||
| 245 | horizontalAlignment: Text.AlignRight | ||
| 246 | |||
| 247 | Layout.fillWidth: true | ||
| 248 | Layout.row: { | ||
| 249 | var res = 1; | ||
| 250 | if (notifSep.visible) | ||
| 251 | res += 1; | ||
| 252 | if (notifBody.visible) | ||
| 253 | res += 1; | ||
| 254 | return res; | ||
| 255 | } | ||
| 256 | Layout.column: notifImage.visible ? 1 : 0 | ||
| 257 | } | ||
| 258 | } | ||
| 259 | } | ||
| 260 | } | ||
| 261 | } | ||
| 262 | } | ||
| 263 | } | ||
| 264 | } | ||
| 265 | } | ||
| 266 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml new file mode 100644 index 00000000..9c6b65a4 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml | |||
| @@ -0,0 +1,483 @@ | |||
| 1 | import QtQuick | ||
| 2 | import QtQuick.Layouts | ||
| 3 | import QtQuick.Controls.Fusion | ||
| 4 | import Quickshell | ||
| 5 | import Quickshell.Services.Pipewire | ||
| 6 | import Quickshell.Widgets | ||
| 7 | |||
| 8 | Item { | ||
| 9 | height: parent.height | ||
| 10 | width: volumeIcon.width + 8 | ||
| 11 | anchors.verticalCenter: parent.verticalCenter | ||
| 12 | |||
| 13 | PwObjectTracker { | ||
| 14 | objects: [Pipewire.defaultAudioSink] | ||
| 15 | } | ||
| 16 | |||
| 17 | WrapperMouseArea { | ||
| 18 | id: widgetMouseArea | ||
| 19 | |||
| 20 | anchors.fill: parent | ||
| 21 | hoverEnabled: true | ||
| 22 | cursorShape: Qt.PointingHandCursor | ||
| 23 | |||
| 24 | onClicked: { | ||
| 25 | if (!Pipewire.defaultAudioSink) | ||
| 26 | return; | ||
| 27 | Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted; | ||
| 28 | } | ||
| 29 | |||
| 30 | property real sensitivity: (1 / 40) / 120 | ||
| 31 | onWheel: event => { | ||
| 32 | if (!Pipewire.defaultAudioSink) | ||
| 33 | return; | ||
| 34 | Pipewire.defaultAudioSink.audio.volume += event.angleDelta.y * sensitivity; | ||
| 35 | } | ||
| 36 | |||
| 37 | Rectangle { | ||
| 38 | id: volumeWidget | ||
| 39 | |||
| 40 | anchors.fill: parent | ||
| 41 | color: { | ||
| 42 | if (widgetMouseArea.containsMouse) | ||
| 43 | return "#33808080"; | ||
| 44 | return "transparent"; | ||
| 45 | } | ||
| 46 | |||
| 47 | Item { | ||
| 48 | anchors.fill: parent | ||
| 49 | |||
| 50 | MaterialDesignIcon { | ||
| 51 | id: volumeIcon | ||
| 52 | |||
| 53 | implicitSize: 14 | ||
| 54 | anchors.centerIn: parent | ||
| 55 | |||
| 56 | icon: { | ||
| 57 | if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted) | ||
| 58 | return "volume-off"; | ||
| 59 | if (Pipewire.defaultAudioSink.audio.volume <= 0.33) | ||
| 60 | return "volume-low"; | ||
| 61 | if (Pipewire.defaultAudioSink.audio.volume <= 0.67) | ||
| 62 | return "volume-medium"; | ||
| 63 | return "volume-high"; | ||
| 64 | } | ||
| 65 | color: "#555" | ||
| 66 | } | ||
| 67 | } | ||
| 68 | } | ||
| 69 | } | ||
| 70 | |||
| 71 | Loader { | ||
| 72 | id: tooltipLoader | ||
| 73 | |||
| 74 | active: false | ||
| 75 | |||
| 76 | Connections { | ||
| 77 | target: widgetMouseArea | ||
| 78 | function onContainsMouseChanged() { | ||
| 79 | if (widgetMouseArea.containsMouse) | ||
| 80 | tooltipLoader.active = true; | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | PwObjectTracker { | ||
| 85 | objects: Pipewire.devices | ||
| 86 | } | ||
| 87 | PwObjectTracker { | ||
| 88 | objects: Pipewire.nodes | ||
| 89 | } | ||
| 90 | |||
| 91 | sourceComponent: PopupWindow { | ||
| 92 | id: tooltip | ||
| 93 | |||
| 94 | property bool openPopup: false | ||
| 95 | property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse || openPopup | ||
| 96 | |||
| 97 | anchor { | ||
| 98 | item: widgetMouseArea | ||
| 99 | edges: Edges.Bottom | Edges.Left | ||
| 100 | } | ||
| 101 | visible: false | ||
| 102 | |||
| 103 | onNextVisibleChanged: hangTimer.restart() | ||
| 104 | |||
| 105 | Timer { | ||
| 106 | id: hangTimer | ||
| 107 | interval: 100 | ||
| 108 | onTriggered: { | ||
| 109 | tooltip.visible = tooltip.nextVisible; | ||
| 110 | if (!tooltip.visible) | ||
| 111 | tooltipLoader.active = false; | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 115 | implicitWidth: tooltipContent.width | ||
| 116 | implicitHeight: tooltipContent.height | ||
| 117 | color: "transparent" | ||
| 118 | |||
| 119 | Rectangle { | ||
| 120 | width: tooltip.width | ||
| 121 | height: tooltipLayout.childrenRect.height + 16 | ||
| 122 | color: "black" | ||
| 123 | } | ||
| 124 | |||
| 125 | WrapperItem { | ||
| 126 | id: tooltipContent | ||
| 127 | |||
| 128 | bottomMargin: Math.max(0, 200 - tooltipLayout.implicitHeight) | ||
| 129 | |||
| 130 | WrapperMouseArea { | ||
| 131 | id: tooltipMouseArea | ||
| 132 | |||
| 133 | hoverEnabled: true | ||
| 134 | enabled: true | ||
| 135 | |||
| 136 | WrapperItem { | ||
| 137 | margin: 8 | ||
| 138 | bottomMargin: 8 | ||
| 139 | |||
| 140 | GridLayout { | ||
| 141 | id: tooltipLayout | ||
| 142 | |||
| 143 | columns: 4 | ||
| 144 | |||
| 145 | Repeater { | ||
| 146 | model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device") | ||
| 147 | |||
| 148 | Item { | ||
| 149 | id: descItem | ||
| 150 | |||
| 151 | required property var modelData | ||
| 152 | required property int index | ||
| 153 | |||
| 154 | Layout.column: 0 | ||
| 155 | Layout.row: index | ||
| 156 | |||
| 157 | implicitWidth: descText.contentWidth | ||
| 158 | implicitHeight: descText.contentHeight | ||
| 159 | |||
| 160 | Text { | ||
| 161 | id: descText | ||
| 162 | |||
| 163 | color: "white" | ||
| 164 | font.pointSize: 10 | ||
| 165 | font.family: "Fira Sans" | ||
| 166 | |||
| 167 | text: descItem.modelData.description | ||
| 168 | } | ||
| 169 | } | ||
| 170 | } | ||
| 171 | |||
| 172 | Repeater { | ||
| 173 | id: defaultSinkRepeater | ||
| 174 | |||
| 175 | model: { | ||
| 176 | Array.from(Pipewire.devices.values) | ||
| 177 | .filter(dev => dev.type == "Audio/Device") | ||
| 178 | .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSink && node.device?.id == device.id )); | ||
| 179 | } | ||
| 180 | |||
| 181 | Item { | ||
| 182 | id: defaultSinkItem | ||
| 183 | |||
| 184 | required property var modelData | ||
| 185 | required property int index | ||
| 186 | |||
| 187 | visible: Boolean(modelData) | ||
| 188 | |||
| 189 | PwObjectTracker { | ||
| 190 | objects: [defaultSinkItem.modelData] | ||
| 191 | } | ||
| 192 | |||
| 193 | Layout.column: 1 | ||
| 194 | Layout.row: index | ||
| 195 | |||
| 196 | Layout.fillHeight: true | ||
| 197 | |||
| 198 | implicitWidth: 16 + 8 | ||
| 199 | |||
| 200 | WrapperMouseArea { | ||
| 201 | id: defaultSinkMouseArea | ||
| 202 | |||
| 203 | anchors.fill: parent | ||
| 204 | hoverEnabled: true | ||
| 205 | cursorShape: Qt.PointingHandCursor | ||
| 206 | |||
| 207 | onClicked: { | ||
| 208 | Pipewire.preferredDefaultAudioSink = defaultSinkItem.modelData | ||
| 209 | } | ||
| 210 | |||
| 211 | onWheel: event => scrollVolume(event); | ||
| 212 | property real sensitivity: (1 / 40) / 120 | ||
| 213 | function scrollVolume(event) { | ||
| 214 | defaultSinkItem.modelData.audio.volume += event.angleDelta.y * sensitivity; | ||
| 215 | } | ||
| 216 | |||
| 217 | Rectangle { | ||
| 218 | id: defaultSinkWidget | ||
| 219 | |||
| 220 | anchors.fill: parent | ||
| 221 | color: { | ||
| 222 | if (defaultSinkMouseArea.containsMouse) | ||
| 223 | return "#33808080"; | ||
| 224 | return "transparent"; | ||
| 225 | } | ||
| 226 | |||
| 227 | MaterialDesignIcon { | ||
| 228 | width: 16 | ||
| 229 | height: 16 | ||
| 230 | anchors.centerIn: parent | ||
| 231 | |||
| 232 | icon: { | ||
| 233 | if (defaultSinkItem.modelData?.id == Pipewire.defaultAudioSink?.id) | ||
| 234 | return "speaker"; | ||
| 235 | return "speaker-off"; | ||
| 236 | } | ||
| 237 | color: icon == "speaker" ? "white" : "#555" | ||
| 238 | } | ||
| 239 | } | ||
| 240 | } | ||
| 241 | |||
| 242 | PopupWindow { | ||
| 243 | id: volumeTooltip | ||
| 244 | |||
| 245 | property bool nextVisible: defaultSinkMouseArea.containsMouse || volumeTooltipMouseArea.containsMouse | ||
| 246 | |||
| 247 | anchor { | ||
| 248 | item: defaultSinkMouseArea | ||
| 249 | edges: Edges.Bottom | Edges.Left | ||
| 250 | } | ||
| 251 | visible: false | ||
| 252 | |||
| 253 | onNextVisibleChanged: volumeHangTimer.restart() | ||
| 254 | |||
| 255 | onVisibleChanged: tooltip.openPopup = volumeTooltip.visible | ||
| 256 | |||
| 257 | Timer { | ||
| 258 | id: volumeHangTimer | ||
| 259 | interval: 100 | ||
| 260 | onTriggered: volumeTooltip.visible = volumeTooltip.nextVisible | ||
| 261 | } | ||
| 262 | |||
| 263 | implicitWidth: volumeTooltipText.contentWidth + 16 | ||
| 264 | implicitHeight: volumeTooltipText.contentHeight + 16 | ||
| 265 | color: "black" | ||
| 266 | |||
| 267 | WrapperMouseArea { | ||
| 268 | id: volumeTooltipMouseArea | ||
| 269 | |||
| 270 | hoverEnabled: true | ||
| 271 | enabled: true | ||
| 272 | |||
| 273 | onWheel: event => defaultSinkMouseArea.scrollVolume(event); | ||
| 274 | |||
| 275 | anchors.fill: parent | ||
| 276 | |||
| 277 | Item { | ||
| 278 | anchors.fill: parent | ||
| 279 | |||
| 280 | Text { | ||
| 281 | id: volumeTooltipText | ||
| 282 | |||
| 283 | anchors.centerIn: parent | ||
| 284 | |||
| 285 | font.pointSize: 10 | ||
| 286 | font.family: "Fira Sans" | ||
| 287 | color: "white" | ||
| 288 | |||
| 289 | text: `${Math.round(defaultSinkItem.modelData?.audio?.volume * 100)}%` | ||
| 290 | } | ||
| 291 | } | ||
| 292 | } | ||
| 293 | } | ||
| 294 | } | ||
| 295 | } | ||
| 296 | |||
| 297 | Repeater { | ||
| 298 | id: defaultSourceRepeater | ||
| 299 | |||
| 300 | model: { | ||
| 301 | Array.from(Pipewire.devices.values) | ||
| 302 | .filter(dev => dev.type == "Audio/Device") | ||
| 303 | .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSource && node.device?.id == device.id )); | ||
| 304 | } | ||
| 305 | |||
| 306 | Item { | ||
| 307 | id: defaultSourceItem | ||
| 308 | |||
| 309 | required property var modelData | ||
| 310 | required property int index | ||
| 311 | |||
| 312 | visible: Boolean(modelData) | ||
| 313 | |||
| 314 | PwObjectTracker { | ||
| 315 | objects: [defaultSourceItem.modelData] | ||
| 316 | } | ||
| 317 | |||
| 318 | Layout.column: 2 | ||
| 319 | Layout.row: index | ||
| 320 | |||
| 321 | Layout.fillHeight: true | ||
| 322 | |||
| 323 | implicitWidth: 16 + 8 | ||
| 324 | |||
| 325 | WrapperMouseArea { | ||
| 326 | id: defaultSourceMouseArea | ||
| 327 | |||
| 328 | anchors.fill: parent | ||
| 329 | hoverEnabled: true | ||
| 330 | cursorShape: Qt.PointingHandCursor | ||
| 331 | |||
| 332 | onClicked: { | ||
| 333 | Pipewire.preferredDefaultAudioSource = defaultSourceItem.modelData | ||
| 334 | } | ||
| 335 | |||
| 336 | onWheel: event => scrollVolume(event); | ||
| 337 | property real sensitivity: (1 / 40) / 120 | ||
| 338 | function scrollVolume(event) { | ||
| 339 | defaultSourceItem.modelData.audio.volume += event.angleDelta.y * sensitivity; | ||
| 340 | } | ||
| 341 | |||
| 342 | Rectangle { | ||
| 343 | id: defaultSourceWidget | ||
| 344 | |||
| 345 | anchors.fill: parent | ||
| 346 | color: { | ||
| 347 | if (defaultSourceMouseArea.containsMouse) | ||
| 348 | return "#33808080"; | ||
| 349 | return "transparent"; | ||
| 350 | } | ||
| 351 | |||
| 352 | MaterialDesignIcon { | ||
| 353 | width: 16 | ||
| 354 | height: 16 | ||
| 355 | anchors.centerIn: parent | ||
| 356 | |||
| 357 | icon: { | ||
| 358 | if (defaultSourceItem.modelData?.id == Pipewire.defaultAudioSource?.id) | ||
| 359 | return "microphone"; | ||
| 360 | return "microphone-off"; | ||
| 361 | } | ||
| 362 | color: icon == "microphone" ? "white" : "#555" | ||
| 363 | } | ||
| 364 | } | ||
| 365 | } | ||
| 366 | |||
| 367 | PopupWindow { | ||
| 368 | id: volumeTooltip | ||
| 369 | |||
| 370 | property bool nextVisible: defaultSourceMouseArea.containsMouse || volumeTooltipMouseArea.containsMouse | ||
| 371 | |||
| 372 | anchor { | ||
| 373 | item: defaultSourceMouseArea | ||
| 374 | edges: Edges.Bottom | Edges.Left | ||
| 375 | } | ||
| 376 | visible: false | ||
| 377 | |||
| 378 | onNextVisibleChanged: volumeHangTimer.restart() | ||
| 379 | |||
| 380 | onVisibleChanged: tooltip.openPopup = volumeTooltip.visible | ||
| 381 | |||
| 382 | Timer { | ||
| 383 | id: volumeHangTimer | ||
| 384 | interval: 100 | ||
| 385 | onTriggered: volumeTooltip.visible = volumeTooltip.nextVisible | ||
| 386 | } | ||
| 387 | |||
| 388 | implicitWidth: volumeTooltipText.contentWidth + 16 | ||
| 389 | implicitHeight: volumeTooltipText.contentHeight + 16 | ||
| 390 | color: "black" | ||
| 391 | |||
| 392 | WrapperMouseArea { | ||
| 393 | id: volumeTooltipMouseArea | ||
| 394 | |||
| 395 | hoverEnabled: true | ||
| 396 | enabled: true | ||
| 397 | |||
| 398 | onWheel: event => defaultSourceMouseArea.scrollVolume(event); | ||
| 399 | |||
| 400 | anchors.fill: parent | ||
| 401 | |||
| 402 | Item { | ||
| 403 | anchors.fill: parent | ||
| 404 | |||
| 405 | Text { | ||
| 406 | id: volumeTooltipText | ||
| 407 | |||
| 408 | anchors.centerIn: parent | ||
| 409 | |||
| 410 | font.pointSize: 10 | ||
| 411 | font.family: "Fira Sans" | ||
| 412 | color: "white" | ||
| 413 | |||
| 414 | text: `${Math.round(defaultSourceItem.modelData?.audio?.volume * 100)}%` | ||
| 415 | } | ||
| 416 | } | ||
| 417 | } | ||
| 418 | } | ||
| 419 | } | ||
| 420 | } | ||
| 421 | |||
| 422 | Repeater { | ||
| 423 | id: profileRepeater | ||
| 424 | |||
| 425 | model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device") | ||
| 426 | |||
| 427 | Item { | ||
| 428 | id: profileItem | ||
| 429 | |||
| 430 | required property var modelData | ||
| 431 | required property int index | ||
| 432 | |||
| 433 | PwObjectTracker { | ||
| 434 | objects: [profileItem.modelData] | ||
| 435 | } | ||
| 436 | |||
| 437 | Layout.column: 3 | ||
| 438 | Layout.row: index | ||
| 439 | |||
| 440 | Layout.fillWidth: true | ||
| 441 | |||
| 442 | implicitWidth: Math.max(profileBox.implicitWidth, 300) | ||
| 443 | implicitHeight: profileBox.height | ||
| 444 | |||
| 445 | ComboBox { | ||
| 446 | id: profileBox | ||
| 447 | |||
| 448 | model: profileItem.modelData.profiles | ||
| 449 | |||
| 450 | textRole: "description" | ||
| 451 | valueRole: "index" | ||
| 452 | onActivated: profileItem.modelData.setProfile(currentValue) | ||
| 453 | |||
| 454 | anchors.fill: parent | ||
| 455 | |||
| 456 | implicitContentWidthPolicy: ComboBox.WidestText | ||
| 457 | |||
| 458 | Connections { | ||
| 459 | target: profileItem.modelData | ||
| 460 | function onCurrentProfileChanged() { | ||
| 461 | profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile); | ||
| 462 | } | ||
| 463 | } | ||
| 464 | Component.onCompleted: { | ||
| 465 | profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile); | ||
| 466 | } | ||
| 467 | |||
| 468 | Connections { | ||
| 469 | target: profileBox.popup | ||
| 470 | function onVisibleChanged() { | ||
| 471 | tooltip.openPopup = profileBox.popup.visible | ||
| 472 | } | ||
| 473 | } | ||
| 474 | } | ||
| 475 | } | ||
| 476 | } | ||
| 477 | } | ||
| 478 | } | ||
| 479 | } | ||
| 480 | } | ||
| 481 | } | ||
| 482 | } | ||
| 483 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/PrivacyWidget.qml b/accounts/gkleen@sif/shell/quickshell/PrivacyWidget.qml new file mode 100644 index 00000000..d7ffadfe --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/PrivacyWidget.qml | |||
| @@ -0,0 +1,49 @@ | |||
| 1 | import QtQuick | ||
| 2 | import QtQuick.Layouts | ||
| 3 | import Quickshell | ||
| 4 | import Quickshell.Widgets | ||
| 5 | import qs.Services | ||
| 6 | |||
| 7 | Item { | ||
| 8 | height: parent.height | ||
| 9 | width: layout.childrenRect.width | ||
| 10 | anchors.verticalCenter: parent.verticalCenter | ||
| 11 | |||
| 12 | visible: Array.from(Privacy.activeItems).length > 0 | ||
| 13 | |||
| 14 | RowLayout { | ||
| 15 | id: layout | ||
| 16 | |||
| 17 | anchors.fill: parent | ||
| 18 | |||
| 19 | spacing: 8 | ||
| 20 | |||
| 21 | Repeater { | ||
| 22 | model: Privacy.activeItems | ||
| 23 | |||
| 24 | Item { | ||
| 25 | id: privacyItem | ||
| 26 | |||
| 27 | required property var modelData; | ||
| 28 | |||
| 29 | height: parent.height | ||
| 30 | width: icon.width | ||
| 31 | |||
| 32 | MaterialDesignIcon { | ||
| 33 | id: icon | ||
| 34 | |||
| 35 | implicitSize: 14 | ||
| 36 | anchors.centerIn: parent | ||
| 37 | |||
| 38 | icon: { | ||
| 39 | if (privacyItem.modelData == Privacy.Item.Microphone) | ||
| 40 | return "microphone"; | ||
| 41 | if (privacyItem.modelData == Privacy.Item.Screensharing) | ||
| 42 | return "monitor-share"; | ||
| 43 | } | ||
| 44 | color: "#f2201f" | ||
| 45 | } | ||
| 46 | } | ||
| 47 | } | ||
| 48 | } | ||
| 49 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml b/accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml new file mode 100644 index 00000000..8318df50 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | pragma Singleton | ||
| 2 | |||
| 3 | import QtQml | ||
| 4 | import Quickshell | ||
| 5 | import Quickshell.Io | ||
| 6 | import Custom as Custom | ||
| 7 | |||
| 8 | Singleton { | ||
| 9 | id: root | ||
| 10 | |||
| 11 | property string subsystem: "backlight" | ||
| 12 | property string device: "intel_backlight" | ||
| 13 | |||
| 14 | property real currBrightness | ||
| 15 | property real exponent: 4 | ||
| 16 | |||
| 17 | function calcCurrBrightness() { | ||
| 18 | if (!currFile.loaded || !maxFile.loaded) | ||
| 19 | return undefined; | ||
| 20 | const curr = Number(currFile.text()); | ||
| 21 | const max = Number(maxFile.text()); | ||
| 22 | const val = Math.pow(curr / max, 1 / root.exponent); | ||
| 23 | return val; | ||
| 24 | } | ||
| 25 | |||
| 26 | Connections { | ||
| 27 | target: currFile | ||
| 28 | function onLoaded() { | ||
| 29 | const b = root.calcCurrBrightness(); | ||
| 30 | if (typeof b !== 'undefined') | ||
| 31 | root.currBrightness = b; | ||
| 32 | } | ||
| 33 | } | ||
| 34 | Connections { | ||
| 35 | target: maxFile | ||
| 36 | function onLoaded() { | ||
| 37 | const b = root.calcCurrBrightness(); | ||
| 38 | if (typeof b !== 'undefined') | ||
| 39 | root.currBrightness = b; | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | onCurrBrightnessChanged: { | ||
| 44 | root.currBrightness = Math.max(0, Math.min(1, root.currBrightness)); | ||
| 45 | |||
| 46 | const prev = root.calcCurrBrightness(); | ||
| 47 | if (typeof prev === 'undefined' || Math.abs(root.currBrightness - prev) < 0.01) | ||
| 48 | return; | ||
| 49 | |||
| 50 | const max = Number(maxFile.text()); | ||
| 51 | const actual = Number(currFile.text()); | ||
| 52 | let curr = Math.max(0, Math.min(max, Math.pow(root.currBrightness, root.exponent) * max)); | ||
| 53 | if (Math.round(curr) == actual && curr < actual) | ||
| 54 | curr = Math.max(0, actual - 1); | ||
| 55 | else if (Math.round(curr) == actual && curr > actual) | ||
| 56 | curr = Math.min(max, actual + 1); | ||
| 57 | // root.currBrightness = Math.pow(curr / max, 1 / root.exponent); | ||
| 58 | Custom.Systemd.setBrightness(root.subsystem, root.device, Math.round(curr)); | ||
| 59 | } | ||
| 60 | |||
| 61 | FileView { | ||
| 62 | id: currFile | ||
| 63 | path: `/sys/class/${root.subsystem}/${root.device}/brightness` | ||
| 64 | blockAllReads: true | ||
| 65 | watchChanges: true | ||
| 66 | onFileChanged: reload() | ||
| 67 | } | ||
| 68 | FileView { | ||
| 69 | id: maxFile | ||
| 70 | path: `/sys/class/${root.subsystem}/${root.device}/max_brightness` | ||
| 71 | blockAllReads: true | ||
| 72 | watchChanges: true | ||
| 73 | onFileChanged: reload() | ||
| 74 | } | ||
| 75 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml b/accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml new file mode 100644 index 00000000..3de69535 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | pragma Singleton | ||
| 2 | |||
| 3 | import Quickshell | ||
| 4 | import Quickshell.Io | ||
| 5 | |||
| 6 | Singleton { | ||
| 7 | id: root | ||
| 8 | |||
| 9 | Socket { | ||
| 10 | id: agentSocket | ||
| 11 | connected: true | ||
| 12 | path: `${Quickshell.env("XDG_RUNTIME_DIR")}/gnupg/S.gpg-agent` | ||
| 13 | } | ||
| 14 | |||
| 15 | function reloadAgent() { | ||
| 16 | agentSocket.write("RELOADAGENT\n") | ||
| 17 | } | ||
| 18 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/InhibitorState.qml b/accounts/gkleen@sif/shell/quickshell/Services/InhibitorState.qml new file mode 100644 index 00000000..fe48fd7f --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/Services/InhibitorState.qml | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | pragma Singleton | ||
| 2 | |||
| 3 | import Quickshell | ||
| 4 | import Custom as Custom | ||
| 5 | |||
| 6 | Singleton { | ||
| 7 | id: inhibitorState | ||
| 8 | |||
| 9 | property bool waylandIdleInhibited: false | ||
| 10 | property alias lidSwitchInhibited: lidSwitchInhibitor.enabled | ||
| 11 | |||
| 12 | Custom.SystemdInhibitor { | ||
| 13 | id: lidSwitchInhibitor | ||
| 14 | |||
| 15 | enabled: false | ||
| 16 | |||
| 17 | what: Custom.SystemdInhibitorParams.HandleLidSwitch | ||
| 18 | who: "quickshell" | ||
| 19 | why: "User request" | ||
| 20 | mode: Custom.SystemdInhibitorParams.BlockWeak | ||
| 21 | } | ||
| 22 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/MprisProxy.qml b/accounts/gkleen@sif/shell/quickshell/Services/MprisProxy.qml new file mode 100644 index 00000000..e3ab9755 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/Services/MprisProxy.qml | |||
| @@ -0,0 +1,8 @@ | |||
| 1 | pragma Singleton | ||
| 2 | |||
| 3 | import Quickshell | ||
| 4 | import Quickshell.Services.Mpris | ||
| 5 | |||
| 6 | Scope { | ||
| 7 | property list<var> players: Mpris.players.values | ||
| 8 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml b/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml new file mode 100644 index 00000000..cce614eb --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml | |||
| @@ -0,0 +1,194 @@ | |||
| 1 | pragma Singleton | ||
| 2 | |||
| 3 | import Quickshell | ||
| 4 | import Quickshell.Io | ||
| 5 | import QtQuick | ||
| 6 | |||
| 7 | Singleton { | ||
| 8 | id: root | ||
| 9 | |||
| 10 | property var workspaces: [] | ||
| 11 | property var outputs: {} | ||
| 12 | property var keyboardLayouts: {} | ||
| 13 | property var windows: [] | ||
| 14 | readonly property string socketPath: Quickshell.env("NIRI_SOCKET") | ||
| 15 | |||
| 16 | function refreshOutputs() { | ||
| 17 | commandSocket.sendCommand("Outputs", data => { | ||
| 18 | outputs = data.Ok.Outputs; | ||
| 19 | }); | ||
| 20 | } | ||
| 21 | |||
| 22 | function sendCommand(command, callback) { | ||
| 23 | commandSocket.sendCommand(command, callback); | ||
| 24 | } | ||
| 25 | |||
| 26 | Socket { | ||
| 27 | id: eventStreamSocket | ||
| 28 | path: root.socketPath | ||
| 29 | connected: true | ||
| 30 | |||
| 31 | property bool acked: false | ||
| 32 | |||
| 33 | onConnectionStateChanged: { | ||
| 34 | if (connected) { | ||
| 35 | acked = false; | ||
| 36 | write('"EventStream"\n'); | ||
| 37 | } | ||
| 38 | } | ||
| 39 | |||
| 40 | parser: SplitParser { | ||
| 41 | onRead: line => { | ||
| 42 | try { | ||
| 43 | const event = JSON.parse(line) | ||
| 44 | |||
| 45 | // console.log(JSON.stringify(event)) | ||
| 46 | |||
| 47 | if (event.WorkspacesChanged) { | ||
| 48 | root.workspaces = event.WorkspacesChanged.workspaces | ||
| 49 | root.refreshOutputs(); | ||
| 50 | } else if (event.WorkspaceActivated) | ||
| 51 | eventWorkspaceActivated(event.WorkspaceActivated); | ||
| 52 | else if (event.WorkspaceUrgencyChanged) | ||
| 53 | eventWorkspaceUrgencyChanged(event.WorkspaceUrgencyChanged); | ||
| 54 | else if (event.WorkspaceActiveWindowChanged) | ||
| 55 | eventWorkspaceActiveWindowChanged(event.WorkspaceActiveWindowChanged); | ||
| 56 | else if (event.KeyboardLayoutsChanged) | ||
| 57 | root.keyboardLayouts = event.KeyboardLayoutsChanged.keyboard_layouts; | ||
| 58 | else if (event.KeyboardLayoutSwitched) | ||
| 59 | root.keyboardLayouts = Object.assign({}, root.keyboardLayouts, {"current_idx": event.KeyboardLayoutSwitched.idx }); | ||
| 60 | else if (event.WindowsChanged) | ||
| 61 | root.windows = event.WindowsChanged.windows | ||
| 62 | else if (event.WindowOpenedOrChanged) | ||
| 63 | eventWindowOpenedOrChanged(event.WindowOpenedOrChanged); | ||
| 64 | else if (event.WindowClosed) | ||
| 65 | eventWindowClosed(event.WindowClosed); | ||
| 66 | else if (event.WindowFocusChanged) | ||
| 67 | eventWindowFocusChanged(event.WindowFocusChanged); | ||
| 68 | else if (event.WindowUrgencyChanged) | ||
| 69 | eventWindowUrgencyChanged(event.WindowUrgencyChanged); | ||
| 70 | else if (event.WindowLayoutsChanged) | ||
| 71 | eventWindowLayoutsChanged(event.WindowLayoutsChanged); | ||
| 72 | else if (event.Ok && !eventStreamSocket.acked) { eventStreamSocket.acked = true; } | ||
| 73 | else if (event.OverviewOpenedOrClosed) {} | ||
| 74 | else if (event.ConfigLoaded) {} | ||
| 75 | else | ||
| 76 | console.log(JSON.stringify(event)); | ||
| 77 | } catch (e) { | ||
| 78 | console.warn("NiriService: Failed to parse event:", line, e) | ||
| 79 | } | ||
| 80 | } | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | Socket { | ||
| 85 | id: commandSocket | ||
| 86 | path: root.socketPath | ||
| 87 | connected: true | ||
| 88 | |||
| 89 | property var awaitingAnswer: null | ||
| 90 | property var cmdQueue: [] | ||
| 91 | |||
| 92 | parser: SplitParser { | ||
| 93 | onRead: line => { | ||
| 94 | if (commandSocket.awaitingAnswer === null) | ||
| 95 | return; | ||
| 96 | |||
| 97 | try { | ||
| 98 | const response = JSON.parse(line); | ||
| 99 | commandSocket.awaitingAnswer.callback(response); | ||
| 100 | commandSocket.awaitingAnswer = null; | ||
| 101 | } catch (e) { | ||
| 102 | console.warn("NiriService: Failed to parse response:", line, e) | ||
| 103 | } | ||
| 104 | commandSocket._handleQueue(); | ||
| 105 | } | ||
| 106 | } | ||
| 107 | |||
| 108 | onCmdQueueChanged: { | ||
| 109 | _handleQueue(); | ||
| 110 | } | ||
| 111 | onAwaitingAnswerChanged: { | ||
| 112 | _handleQueue(); | ||
| 113 | } | ||
| 114 | |||
| 115 | function _handleQueue() { | ||
| 116 | if (cmdQueue.length <= 0 || awaitingAnswer !== null) | ||
| 117 | return; | ||
| 118 | |||
| 119 | let localQueue = Array.from(cmdQueue); | ||
| 120 | awaitingAnswer = localQueue.shift(); | ||
| 121 | cmdQueue = localQueue; | ||
| 122 | write(JSON.stringify(awaitingAnswer.command) + '\n'); | ||
| 123 | } | ||
| 124 | |||
| 125 | function sendCommand(command, callback) { | ||
| 126 | cmdQueue = Array.from(cmdQueue).concat([{ "command": command, "callback": callback }]) | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | function eventWorkspaceActivated(data) { | ||
| 131 | let relevant_output = null; | ||
| 132 | Array.from(root.workspaces).forEach(ws => { | ||
| 133 | if (data.id === ws.id) | ||
| 134 | relevant_output = ws.output; | ||
| 135 | }); | ||
| 136 | root.workspaces = Array.from(root.workspaces).map(ws => { | ||
| 137 | if (data.focused) | ||
| 138 | ws.is_focused = false; | ||
| 139 | if (ws.output === relevant_output) | ||
| 140 | ws.is_active = false; | ||
| 141 | if (data.id === ws.id) { | ||
| 142 | ws.is_active = true; | ||
| 143 | ws.is_focused = data.focused; | ||
| 144 | } | ||
| 145 | return ws; | ||
| 146 | }); | ||
| 147 | } | ||
| 148 | function eventWorkspaceUrgencyChanged(data) { | ||
| 149 | root.workspaces = Array.from(root.workspaces).map(ws => { | ||
| 150 | if (data.id == ws.id) | ||
| 151 | ws.is_urgent = data.urgent; | ||
| 152 | return ws; | ||
| 153 | }); | ||
| 154 | } | ||
| 155 | function eventWorkspaceActiveWindowChanged(data) { | ||
| 156 | root.workspaces = Array.from(root.workspaces).map(ws => { | ||
| 157 | if (data.workspace_id === ws.id) | ||
| 158 | ws.active_window_id = data.active_window_id; | ||
| 159 | return ws; | ||
| 160 | }); | ||
| 161 | } | ||
| 162 | function eventWindowOpenedOrChanged(data) { | ||
| 163 | root.windows = Array.from(root.windows).map(win => { | ||
| 164 | if (data.window.is_focused) | ||
| 165 | win.is_focused = false; | ||
| 166 | return win; | ||
| 167 | }).filter(win => win.id !== data.window.id).concat([data.window]); | ||
| 168 | } | ||
| 169 | function eventWindowClosed(data) { | ||
| 170 | root.windows = Array.from(root.windows).filter(win => win.id !== data.id); | ||
| 171 | } | ||
| 172 | function eventWindowFocusChanged(data) { | ||
| 173 | root.windows = Array.from(root.windows).map(win => { | ||
| 174 | win.is_focused = win.id === data.id; | ||
| 175 | return win; | ||
| 176 | }); | ||
| 177 | } | ||
| 178 | function eventWindowUrgencyChanged(data) { | ||
| 179 | root.windows = Array.from(root.windows).map(win => { | ||
| 180 | if (win.id === data.id) | ||
| 181 | win.is_urgent = data.urgent; | ||
| 182 | return win; | ||
| 183 | }); | ||
| 184 | } | ||
| 185 | function eventWindowLayoutsChanged(data) { | ||
| 186 | root.windows = Array.from(root.windows).map(win => { | ||
| 187 | Array.from(data.changes).forEach(change => { | ||
| 188 | if (win.id === change[0]) | ||
| 189 | win.layout = change[1]; | ||
| 190 | }); | ||
| 191 | return win; | ||
| 192 | }); | ||
| 193 | } | ||
| 194 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml b/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml new file mode 100644 index 00000000..f02d1695 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml | |||
| @@ -0,0 +1,162 @@ | |||
| 1 | pragma Singleton | ||
| 2 | |||
| 3 | import QtQml | ||
| 4 | import Quickshell | ||
| 5 | import Quickshell.Services.Notifications | ||
| 6 | |||
| 7 | Singleton { | ||
| 8 | id: root | ||
| 9 | |||
| 10 | readonly property bool active: !root.lockscreenActive && !root.displayInhibited | ||
| 11 | property bool lockscreenActive: false | ||
| 12 | property bool displayInhibited: false | ||
| 13 | property alias trackedNotifications: server.trackedNotifications | ||
| 14 | readonly property var groups: { | ||
| 15 | function matchesGroupKey(notif, groupKey) { | ||
| 16 | var matches = true; | ||
| 17 | for (const prop in groupKey.test) { | ||
| 18 | if (notif[prop] !== groupKey.test[prop]) { | ||
| 19 | matches = false; | ||
| 20 | break; | ||
| 21 | } | ||
| 22 | } | ||
| 23 | return matches; | ||
| 24 | } | ||
| 25 | |||
| 26 | var groups = new Map(); | ||
| 27 | var notifs = new Array(); | ||
| 28 | for (const [ix, notif] of server.trackedNotifications.values.entries()) { | ||
| 29 | var didGroup = false; | ||
| 30 | for (const groupKey of root.groupKeys) { | ||
| 31 | if (!matchesGroupKey(notif, groupKey)) | ||
| 32 | continue; | ||
| 33 | |||
| 34 | const key = JSON.stringify({ | ||
| 35 | "key": groupKey, | ||
| 36 | "values": Object.assign({}, ...(Array.from(groupKey["group-by"]).map(prop => { | ||
| 37 | var res = {}; | ||
| 38 | res[prop] = notif[prop]; | ||
| 39 | return res; | ||
| 40 | }))) | ||
| 41 | }); | ||
| 42 | if (!groups.has(key)) | ||
| 43 | groups.set(key, new Array()); | ||
| 44 | groups.get(key).push({ "ix": ix, "notif": notif }); | ||
| 45 | didGroup = true; | ||
| 46 | break; | ||
| 47 | } | ||
| 48 | |||
| 49 | if (!didGroup) | ||
| 50 | notifs.push([{ "ix": ix, "notif": notif }]); | ||
| 51 | } | ||
| 52 | notifs.push(...groups.values()); | ||
| 53 | notifs.sort((as, bs) => Math.min(...(as.map(o => o.ix))) - Math.min(...(bs.map(o => o.ix)))); | ||
| 54 | return notifs.map(ns => ns.map(n => n.notif)); | ||
| 55 | } | ||
| 56 | |||
| 57 | property var groupKeys: [ | ||
| 58 | { "test": { "appName": "Element" }, "group-by": [ "summary" ] } | ||
| 59 | ]; | ||
| 60 | |||
| 61 | property int historyLimit: 100 | ||
| 62 | property var history: [] | ||
| 63 | |||
| 64 | Component { | ||
| 65 | id: expirationTimer | ||
| 66 | |||
| 67 | QtObject { | ||
| 68 | id: timer | ||
| 69 | |||
| 70 | required property QtObject parent | ||
| 71 | required property int expirationTime | ||
| 72 | |||
| 73 | property list<QtObject> data: [ | ||
| 74 | Timer { | ||
| 75 | running: root.active && !timer.expired | ||
| 76 | interval: timer.expirationTime | ||
| 77 | onTriggered: { | ||
| 78 | timer.parent.expirationTimer.destroy(); | ||
| 79 | timer.parent.expirationTimer = null; | ||
| 80 | timer.parent.expire(); | ||
| 81 | } | ||
| 82 | } | ||
| 83 | ] | ||
| 84 | } | ||
| 85 | } | ||
| 86 | |||
| 87 | Component { | ||
| 88 | id: notificationLock | ||
| 89 | |||
| 90 | RetainableLock {} | ||
| 91 | } | ||
| 92 | |||
| 93 | readonly property SystemClock clock: SystemClock { | ||
| 94 | precision: SystemClock.Minutes | ||
| 95 | } | ||
| 96 | |||
| 97 | function formatTime(time) { | ||
| 98 | const now = root.clock.date; | ||
| 99 | const diff = now - time; | ||
| 100 | const minutes = Math.ceil(diff / 60000); | ||
| 101 | const hours = Math.floor(minutes / 60); | ||
| 102 | |||
| 103 | if (hours < 1) { | ||
| 104 | if (minutes < 1) | ||
| 105 | return "now"; | ||
| 106 | if (minutes == 1) | ||
| 107 | return "1 minute"; | ||
| 108 | return `${minutes} minutes`; | ||
| 109 | } | ||
| 110 | |||
| 111 | const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()) | ||
| 112 | const timeDate = new Date(time.getFullYear(), time.getMonth(), time.getDate()) | ||
| 113 | const days = Math.floor((nowDate - timeDate) / (1000 * 86400)) | ||
| 114 | |||
| 115 | const timeStr = time.toLocaleTimeString(Qt.locale(), "HH:mm"); | ||
| 116 | |||
| 117 | if (days === 0) | ||
| 118 | return timeStr; | ||
| 119 | if (days === 1) | ||
| 120 | return `yesterday ${timeStr}`; | ||
| 121 | |||
| 122 | const dateStr = time.toLocaleTimeString(Qt.locale(), "YYYY-MM-DD"); | ||
| 123 | return `${dateStr} ${timeStr}`; | ||
| 124 | } | ||
| 125 | |||
| 126 | NotificationServer { | ||
| 127 | id: server | ||
| 128 | |||
| 129 | bodySupported: true | ||
| 130 | actionsSupported: true | ||
| 131 | actionIconsSupported: true | ||
| 132 | imageSupported: true | ||
| 133 | bodyMarkupSupported: true | ||
| 134 | bodyImagesSupported: true | ||
| 135 | |||
| 136 | onNotification: notification => { | ||
| 137 | var timeout = notification.expireTimeout * 1000; | ||
| 138 | if (notification.appName == "poweralertd") | ||
| 139 | timeout = 2000; | ||
| 140 | if (timeout > 0) { | ||
| 141 | Object.defineProperty(notification, "expirationTimer", { configurable: true, enumerable: true, writable: true }); | ||
| 142 | notification.expirationTimer = expirationTimer.createObject(notification, { parent: notification, expirationTime: timeout }); | ||
| 143 | } | ||
| 144 | Object.defineProperty(notification, "receivedTime", { configurable: true, enumerable: true, writable: true }); | ||
| 145 | notification.receivedTime = root.clock.date; | ||
| 146 | notification.closed.connect((reason) => server.onNotificationClosed(notification, reason)); | ||
| 147 | notification.tracked = true; | ||
| 148 | } | ||
| 149 | |||
| 150 | function onNotificationClosed(notification, reason) { | ||
| 151 | while (root.history.length >= root.historyLimit) { | ||
| 152 | root.history[0].lock.locked = false; | ||
| 153 | root.history.shift(); | ||
| 154 | } | ||
| 155 | |||
| 156 | root.history.push({ | ||
| 157 | lock: notificationLock.createObject(root, { locked: true, object: notification }), | ||
| 158 | notification: notification | ||
| 159 | }); | ||
| 160 | } | ||
| 161 | } | ||
| 162 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/Privacy.qml b/accounts/gkleen@sif/shell/quickshell/Services/Privacy.qml new file mode 100644 index 00000000..9c813e49 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/Services/Privacy.qml | |||
| @@ -0,0 +1,63 @@ | |||
| 1 | pragma Singleton | ||
| 2 | |||
| 3 | import QtQml | ||
| 4 | import Quickshell | ||
| 5 | import Quickshell.Services.Pipewire | ||
| 6 | |||
| 7 | Singleton { | ||
| 8 | id: root | ||
| 9 | |||
| 10 | PwObjectTracker { | ||
| 11 | objects: Pipewire.nodes.values | ||
| 12 | } | ||
| 13 | |||
| 14 | enum Item { | ||
| 15 | Microphone, | ||
| 16 | Screensharing | ||
| 17 | } | ||
| 18 | |||
| 19 | readonly property list<var> activeItems: { | ||
| 20 | var items = []; | ||
| 21 | if (microphoneActive) | ||
| 22 | items.push(Privacy.Item.Microphone); | ||
| 23 | if (screensharingActive) | ||
| 24 | items.push(Privacy.Item.Screensharing); | ||
| 25 | return items; | ||
| 26 | } | ||
| 27 | |||
| 28 | readonly property bool microphoneActive: { | ||
| 29 | if (!Pipewire.ready || !Pipewire.nodes?.values) { | ||
| 30 | return false | ||
| 31 | } | ||
| 32 | |||
| 33 | for (const node of Pipewire.nodes.values) { | ||
| 34 | if (!node || (node.type & PwNodeType.AudioInStream) != PwNodeType.AudioInStream) | ||
| 35 | continue; | ||
| 36 | |||
| 37 | if (node.properties?.["stream.monitor"] === "true") | ||
| 38 | continue; | ||
| 39 | |||
| 40 | if (node.audio?.muted) | ||
| 41 | continue; | ||
| 42 | |||
| 43 | return true; | ||
| 44 | } | ||
| 45 | |||
| 46 | return false; | ||
| 47 | } | ||
| 48 | |||
| 49 | readonly property bool screensharingActive: { | ||
| 50 | if (!Pipewire.ready || !Pipewire.nodes?.values) { | ||
| 51 | return false | ||
| 52 | } | ||
| 53 | |||
| 54 | for (const node of Pipewire.nodes.values) { | ||
| 55 | if (!node || (node.type & PwNodeType.VideoInStream) != PwNodeType.VideoInStream) | ||
| 56 | continue; | ||
| 57 | |||
| 58 | return true; | ||
| 59 | } | ||
| 60 | |||
| 61 | return false; | ||
| 62 | } | ||
| 63 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml b/accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml new file mode 100644 index 00000000..3c524955 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml | |||
| @@ -0,0 +1,8 @@ | |||
| 1 | import Custom as Custom | ||
| 2 | |||
| 3 | Custom.FileSelector { | ||
| 4 | id: root | ||
| 5 | |||
| 6 | directory: @wallpapers@ | ||
| 7 | epoch: 72000000 | ||
| 8 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/SystemTray.qml b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml new file mode 100644 index 00000000..f7b4ed96 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml | |||
| @@ -0,0 +1,201 @@ | |||
| 1 | import QtQuick | ||
| 2 | import QtQuick.Effects | ||
| 3 | import Quickshell | ||
| 4 | import Quickshell.Widgets | ||
| 5 | import Quickshell.Services.SystemTray | ||
| 6 | |||
| 7 | Item { | ||
| 8 | anchors.verticalCenter: parent.verticalCenter | ||
| 9 | width: systemTrayRow.childrenRect.width | ||
| 10 | height: parent.height | ||
| 11 | clip: true | ||
| 12 | |||
| 13 | Row { | ||
| 14 | id: systemTrayRow | ||
| 15 | anchors.centerIn: parent | ||
| 16 | width: childrenRect.width | ||
| 17 | height: parent.height | ||
| 18 | spacing: 0 | ||
| 19 | |||
| 20 | Repeater { | ||
| 21 | model: ScriptModel { | ||
| 22 | values: { | ||
| 23 | var trayItems = Array.from(SystemTray.items.values).filter(item => item.status !== Status.Passive); | ||
| 24 | trayItems.sort((a, b) => a.category !== b.category ? b.category - a.category : a.id.localeCompare(b.id)) | ||
| 25 | return trayItems; | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | delegate: Item { | ||
| 30 | id: trayItemWrapper | ||
| 31 | |||
| 32 | required property var modelData | ||
| 33 | required property int index | ||
| 34 | |||
| 35 | property var trayItem: modelData | ||
| 36 | property string iconSource: { | ||
| 37 | let icon = trayItem && trayItem.icon | ||
| 38 | if (typeof icon === 'string' || icon instanceof String) { | ||
| 39 | if (icon.includes("?path=")) { | ||
| 40 | const split = icon.split("?path=") | ||
| 41 | if (split.length !== 2) | ||
| 42 | return icon | ||
| 43 | const name = split[0] | ||
| 44 | const path = split[1] | ||
| 45 | const fileName = name.substring( | ||
| 46 | name.lastIndexOf("/") + 1) | ||
| 47 | return `file://${path}/${fileName}` | ||
| 48 | } | ||
| 49 | return icon | ||
| 50 | } | ||
| 51 | return "" | ||
| 52 | } | ||
| 53 | |||
| 54 | width: icon.width + 6 | ||
| 55 | height: parent.height | ||
| 56 | anchors.verticalCenter: parent.verticalCenter | ||
| 57 | |||
| 58 | WrapperMouseArea { | ||
| 59 | id: trayItemArea | ||
| 60 | |||
| 61 | anchors.fill: parent | ||
| 62 | acceptedButtons: Qt.LeftButton | Qt.RightButton | ||
| 63 | hoverEnabled: true | ||
| 64 | cursorShape: trayItem.onlyMenu ? Qt.ArrowCursor : Qt.PointingHandCursor | ||
| 65 | onClicked: mouse => { | ||
| 66 | if (!trayItem) | ||
| 67 | return | ||
| 68 | |||
| 69 | if (mouse.button === Qt.LeftButton | ||
| 70 | && !trayItem.onlyMenu) { | ||
| 71 | trayItem.activate() | ||
| 72 | return | ||
| 73 | } | ||
| 74 | |||
| 75 | if (trayItem.hasMenu) { | ||
| 76 | var globalPos = mapToGlobal(0, 0) | ||
| 77 | var currentScreen = screen || Screen | ||
| 78 | var screenX = currentScreen.x || 0 | ||
| 79 | var relativeX = globalPos.x - screenX | ||
| 80 | menuAnchor.menu = trayItem.menu | ||
| 81 | menuAnchor.anchor.window = bar | ||
| 82 | menuAnchor.anchor.rect = Qt.rect( | ||
| 83 | relativeX, | ||
| 84 | 21, | ||
| 85 | parent.width, 1) | ||
| 86 | menuAnchor.open() | ||
| 87 | } | ||
| 88 | } | ||
| 89 | |||
| 90 | Rectangle { | ||
| 91 | anchors.fill: parent | ||
| 92 | color: { | ||
| 93 | if (trayItemArea.containsMouse) | ||
| 94 | return "#33808080"; | ||
| 95 | return "transparent"; | ||
| 96 | } | ||
| 97 | |||
| 98 | Item { | ||
| 99 | anchors.fill: parent | ||
| 100 | |||
| 101 | layer.enabled: true | ||
| 102 | layer.effect: MultiEffect { | ||
| 103 | colorization: 1 | ||
| 104 | colorizationColor: "#555" | ||
| 105 | } | ||
| 106 | |||
| 107 | IconImage { | ||
| 108 | id: icon | ||
| 109 | |||
| 110 | anchors.centerIn: parent | ||
| 111 | implicitSize: 16 | ||
| 112 | source: trayItemWrapper.iconSource | ||
| 113 | asynchronous: true | ||
| 114 | smooth: true | ||
| 115 | mipmap: true | ||
| 116 | |||
| 117 | layer.enabled: true | ||
| 118 | layer.effect: MultiEffect { | ||
| 119 | id: effect | ||
| 120 | |||
| 121 | brightness: 1 | ||
| 122 | } | ||
| 123 | } | ||
| 124 | } | ||
| 125 | } | ||
| 126 | } | ||
| 127 | |||
| 128 | PopupWindow { | ||
| 129 | id: tooltip | ||
| 130 | |||
| 131 | property bool nextVisible: (trayItem.tooltipTitle || trayItem.tooltipDescription) && (trayItemArea.containsMouse || tooltipMouseArea.containsMouse) && !menuAnchor.visible | ||
| 132 | |||
| 133 | anchor { | ||
| 134 | item: trayItemArea | ||
| 135 | edges: Edges.Bottom | ||
| 136 | } | ||
| 137 | |||
| 138 | visible: false | ||
| 139 | onNextVisibleChanged: hangTimer.restart() | ||
| 140 | |||
| 141 | Timer { | ||
| 142 | id: hangTimer | ||
| 143 | interval: 100 | ||
| 144 | onTriggered: tooltip.visible = tooltip.nextVisible | ||
| 145 | } | ||
| 146 | |||
| 147 | color: "black" | ||
| 148 | |||
| 149 | implicitWidth: Math.max(tooltipTitle.contentWidth, tooltipDescription.contentWidth) + 16 | ||
| 150 | implicitHeight: (trayItem.tooltipTitle ? tooltipTitle.contentHeight : 0) + (trayItem.tooltipDescription ? tooltipDescription.contentHeight : 0) + 16 | ||
| 151 | |||
| 152 | WrapperMouseArea { | ||
| 153 | id: tooltipMouseArea | ||
| 154 | |||
| 155 | hoverEnabled: true | ||
| 156 | enabled: true | ||
| 157 | |||
| 158 | margin: 4 | ||
| 159 | |||
| 160 | anchors.fill: parent | ||
| 161 | |||
| 162 | Item { | ||
| 163 | anchors.fill: parent | ||
| 164 | |||
| 165 | Column { | ||
| 166 | anchors.centerIn: parent | ||
| 167 | Text { | ||
| 168 | id: tooltipTitle | ||
| 169 | |||
| 170 | enabled: trayItem.tooltipTitle | ||
| 171 | |||
| 172 | font.pointSize: 10 | ||
| 173 | font.family: "Fira Sans" | ||
| 174 | font.bold: true | ||
| 175 | color: "white" | ||
| 176 | |||
| 177 | text: trayItem.tooltipTitle | ||
| 178 | } | ||
| 179 | |||
| 180 | Text { | ||
| 181 | id: tooltipDescription | ||
| 182 | |||
| 183 | enabled: trayItem.tooltipDescription | ||
| 184 | |||
| 185 | font.pointSize: 10 | ||
| 186 | font.family: "Fira Sans" | ||
| 187 | color: "white" | ||
| 188 | |||
| 189 | text: trayItem.tooltipDescription | ||
| 190 | } | ||
| 191 | } | ||
| 192 | } | ||
| 193 | } | ||
| 194 | } | ||
| 195 | } | ||
| 196 | } | ||
| 197 | } | ||
| 198 | QsMenuAnchor { | ||
| 199 | id: menuAnchor | ||
| 200 | } | ||
| 201 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml b/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml new file mode 100644 index 00000000..05a40dbc --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml | |||
| @@ -0,0 +1,97 @@ | |||
| 1 | import Quickshell | ||
| 2 | import Quickshell.Io | ||
| 3 | import Quickshell.Services.Pipewire | ||
| 4 | import Quickshell.Services.Mpris | ||
| 5 | import qs.Services | ||
| 6 | import Custom as Custom | ||
| 7 | import QtQml | ||
| 8 | |||
| 9 | Scope { | ||
| 10 | id: root | ||
| 11 | |||
| 12 | SocketServer { | ||
| 13 | active: true | ||
| 14 | path: `${Quickshell.env("XDG_RUNTIME_DIR")}/shell.sock` | ||
| 15 | handler: Socket { | ||
| 16 | parser: SplitParser { | ||
| 17 | onRead: line => { | ||
| 18 | const command = (() => { | ||
| 19 | try { | ||
| 20 | return JSON.parse(line); | ||
| 21 | } catch (e) { | ||
| 22 | console.warn("UnixIPC: Failed to parse command:", line, e); | ||
| 23 | } | ||
| 24 | })(); | ||
| 25 | if (!command) | ||
| 26 | return; | ||
| 27 | |||
| 28 | if (command.Volume) | ||
| 29 | root.onCommandVolume(command.Volume); | ||
| 30 | else if (command.Brightness) | ||
| 31 | root.onCommandBrightness(command.Brightness); | ||
| 32 | else if (command.LockSession) | ||
| 33 | Custom.Systemd.lockSession(); | ||
| 34 | else if (command.Suspend) | ||
| 35 | Custom.Systemd.suspend(); | ||
| 36 | else if (command.Hibernate) | ||
| 37 | Custom.Systemd.hibernate(); | ||
| 38 | else if (command.Mpris) | ||
| 39 | root.onCommandMpris(command.Mpris); | ||
| 40 | else if (command.Notifications) | ||
| 41 | root.onCommandNotifications(command.Notifications); | ||
| 42 | else | ||
| 43 | console.warn("UnixIPC: Command not handled:", JSON.stringify(command)); | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | onError: e => { | ||
| 48 | if (e == 1) | ||
| 49 | return; | ||
| 50 | console.warn("QLocalSocket::LocalSocketError", e); | ||
| 51 | } | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | PwObjectTracker { | ||
| 56 | objects: [ Pipewire.defaultAudioSink, Pipewire.defaultAudioSource ] | ||
| 57 | } | ||
| 58 | function onCommandVolume(command) { | ||
| 59 | if (command.muted === "toggle") | ||
| 60 | Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted; | ||
| 61 | if (command.volume === "up") | ||
| 62 | Pipewire.defaultAudioSink.audio.volume += 0.02; | ||
| 63 | if (command.volume === "down") | ||
| 64 | Pipewire.defaultAudioSink.audio.volume -= 0.02; | ||
| 65 | |||
| 66 | if (command["mic-muted"] === "toggle") | ||
| 67 | Pipewire.defaultAudioSource.audio.muted = !Pipewire.defaultAudioSource.audio.muted; | ||
| 68 | } | ||
| 69 | |||
| 70 | function onCommandBrightness(command) { | ||
| 71 | if (command === "up") | ||
| 72 | Brightness.currBrightness += 0.02 | ||
| 73 | if (command === "down") | ||
| 74 | Brightness.currBrightness -= 0.02 | ||
| 75 | } | ||
| 76 | |||
| 77 | function onCommandMpris(command) { | ||
| 78 | if (command.PauseAll) | ||
| 79 | Array.from(MprisProxy.players).forEach(player => { | ||
| 80 | if (player.canPause && player.isPlaying) | ||
| 81 | player.pause(); | ||
| 82 | }); | ||
| 83 | } | ||
| 84 | Component.onCompleted: { (_ => {})(MprisProxy.players); } | ||
| 85 | |||
| 86 | function onCommandNotifications(command) { | ||
| 87 | if (command.DismissGroup && NotificationManager.active) { | ||
| 88 | if (NotificationManager.groups.length > 0) | ||
| 89 | for (const notif of [...NotificationManager.groups[0]]) | ||
| 90 | notif.dismiss(); | ||
| 91 | } | ||
| 92 | if (command.DismissAll && NotificationManager.active) { | ||
| 93 | for (const notif of [...NotificationManager.trackedNotifications.values]) | ||
| 94 | notif.dismiss(); | ||
| 95 | } | ||
| 96 | } | ||
| 97 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml b/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml new file mode 100644 index 00000000..653f4763 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml | |||
| @@ -0,0 +1,163 @@ | |||
| 1 | import QtQuick | ||
| 2 | import QtQuick.Layouts | ||
| 3 | import Quickshell | ||
| 4 | import Quickshell.Services.Pipewire | ||
| 5 | import Quickshell.Widgets | ||
| 6 | |||
| 7 | Scope { | ||
| 8 | id: root | ||
| 9 | |||
| 10 | property string show: "" | ||
| 11 | property bool inhibited: true | ||
| 12 | |||
| 13 | PwObjectTracker { | ||
| 14 | objects: [ Pipewire.defaultAudioSink, Pipewire.defaultAudioSource ] | ||
| 15 | } | ||
| 16 | |||
| 17 | Connections { | ||
| 18 | enabled: Pipewire.defaultAudioSink | ||
| 19 | target: Pipewire.defaultAudioSink?.audio | ||
| 20 | |||
| 21 | function onVolumeChanged() { | ||
| 22 | root.show = "sink"; | ||
| 23 | hideTimer.restart(); | ||
| 24 | } | ||
| 25 | function onMutedChanged() { | ||
| 26 | root.show = "sink"; | ||
| 27 | hideTimer.restart(); | ||
| 28 | } | ||
| 29 | } | ||
| 30 | |||
| 31 | Connections { | ||
| 32 | enabled: Pipewire.defaultAudioSource | ||
| 33 | target: Pipewire.defaultAudioSource?.audio | ||
| 34 | |||
| 35 | function onVolumeChanged() { | ||
| 36 | root.show = "source"; | ||
| 37 | hideTimer.restart(); | ||
| 38 | } | ||
| 39 | function onMutedChanged() { | ||
| 40 | root.show = "source"; | ||
| 41 | hideTimer.restart(); | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | onShowChanged: { | ||
| 46 | if (show) | ||
| 47 | hideTimer.restart(); | ||
| 48 | } | ||
| 49 | |||
| 50 | Timer { | ||
| 51 | id: hideTimer | ||
| 52 | interval: 1000 | ||
| 53 | onTriggered: root.show = "" | ||
| 54 | } | ||
| 55 | |||
| 56 | Timer { | ||
| 57 | id: startInhibit | ||
| 58 | interval: 100 | ||
| 59 | running: true | ||
| 60 | onTriggered: { | ||
| 61 | root.show = ""; | ||
| 62 | root.inhibited = false; | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | LazyLoader { | ||
| 67 | active: root.show && !root.inhibited | ||
| 68 | |||
| 69 | Variants { | ||
| 70 | model: Quickshell.screens | ||
| 71 | |||
| 72 | delegate: Scope { | ||
| 73 | id: screenScope | ||
| 74 | |||
| 75 | required property var modelData | ||
| 76 | |||
| 77 | PanelWindow { | ||
| 78 | id: window | ||
| 79 | |||
| 80 | screen: screenScope.modelData | ||
| 81 | |||
| 82 | anchors.top: true | ||
| 83 | margins.top: screen.height / 2 - 50 + 3.5 | ||
| 84 | exclusiveZone: 0 | ||
| 85 | exclusionMode: ExclusionMode.Ignore | ||
| 86 | |||
| 87 | implicitWidth: 400 | ||
| 88 | implicitHeight: 50 | ||
| 89 | |||
| 90 | mask: Region {} | ||
| 91 | |||
| 92 | color: "transparent" | ||
| 93 | |||
| 94 | Rectangle { | ||
| 95 | anchors.fill: parent | ||
| 96 | color: Qt.rgba(0, 0, 0, 0.75) | ||
| 97 | } | ||
| 98 | |||
| 99 | RowLayout { | ||
| 100 | id: layout | ||
| 101 | |||
| 102 | anchors.centerIn: parent | ||
| 103 | |||
| 104 | height: 50 - 8*2 | ||
| 105 | width: 400 - 8*2 | ||
| 106 | |||
| 107 | MaterialDesignIcon { | ||
| 108 | id: volumeIcon | ||
| 109 | |||
| 110 | implicitWidth: parent.height | ||
| 111 | implicitHeight: parent.height | ||
| 112 | |||
| 113 | icon: { | ||
| 114 | if (root.show == "sink") { | ||
| 115 | if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted) | ||
| 116 | return "volume-off"; | ||
| 117 | if (Pipewire.defaultAudioSink.audio.volume <= 0.33) | ||
| 118 | return "volume-low"; | ||
| 119 | if (Pipewire.defaultAudioSink.audio.volume <= 0.67) | ||
| 120 | return "volume-medium"; | ||
| 121 | return "volume-high"; | ||
| 122 | } else if (root.show == "source") { | ||
| 123 | if (!Pipewire.defaultAudioSource || Pipewire.defaultAudioSource.audio.muted) | ||
| 124 | return "microphone-off"; | ||
| 125 | if (Pipewire.defaultAudioSource.audio.volume > 1) | ||
| 126 | return "microphone-plus"; | ||
| 127 | return "microphone"; | ||
| 128 | } | ||
| 129 | return "volume-high"; | ||
| 130 | } | ||
| 131 | } | ||
| 132 | |||
| 133 | Rectangle { | ||
| 134 | Layout.fillWidth: true | ||
| 135 | |||
| 136 | implicitHeight: 10 | ||
| 137 | |||
| 138 | color: "#50ffffff" | ||
| 139 | |||
| 140 | Rectangle { | ||
| 141 | anchors { | ||
| 142 | left: parent.left | ||
| 143 | top: parent.top | ||
| 144 | bottom: parent.bottom | ||
| 145 | } | ||
| 146 | |||
| 147 | color: Pipewire.defaultAudioSink?.audio.muted ? "#70ffffff" : "white" | ||
| 148 | |||
| 149 | implicitWidth: { | ||
| 150 | if (root.show == "sink") | ||
| 151 | return parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0); | ||
| 152 | else if (root.show == "source") | ||
| 153 | return parent.width * Math.min(1, (Pipewire.defaultAudioSource?.audio.volume ?? 0)); | ||
| 154 | return 0; | ||
| 155 | } | ||
| 156 | } | ||
| 157 | } | ||
| 158 | } | ||
| 159 | } | ||
| 160 | } | ||
| 161 | } | ||
| 162 | } | ||
| 163 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml b/accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml new file mode 100644 index 00000000..4f85a900 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml | |||
| @@ -0,0 +1,89 @@ | |||
| 1 | import QtQuick | ||
| 2 | import Quickshell | ||
| 3 | import qs.Services | ||
| 4 | |||
| 5 | Item { | ||
| 6 | id: root | ||
| 7 | |||
| 8 | anchors.fill: parent | ||
| 9 | |||
| 10 | required property string screen | ||
| 11 | |||
| 12 | property Img current: one | ||
| 13 | property string source: selector.selected | ||
| 14 | |||
| 15 | WallpaperSelector { | ||
| 16 | id: selector | ||
| 17 | seed: screen | ||
| 18 | } | ||
| 19 | |||
| 20 | onSourceChanged: { | ||
| 21 | if (!source) | ||
| 22 | current = null; | ||
| 23 | else if (current === one) | ||
| 24 | two.update() | ||
| 25 | else | ||
| 26 | one.update() | ||
| 27 | } | ||
| 28 | |||
| 29 | Img { id: one } | ||
| 30 | Img { id: two } | ||
| 31 | |||
| 32 | component Img: Image { | ||
| 33 | id: img | ||
| 34 | |||
| 35 | function update() { | ||
| 36 | source = root.source || "" | ||
| 37 | } | ||
| 38 | |||
| 39 | anchors.fill: parent | ||
| 40 | fillMode: Image.PreserveAspectCrop | ||
| 41 | smooth: true | ||
| 42 | asynchronous: true | ||
| 43 | cache: false | ||
| 44 | |||
| 45 | opacity: 0 | ||
| 46 | |||
| 47 | onStatusChanged: { | ||
| 48 | if (status === Image.Ready) { | ||
| 49 | root.current = this | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | states: State { | ||
| 54 | name: "visible" | ||
| 55 | when: root.current === img | ||
| 56 | |||
| 57 | PropertyChanges { | ||
| 58 | img.opacity: 1 | ||
| 59 | } | ||
| 60 | StateChangeScript { | ||
| 61 | name: "unloadOther" | ||
| 62 | script: { | ||
| 63 | if (img === one) | ||
| 64 | two.source = "" | ||
| 65 | if (img === two) | ||
| 66 | one.source = "" | ||
| 67 | } | ||
| 68 | } | ||
| 69 | } | ||
| 70 | |||
| 71 | transitions: Transition { | ||
| 72 | SequentialAnimation { | ||
| 73 | NumberAnimation { | ||
| 74 | target: img | ||
| 75 | properties: "opacity" | ||
| 76 | duration: { | ||
| 77 | if (img === one && two.source == "" || img === two && one.source == "") | ||
| 78 | return 0; | ||
| 79 | return 5000; | ||
| 80 | } | ||
| 81 | easing.type: Easing.OutCubic | ||
| 82 | } | ||
| 83 | ScriptAction { | ||
| 84 | scriptName: "unloadOther" | ||
| 85 | } | ||
| 86 | } | ||
| 87 | } | ||
| 88 | } | ||
| 89 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/WaylandInhibitorWidget.qml b/accounts/gkleen@sif/shell/quickshell/WaylandInhibitorWidget.qml new file mode 100644 index 00000000..0512ff51 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/WaylandInhibitorWidget.qml | |||
| @@ -0,0 +1,56 @@ | |||
| 1 | import Quickshell | ||
| 2 | import QtQuick | ||
| 3 | import Quickshell.Widgets | ||
| 4 | import Quickshell.Wayland | ||
| 5 | import qs.Services | ||
| 6 | |||
| 7 | Item { | ||
| 8 | id: root | ||
| 9 | |||
| 10 | required property var window | ||
| 11 | |||
| 12 | width: icon.width + 8 | ||
| 13 | height: parent.height | ||
| 14 | anchors.verticalCenter: parent.verticalCenter | ||
| 15 | |||
| 16 | IdleInhibitor { | ||
| 17 | id: inhibitor | ||
| 18 | enabled: InhibitorState.waylandIdleInhibited | ||
| 19 | window: root.window | ||
| 20 | } | ||
| 21 | |||
| 22 | WrapperMouseArea { | ||
| 23 | id: widgetMouseArea | ||
| 24 | |||
| 25 | anchors.fill: parent | ||
| 26 | |||
| 27 | hoverEnabled: true | ||
| 28 | cursorShape: Qt.PointingHandCursor | ||
| 29 | |||
| 30 | onClicked: InhibitorState.waylandIdleInhibited = !InhibitorState.waylandIdleInhibited | ||
| 31 | |||
| 32 | Rectangle { | ||
| 33 | anchors.fill: parent | ||
| 34 | color: { | ||
| 35 | if (widgetMouseArea.containsMouse) { | ||
| 36 | return "#33808080"; | ||
| 37 | } | ||
| 38 | return "transparent"; | ||
| 39 | } | ||
| 40 | |||
| 41 | Item { | ||
| 42 | anchors.fill: parent | ||
| 43 | |||
| 44 | MaterialDesignIcon { | ||
| 45 | id: icon | ||
| 46 | |||
| 47 | implicitSize: 14 | ||
| 48 | anchors.centerIn: parent | ||
| 49 | |||
| 50 | icon: inhibitor.enabled ? "eye" : "eye-off" | ||
| 51 | color: inhibitor.enabled ? "white" : "#555" | ||
| 52 | } | ||
| 53 | } | ||
| 54 | } | ||
| 55 | } | ||
| 56 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml b/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml new file mode 100644 index 00000000..3ae94346 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml | |||
| @@ -0,0 +1,204 @@ | |||
| 1 | import Quickshell | ||
| 2 | import QtQuick | ||
| 3 | import qs.Services | ||
| 4 | import Quickshell.Widgets | ||
| 5 | import QtQuick.Layouts | ||
| 6 | |||
| 7 | Row { | ||
| 8 | id: workspaces | ||
| 9 | |||
| 10 | required property var screen | ||
| 11 | |||
| 12 | property var ignoreWorkspaces: @ignore_workspaces@ | ||
| 13 | |||
| 14 | height: parent.height | ||
| 15 | anchors.verticalCenter: parent.verticalCenter | ||
| 16 | spacing: 0 | ||
| 17 | |||
| 18 | Repeater { | ||
| 19 | model: ScriptModel { | ||
| 20 | values: { | ||
| 21 | let currWorkspaces = NiriService.workspaces; | ||
| 22 | const ignoreWorkspaces = Array.from(workspaces.ignoreWorkspaces); | ||
| 23 | currWorkspaces = currWorkspaces.filter(ws => ws.output == workspaces.screen.name).filter(ws => ws.is_active || ignoreWorkspaces.every(iws => iws !== ws.name)); | ||
| 24 | currWorkspaces.sort((a, b) => { | ||
| 25 | if (NiriService.outputs?.[a.output]?.logical?.x !== NiriService.outputs?.[b.output]?.logical?.x) | ||
| 26 | return NiriService.outputs?.[a.output]?.logical?.x - NiriService.outputs?.[b.output]?.logical?.x | ||
| 27 | if (NiriService.outputs?.[a.output]?.logical?.y !== NiriService.outputs?.[b.output]?.logical?.y) | ||
| 28 | return NiriService.outputs?.[a.output]?.logical?.y - NiriService.outputs?.[b.output]?.logical?.y | ||
| 29 | return a.idx - b.idx; | ||
| 30 | }); | ||
| 31 | return currWorkspaces; | ||
| 32 | } | ||
| 33 | } | ||
| 34 | |||
| 35 | Item { | ||
| 36 | id: wsItem | ||
| 37 | |||
| 38 | property var workspaceData: modelData | ||
| 39 | |||
| 40 | width: wsLabel.contentWidth + 8 | ||
| 41 | height: parent.height | ||
| 42 | anchors.verticalCenter: parent.verticalCenter | ||
| 43 | |||
| 44 | WrapperMouseArea { | ||
| 45 | id: mouseArea | ||
| 46 | |||
| 47 | anchors.fill: parent | ||
| 48 | |||
| 49 | hoverEnabled: true | ||
| 50 | cursorShape: Qt.PointingHandCursor | ||
| 51 | enabled: true | ||
| 52 | onClicked: { | ||
| 53 | NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {}); | ||
| 54 | } | ||
| 55 | |||
| 56 | Rectangle { | ||
| 57 | anchors.fill: parent | ||
| 58 | |||
| 59 | color: { | ||
| 60 | if (mouseArea.containsMouse) { | ||
| 61 | return "#33808080"; | ||
| 62 | } | ||
| 63 | return "transparent"; | ||
| 64 | } | ||
| 65 | |||
| 66 | Text { | ||
| 67 | id: wsLabel | ||
| 68 | |||
| 69 | anchors.centerIn: parent | ||
| 70 | |||
| 71 | font.pointSize: 10 | ||
| 72 | font.family: "Fira Sans" | ||
| 73 | color: { | ||
| 74 | if (workspaceData.is_active) | ||
| 75 | return "#23fd00"; | ||
| 76 | if (workspaceData.active_window_id === null) | ||
| 77 | return "#555"; | ||
| 78 | return "white"; | ||
| 79 | } | ||
| 80 | |||
| 81 | text: workspaceData.name ? workspaceData.name : workspaceData.idx | ||
| 82 | } | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | PopupWindow { | ||
| 87 | id: tooltip | ||
| 88 | |||
| 89 | property bool nextVisible: (mouseArea.containsMouse || tooltipMouseArea.containsMouse) && [...windowsModel.values].length > 0 | ||
| 90 | |||
| 91 | anchor { | ||
| 92 | item: mouseArea | ||
| 93 | edges: Edges.Bottom | Edges.Left | ||
| 94 | } | ||
| 95 | visible: false | ||
| 96 | |||
| 97 | onNextVisibleChanged: hangTimer.restart() | ||
| 98 | |||
| 99 | Timer { | ||
| 100 | id: hangTimer | ||
| 101 | interval: 100 | ||
| 102 | onTriggered: tooltip.visible = tooltip.nextVisible | ||
| 103 | } | ||
| 104 | |||
| 105 | implicitWidth: tooltipContent.implicitWidth | ||
| 106 | implicitHeight: tooltipContent.implicitHeight | ||
| 107 | color: "black" | ||
| 108 | |||
| 109 | WrapperMouseArea { | ||
| 110 | id: tooltipMouseArea | ||
| 111 | |||
| 112 | hoverEnabled: true | ||
| 113 | enabled: true | ||
| 114 | |||
| 115 | anchors.fill: parent | ||
| 116 | |||
| 117 | WrapperItem { | ||
| 118 | id: tooltipContent | ||
| 119 | |||
| 120 | margin: 0 | ||
| 121 | |||
| 122 | ColumnLayout { | ||
| 123 | spacing: 0 | ||
| 124 | |||
| 125 | Repeater { | ||
| 126 | model: ScriptModel { | ||
| 127 | id: windowsModel | ||
| 128 | |||
| 129 | values: { | ||
| 130 | let currWindows = Array.from(NiriService.windows).filter(win => win.workspace_id == wsItem.workspaceData.id); | ||
| 131 | currWindows.sort((a, b) => { | ||
| 132 | if (a.is_floating !== b.is_floating) | ||
| 133 | return b.is_floating - a.is_floating; | ||
| 134 | if (a.layout.tile_pos_in_workspace_view?.[0] !== b.layout.tile_pos_in_workspace_view?.[0]) | ||
| 135 | return a.layout.tile_pos_in_workspace_view?.[0] - b.layout.tile_pos_in_workspace_view?.[0] | ||
| 136 | if (a.layout.tile_pos_in_workspace_view?.[1] !== b.layout.tile_pos_in_workspace_view?.[1]) | ||
| 137 | return a.layout.tile_pos_in_workspace_view?.[1] - b.layout.tile_pos_in_workspace_view?.[1] | ||
| 138 | if (a.layout.pos_in_scrolling_layout?.[0] !== b.layout.pos_in_scrolling_layout?.[0]) | ||
| 139 | return a.layout.pos_in_scrolling_layout?.[0] - b.layout.pos_in_scrolling_layout?.[0] | ||
| 140 | if (a.layout.pos_in_scrolling_layout?.[1] !== b.layout.pos_in_scrolling_layout?.[1]) | ||
| 141 | return a.layout.pos_in_scrolling_layout?.[1] - b.layout.pos_in_scrolling_layout?.[1] | ||
| 142 | if (a.app_id !== b.app_id) | ||
| 143 | return a.app_id.localeCompare(b.app_id); | ||
| 144 | |||
| 145 | return a.title.localeCompare(b.title); | ||
| 146 | }); | ||
| 147 | return currWindows; | ||
| 148 | } | ||
| 149 | } | ||
| 150 | |||
| 151 | WrapperMouseArea { | ||
| 152 | id: windowMouseArea | ||
| 153 | |||
| 154 | required property int index | ||
| 155 | required property var modelData | ||
| 156 | property var windowData: modelData | ||
| 157 | |||
| 158 | hoverEnabled: true | ||
| 159 | cursorShape: Qt.PointingHandCursor | ||
| 160 | enabled: true | ||
| 161 | |||
| 162 | Layout.fillWidth: true | ||
| 163 | |||
| 164 | onClicked: { | ||
| 165 | NiriService.sendCommand({ "Action": { "FocusWindow": { "id": windowData.id } } }, _ => {}) | ||
| 166 | } | ||
| 167 | |||
| 168 | WrapperRectangle { | ||
| 169 | color: windowMouseArea.containsMouse ? "#33808080" : "transparent"; | ||
| 170 | |||
| 171 | WrapperItem { | ||
| 172 | rightMargin: 8 | ||
| 173 | leftMargin: 8 | ||
| 174 | topMargin: windowMouseArea.index == 0 ? 8 : 4 | ||
| 175 | bottomMargin: windowMouseArea.index == windowsModel.values.length - 1 ? 8 : 4 | ||
| 176 | |||
| 177 | Text { | ||
| 178 | id: windowLabel | ||
| 179 | |||
| 180 | font.pointSize: 10 | ||
| 181 | font.family: "Fira Sans" | ||
| 182 | color: { | ||
| 183 | if (windowData.is_focused) | ||
| 184 | return "#23fd00"; | ||
| 185 | if (NiriService.workspaces.find(ws => ws.id == windowData.workspace_id)?.active_window_id == windowData.id) | ||
| 186 | return "white"; | ||
| 187 | return "#555"; | ||
| 188 | } | ||
| 189 | |||
| 190 | text: windowData.title | ||
| 191 | |||
| 192 | horizontalAlignment: Text.AlignLeft | ||
| 193 | } | ||
| 194 | } | ||
| 195 | } | ||
| 196 | } | ||
| 197 | } | ||
| 198 | } | ||
| 199 | } | ||
| 200 | } | ||
| 201 | } | ||
| 202 | } | ||
| 203 | } | ||
| 204 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/WorktimeWidget.qml b/accounts/gkleen@sif/shell/quickshell/WorktimeWidget.qml new file mode 100644 index 00000000..04bcc581 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/WorktimeWidget.qml | |||
| @@ -0,0 +1,120 @@ | |||
| 1 | import QtQml | ||
| 2 | import Quickshell | ||
| 3 | import Quickshell.Io | ||
| 4 | import QtQuick | ||
| 5 | import Quickshell.Widgets | ||
| 6 | |||
| 7 | Item { | ||
| 8 | id: root | ||
| 9 | |||
| 10 | required property string command | ||
| 11 | property var state: null | ||
| 12 | |||
| 13 | height: parent.height | ||
| 14 | width: label.contentWidth + 8 | ||
| 15 | anchors.verticalCenter: parent.verticalCenter | ||
| 16 | |||
| 17 | Process { | ||
| 18 | id: process | ||
| 19 | running: true | ||
| 20 | command: [ @worktime@, root.command, "--waybar" ] | ||
| 21 | stdout: StdioCollector { | ||
| 22 | id: processCollector | ||
| 23 | onStreamFinished: { | ||
| 24 | try { | ||
| 25 | root.state = JSON.parse(processCollector.text); | ||
| 26 | } catch (e) { | ||
| 27 | console.warn("Worktime: Failed to parse output:", processCollector.text, e); | ||
| 28 | } | ||
| 29 | } | ||
| 30 | } | ||
| 31 | } | ||
| 32 | |||
| 33 | Timer { | ||
| 34 | running: true | ||
| 35 | interval: 60 | ||
| 36 | repeat: true | ||
| 37 | onTriggered: process.running = true | ||
| 38 | } | ||
| 39 | |||
| 40 | WrapperMouseArea { | ||
| 41 | id: mouseArea | ||
| 42 | |||
| 43 | anchors.fill: parent | ||
| 44 | |||
| 45 | enabled: true | ||
| 46 | hoverEnabled: true | ||
| 47 | |||
| 48 | Item { | ||
| 49 | anchors.fill: parent | ||
| 50 | |||
| 51 | Text { | ||
| 52 | id: label | ||
| 53 | |||
| 54 | anchors.centerIn: parent | ||
| 55 | |||
| 56 | visible: root.state?.text ?? false | ||
| 57 | text: root.state?.text ?? "" | ||
| 58 | |||
| 59 | font.pointSize: 10 | ||
| 60 | font.family: "Fira Sans" | ||
| 61 | color: { | ||
| 62 | if (root.state?.class == "running") | ||
| 63 | return "white"; | ||
| 64 | if (root.state?.class == "over") | ||
| 65 | return "#f28a21"; | ||
| 66 | return "#555"; | ||
| 67 | } | ||
| 68 | } | ||
| 69 | } | ||
| 70 | } | ||
| 71 | |||
| 72 | PopupWindow { | ||
| 73 | id: tooltip | ||
| 74 | |||
| 75 | property bool nextVisible: Boolean(root.state?.tooltip ?? false) && (mouseArea.containsMouse || tooltipMouseArea.containsMouse) | ||
| 76 | |||
| 77 | anchor { | ||
| 78 | item: mouseArea | ||
| 79 | edges: Edges.Bottom | Edges.Left | ||
| 80 | } | ||
| 81 | visible: false | ||
| 82 | |||
| 83 | onNextVisibleChanged: hangTimer.restart() | ||
| 84 | |||
| 85 | Timer { | ||
| 86 | id: hangTimer | ||
| 87 | interval: 100 | ||
| 88 | onTriggered: tooltip.visible = tooltip.nextVisible | ||
| 89 | } | ||
| 90 | |||
| 91 | implicitWidth: tooltipText.contentWidth + 16 | ||
| 92 | implicitHeight: tooltipText.contentHeight + 16 | ||
| 93 | color: "black" | ||
| 94 | |||
| 95 | WrapperMouseArea { | ||
| 96 | id: tooltipMouseArea | ||
| 97 | |||
| 98 | enabled: true | ||
| 99 | hoverEnabled: true | ||
| 100 | |||
| 101 | anchors.fill: parent | ||
| 102 | |||
| 103 | Item { | ||
| 104 | anchors.fill: parent | ||
| 105 | |||
| 106 | Text { | ||
| 107 | id: tooltipText | ||
| 108 | |||
| 109 | anchors.centerIn: parent | ||
| 110 | |||
| 111 | font.pointSize: 10 | ||
| 112 | font.family: "Fira Sans" | ||
| 113 | color: "white" | ||
| 114 | |||
| 115 | text: root.state?.tooltip ?? "" | ||
| 116 | } | ||
| 117 | } | ||
| 118 | } | ||
| 119 | } | ||
| 120 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/displaymanager.qml b/accounts/gkleen@sif/shell/quickshell/displaymanager.qml new file mode 100644 index 00000000..b452c03d --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/displaymanager.qml | |||
| @@ -0,0 +1,115 @@ | |||
| 1 | //@ pragma UseQApplication | ||
| 2 | |||
| 3 | import Quickshell | ||
| 4 | import Quickshell.Wayland | ||
| 5 | import Quickshell.Io | ||
| 6 | import Quickshell.Services.Greetd | ||
| 7 | import QtQml | ||
| 8 | |||
| 9 | |||
| 10 | ShellRoot { | ||
| 11 | id: displaymanager | ||
| 12 | |||
| 13 | settings.watchFiles: false | ||
| 14 | |||
| 15 | property string currentText: "" | ||
| 16 | property string username: @username@ | ||
| 17 | property list<string> command: @niri_session@ | ||
| 18 | property list<var> messages: [] | ||
| 19 | property bool responseRequired: false | ||
| 20 | property bool responseVisible: false | ||
| 21 | |||
| 22 | signal startAuth() | ||
| 23 | |||
| 24 | onStartAuth: { | ||
| 25 | if (Greetd.state !== GreetdState.Inactive) | ||
| 26 | Greetd.cancelSession(); | ||
| 27 | displaymanager.messages = []; | ||
| 28 | Greetd.createSession(displaymanager.username); | ||
| 29 | } | ||
| 30 | |||
| 31 | Connections { | ||
| 32 | target: Greetd | ||
| 33 | function onStateChanged() { | ||
| 34 | console.log("greetd state: ", GreetdState.toString(Greetd.state)); | ||
| 35 | if (Greetd.state === GreetdState.ReadyToLaunch) | ||
| 36 | Greetd.launch(displaymanager.command); | ||
| 37 | } | ||
| 38 | function onAuthMessage(message: string, error: bool, responseRequired: bool, echoResponse: bool) { | ||
| 39 | displaymanager.responseVisible = echoResponse; | ||
| 40 | displaymanager.responseRequired = responseRequired; | ||
| 41 | displaymanager.messages = Array.from(displaymanager.messages).concat([{ "text": message, "error": error }]); | ||
| 42 | } | ||
| 43 | function onAuthFailure(message: string) { | ||
| 44 | displaymanager.responseRequired = false; | ||
| 45 | displaymanager.messages = Array.from(displaymanager.messages).concat([{ "text": message, "error": true }]); | ||
| 46 | } | ||
| 47 | } | ||
| 48 | |||
| 49 | Component.onCompleted: { | ||
| 50 | if (Greetd.state !== GreetdState.Inactive) | ||
| 51 | Greetd.cancelSession(); | ||
| 52 | } | ||
| 53 | |||
| 54 | Variants { | ||
| 55 | model: Quickshell.screens | ||
| 56 | |||
| 57 | delegate: Scope { | ||
| 58 | id: screenScope | ||
| 59 | |||
| 60 | required property var modelData | ||
| 61 | |||
| 62 | PanelWindow { | ||
| 63 | color: "black" | ||
| 64 | |||
| 65 | screen: screenScope.modelData | ||
| 66 | |||
| 67 | WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive | ||
| 68 | |||
| 69 | anchors.top: true | ||
| 70 | anchors.bottom: true | ||
| 71 | anchors.left: true | ||
| 72 | anchors.right: true | ||
| 73 | |||
| 74 | LockSurface { | ||
| 75 | id: surfaceContent | ||
| 76 | |||
| 77 | screen: screenScope.modelData | ||
| 78 | |||
| 79 | onCurrentTextChanged: displaymanager.currentText = currentText | ||
| 80 | Connections { | ||
| 81 | target: displaymanager | ||
| 82 | function onCurrentTextChanged() { surfaceContent.currentText = displaymanager.currentText; } | ||
| 83 | function onMessagesChanged() { surfaceContent.messages = Array.from(displaymanager.messages); } | ||
| 84 | function onResponseRequiredChanged() { surfaceContent.responseRequired = displaymanager.responseRequired; } | ||
| 85 | function onResponseVisibleChanged() { surfaceContent.responseVisible = displaymanager.responseVisible; } | ||
| 86 | } | ||
| 87 | |||
| 88 | onResponse: responseText => Greetd.respond(responseText); | ||
| 89 | Connections { | ||
| 90 | target: Greetd | ||
| 91 | function onStateChanged() { | ||
| 92 | if (Greetd.state === GreetdState.Authenticating) { | ||
| 93 | surfaceContent.authRunning = true; | ||
| 94 | } else { | ||
| 95 | surfaceContent.authRunning = false; | ||
| 96 | } | ||
| 97 | } | ||
| 98 | } | ||
| 99 | |||
| 100 | onAuthRunningChanged: { | ||
| 101 | if (surfaceContent.authRunning && Greetd.state !== GreetdState.Authenticating) | ||
| 102 | displaymanager.startAuth(); | ||
| 103 | } | ||
| 104 | Component.onCompleted: { | ||
| 105 | surfaceContent.authRunning = Greetd.state === GreetdState.Authenticating | ||
| 106 | surfaceContent.messages = Array.from(displaymanager.messages); | ||
| 107 | surfaceContent.responseVisible = displaymanager.responseVisible; | ||
| 108 | surfaceContent.responseRequired = displaymanager.responseRequired; | ||
| 109 | surfaceContent.currentText = displaymanager.currentText; | ||
| 110 | } | ||
| 111 | } | ||
| 112 | } | ||
| 113 | } | ||
| 114 | } | ||
| 115 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/shell.qml b/accounts/gkleen@sif/shell/quickshell/shell.qml new file mode 100644 index 00000000..fb8b16dc --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/shell.qml | |||
| @@ -0,0 +1,53 @@ | |||
| 1 | //@ pragma UseQApplication | ||
| 2 | |||
| 3 | import Quickshell | ||
| 4 | import Quickshell.Wayland | ||
| 5 | |||
| 6 | ShellRoot { | ||
| 7 | settings.watchFiles: false | ||
| 8 | |||
| 9 | Variants { | ||
| 10 | model: Quickshell.screens | ||
| 11 | |||
| 12 | delegate: Scope { | ||
| 13 | id: screenScope | ||
| 14 | |||
| 15 | required property var modelData | ||
| 16 | |||
| 17 | PanelWindow { | ||
| 18 | id: bgWindow | ||
| 19 | |||
| 20 | screen: screenScope.modelData | ||
| 21 | |||
| 22 | WlrLayershell.layer: WlrLayer.Background | ||
| 23 | WlrLayershell.namespace: "background" | ||
| 24 | exclusionMode: ExclusionMode.Ignore | ||
| 25 | |||
| 26 | anchors.top: true | ||
| 27 | anchors.bottom: true | ||
| 28 | anchors.left: true | ||
| 29 | anchors.right: true | ||
| 30 | |||
| 31 | color: "black" | ||
| 32 | |||
| 33 | WallpaperBackground { | ||
| 34 | screen: bgWindow.screen.name | ||
| 35 | } | ||
| 36 | } | ||
| 37 | |||
| 38 | Bar { | ||
| 39 | modelData: screenScope.modelData | ||
| 40 | } | ||
| 41 | } | ||
| 42 | } | ||
| 43 | |||
| 44 | Lockscreen {} | ||
| 45 | NiriIdle {} | ||
| 46 | |||
| 47 | VolumeOSD {} | ||
| 48 | BrightnessOSD {} | ||
| 49 | |||
| 50 | NotificationDisplay {} | ||
| 51 | |||
| 52 | UnixIPC {} | ||
| 53 | } | ||
