summaryrefslogtreecommitdiff
path: root/accounts/gkleen@sif/shell/quickshell
diff options
context:
space:
mode:
Diffstat (limited to 'accounts/gkleen@sif/shell/quickshell')
-rw-r--r--accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml182
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Bar.qml29
-rw-r--r--accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml130
-rw-r--r--accounts/gkleen@sif/shell/quickshell/BrightnessOSD.qml117
-rw-r--r--accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml78
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Clock.qml6
-rw-r--r--accounts/gkleen@sif/shell/quickshell/LidSwitchInhibitorWidget.qml46
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Lockscreen.qml41
-rw-r--r--accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml17
-rw-r--r--accounts/gkleen@sif/shell/quickshell/NiriIdle.qml30
-rw-r--r--accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml415
-rw-r--r--accounts/gkleen@sif/shell/quickshell/PrivacyWidget.qml49
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml75
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/InhibitorState.qml22
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/Privacy.qml63
-rw-r--r--accounts/gkleen@sif/shell/quickshell/SystemTray.qml55
-rw-r--r--accounts/gkleen@sif/shell/quickshell/UnixIPC.qml59
-rw-r--r--accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml57
-rw-r--r--accounts/gkleen@sif/shell/quickshell/WaylandInhibitorWidget.qml55
-rw-r--r--accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml2
-rw-r--r--accounts/gkleen@sif/shell/quickshell/shell.qml4
21 files changed, 1291 insertions, 241 deletions
diff --git a/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml b/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml
index 57ade488..883f9001 100644
--- a/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml
+++ b/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml
@@ -22,72 +22,144 @@ Item {
22 width: activeWindowDisplayContent.width 22 width: activeWindowDisplayContent.width
23 height: parent.height 23 height: parent.height
24 24
25 Row { 25 WrapperMouseArea {
26 id: activeWindowDisplayContent 26 id: widgetMouseArea
27 27
28 width: childrenRect.width 28 anchors.fill: parent
29 height: parent.height
30 anchors.verticalCenter: parent.verticalCenter
31 spacing: 8
32 29
33 IconImage { 30 hoverEnabled: true
34 id: activeWindowIcon
35 31
36 height: 14 32 Item {
37 width: 14 33 anchors.fill: parent
38 34
39 anchors.verticalCenter: parent.verticalCenter 35 Row {
36 id: activeWindowDisplayContent
40 37
41 source: { 38 width: childrenRect.width
42 let icon = activeWindowDisplay.windowEntry?.icon 39 height: parent.height
43 if (typeof icon === 'string' || icon instanceof String) { 40 anchors.verticalCenter: parent.verticalCenter
44 if (icon.includes("?path=")) { 41 spacing: 8
45 const split = icon.split("?path=") 42
46 if (split.length !== 2) 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);
47 return icon 64 return icon
48 const name = split[0] 65 }
49 const path = split[1] 66 return ""
50 const fileName = name.substring( 67 }
51 name.lastIndexOf("/") + 1) 68 asynchronous: true
52 return `file://${path}/${fileName}` 69 smooth: true
53 } else 70 mipmap: true
54 icon = Quickshell.iconPath(icon); 71 }
55 return icon 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 }
56 } 101 }
57 return ""
58 } 102 }
59 asynchronous: true
60 smooth: true
61 mipmap: true
62 } 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.centerIn: parent
153
154 Text {
155 id: widgetTooltipText
156
157 font.pointSize: 10
158 font.family: "Fira Mono"
159 color: "white"
63 160
64 Text { 161 text: JSON.stringify(Object.assign({}, activeWindowDisplay.activeWindow), null, 2)
65 id: windowTitle
66
67 width: Math.min(implicitWidth, activeWindowDisplay.maxWidth - activeWindowIcon.width - activeWindowDisplayContent.spacing)
68
69 property var appAliases: { "Firefox": "Mozilla Firefox", "mpv Media Player": "mpv", "Thunderbird": "Mozilla Thunderbird", "Thunderbird (LMU)": "Mozilla Thunderbird" }
70
71 elide: Text.ElideRight
72 maximumLineCount: 1
73 color: "white"
74 anchors.verticalCenter: parent.verticalCenter
75 text: {
76 if (!activeWindowDisplay.activeWindow)
77 return "";
78
79 var title = activeWindowDisplay.activeWindow.title;
80 var appName = activeWindowDisplay.windowEntry?.name;
81 if (appAliases[appName])
82 appName = appAliases[appName];
83 if (appName && title.endsWith(appName)) {
84 const oldTitle = title;
85 title = title.substring(0, title.length - appName.length);
86 title = title.replace(/\s*(—|-)\s*$/, "");
87 if (!title)
88 title = oldTitle;
89 } 162 }
90 return title;
91 } 163 }
92 } 164 }
93 } 165 }
diff --git a/accounts/gkleen@sif/shell/quickshell/Bar.qml b/accounts/gkleen@sif/shell/quickshell/Bar.qml
index 3652af54..f8092604 100644
--- a/accounts/gkleen@sif/shell/quickshell/Bar.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml
@@ -1,7 +1,6 @@
1import Quickshell 1import Quickshell
2import QtQuick 2import QtQuick
3 3
4
5PanelWindow { 4PanelWindow {
6 id: bar 5 id: bar
7 6
@@ -64,25 +63,41 @@ PanelWindow {
64 anchors.verticalCenter: parent.verticalCenter 63 anchors.verticalCenter: parent.verticalCenter
65 spacing: 0 64 spacing: 0
66 65
67 PipewireWidget {} 66 KeyboardLayout {}
68 67
69 Item { 68 Item {
69 visible: privacy.visible
70 height: parent.height 70 height: parent.height
71 width: 4 71 width: 8 - 4
72 } 72 }
73 73
74 SystemTray {} 74 PrivacyWidget {
75 id: privacy
76 }
75 77
76 Item { 78 Item {
79 visible: privacy.visible
77 height: parent.height 80 height: parent.height
78 width: 4 81 width: 8 - 4
79 } 82 }
80 83
81 KeyboardLayout {} 84 SystemTray {}
85
86 PipewireWidget {}
87
88 BrightnessWidget {}
89
90 BatteryWidget {}
91
92 WaylandInhibitorWidget {
93 window: bar
94 }
95
96 LidSwitchInhibitorWidget {}
82 97
83 Item { 98 Item {
84 height: parent.height 99 height: parent.height
85 width: 4 100 width: 8 - 4
86 } 101 }
87 102
88 Clock {} 103 Clock {}
diff --git a/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml b/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml
new file mode 100644
index 00000000..fd031627
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml
@@ -0,0 +1,130 @@
1import QtQuick
2import Quickshell
3import Quickshell.Widgets
4import Quickshell.Services.UPower
5
6Item {
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.centerIn: parent
91
92 Text {
93 id: widgetTooltipText
94
95 font.pointSize: 10
96 font.family: "Fira Sans"
97 color: "white"
98
99 text: {
100 const stateStr = UPowerDeviceState.toString(root.batteryDevice.state);
101 var outStr = stateStr;
102 if (root.batteryDevice.state != UPowerDeviceState.FullyCharged)
103 outStr += ` ${Math.round(root.batteryDevice.percentage * 100)}%`;
104
105 function formatTime(t) {
106 var res = "";
107 for (const unit of [{ "s": "h", "v": 3600 }, { "s": "m", "v": 60 }, { "s": "s", "v": 1 }]) {
108 if (t < unit.v)
109 continue;
110 res += Math.floor(t / unit.v) + unit.s;
111 t %= unit.v;
112 }
113 return res;
114 }
115 if (root.batteryDevice.timeToEmpty != 0) {
116 const tStr = formatTime(Math.floor(root.batteryDevice.timeToEmpty / 60) * 60);
117 if (tStr)
118 outStr += " " + tStr;
119 } else if (root.batteryDevice.timeToFull != 0) {
120 const tStr = formatTime(Math.ceil(root.batteryDevice.timeToFull / 60) * 60);
121 if (tStr)
122 outStr += " " + tStr;
123 }
124
125 return outStr;
126 }
127 }
128 }
129 }
130}
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 @@
1import QtQuick
2import QtQuick.Layouts
3import Quickshell
4import Quickshell.Widgets
5import qs.Services
6
7Scope {
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..7f9c1ad0
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml
@@ -0,0 +1,78 @@
1import QtQuick
2import Quickshell
3import Quickshell.Widgets
4import qs.Services
5
6Item {
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.centerIn: parent
66
67 Text {
68 id: widgetTooltipText
69
70 font.pointSize: 10
71 font.family: "Fira Sans"
72 color: "white"
73
74 text: `${Math.round(Brightness.currBrightness * 100)}%`
75 }
76 }
77 }
78}
diff --git a/accounts/gkleen@sif/shell/quickshell/Clock.qml b/accounts/gkleen@sif/shell/quickshell/Clock.qml
index 4644d5e7..bb618f6a 100644
--- a/accounts/gkleen@sif/shell/quickshell/Clock.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Clock.qml
@@ -73,7 +73,11 @@ Item {
73 Timer { 73 Timer {
74 id: hangTimer 74 id: hangTimer
75 interval: 100 75 interval: 100
76 onTriggered: tooltip.visible = tooltip.nextVisible 76 onTriggered: {
77 tooltip.visible = tooltip.nextVisible;
78 if (!tooltip.visible)
79 tooltipLoader.active = false;
80 }
77 } 81 }
78 82
79 implicitWidth: clockTooltipContent.width 83 implicitWidth: clockTooltipContent.width
diff --git a/accounts/gkleen@sif/shell/quickshell/LidSwitchInhibitorWidget.qml b/accounts/gkleen@sif/shell/quickshell/LidSwitchInhibitorWidget.qml
new file mode 100644
index 00000000..2be0692a
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/LidSwitchInhibitorWidget.qml
@@ -0,0 +1,46 @@
1import Quickshell
2import QtQuick
3import Quickshell.Widgets
4import qs.Services
5
6Item {
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
20 onClicked: InhibitorState.lidSwitchInhibited = !InhibitorState.lidSwitchInhibited
21
22 Rectangle {
23 anchors.fill: parent
24 color: {
25 if (widgetMouseArea.containsMouse) {
26 return "#33808080";
27 }
28 return "transparent";
29 }
30
31 Item {
32 anchors.fill: parent
33
34 MaterialDesignIcon {
35 id: icon
36
37 implicitSize: 14
38 anchors.centerIn: parent
39
40 icon: InhibitorState.lidSwitchInhibited ? "laptop-off" : "laptop"
41 color: InhibitorState.lidSwitchInhibited ? "#f28a21" : "#555"
42 }
43 }
44 }
45 }
46}
diff --git a/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
index 8e739359..ac2e38f8 100644
--- a/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
@@ -38,6 +38,43 @@ Scope {
38 function getLocked(): bool { return lock.locked; } 38 function getLocked(): bool { return lock.locked; }
39 } 39 }
40 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 Scope {
74 id: mprisProxy
75 property list<var> players: Mpris.players.values
76 }
77
41 WlSessionLock { 78 WlSessionLock {
42 id: lock 79 id: lock
43 80
@@ -47,7 +84,7 @@ Scope {
47 84
48 if (locked) { 85 if (locked) {
49 Custom.KeePassXC.lockAllDatabases(); 86 Custom.KeePassXC.lockAllDatabases();
50 Array.from(Mpris.players.values).forEach(player => { 87 Array.from(mprisProxy.players).forEach(player => {
51 if (player.canPause && player.isPlaying) 88 if (player.canPause && player.isPlaying)
52 player.pause(); 89 player.pause();
53 }); 90 });
@@ -56,6 +93,8 @@ Scope {
56 } 93 }
57 } 94 }
58 95
96 onSecureStateChanged: Custom.Systemd.lockedHint = lock.secure
97
59 WlSessionLockSurface { 98 WlSessionLockSurface {
60 id: lockSurface 99 id: lockSurface
61 100
diff --git a/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml b/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml
index 387dcc8b..155a009e 100644
--- a/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml
+++ b/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml
@@ -2,15 +2,26 @@ import QtQuick
2import QtQuick.Effects 2import QtQuick.Effects
3 3
4Item { 4Item {
5 id: icon 5 id: root
6 6
7 required property string icon 7 required property string icon
8 property color color: "white" 8 property color color: "white"
9 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
10 Image { 17 Image {
11 id: sourceImage 18 id: sourceImage
12 source: "file://" + @mdi@ + "/svg/" + icon.icon + ".svg" 19 source: "file://" + @mdi@ + "/svg/" + root.icon + ".svg"
13 anchors.fill: parent 20 anchors.fill: parent
21 fillMode: Image.PreserveAspectFit
22
23 sourceSize.width: root.actualSize
24 sourceSize.height: root.actualSize
14 25
15 layer.enabled: true 26 layer.enabled: true
16 layer.effect: MultiEffect { 27 layer.effect: MultiEffect {
@@ -18,7 +29,7 @@ Item {
18 29
19 brightness: 1 30 brightness: 1
20 colorization: 1 31 colorization: 1
21 colorizationColor: icon.color 32 colorizationColor: root.color
22 } 33 }
23 } 34 }
24} 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..faa77c3f
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/NiriIdle.qml
@@ -0,0 +1,30 @@
1import QtQml
2import Quickshell
3import Quickshell.Wayland
4import qs.Services
5import Custom as Custom
6
7Scope {
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/PipewireWidget.qml b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
index 7fdb5006..3e0b8fd9 100644
--- a/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
+++ b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
@@ -50,8 +50,7 @@ Item {
50 MaterialDesignIcon { 50 MaterialDesignIcon {
51 id: volumeIcon 51 id: volumeIcon
52 52
53 width: 16 53 implicitSize: 14
54 height: 16
55 anchors.centerIn: parent 54 anchors.centerIn: parent
56 55
57 icon: { 56 icon: {
@@ -115,232 +114,350 @@ Item {
115 114
116 implicitWidth: tooltipContent.width 115 implicitWidth: tooltipContent.width
117 implicitHeight: tooltipContent.height 116 implicitHeight: tooltipContent.height
118 color: "black" 117 color: "transparent"
119 118
120 WrapperMouseArea { 119 Rectangle {
121 id: tooltipMouseArea 120 width: tooltip.width
121 height: tooltipLayout.childrenRect.height + 16
122 color: "black"
123 }
122 124
123 hoverEnabled: true 125 WrapperItem {
124 enabled: true 126 id: tooltipContent
125 127
126 anchors.fill: parent 128 bottomMargin: Math.max(0, 200 - tooltipLayout.implicitHeight)
129
130 WrapperMouseArea {
131 id: tooltipMouseArea
127 132
128 WrapperItem { 133 hoverEnabled: true
129 id: tooltipContent 134 enabled: true
130 135
131 margin: 8 136 WrapperItem {
132 bottomMargin: 8 + Math.max(0, 200 - tooltipLayout.implicitHeight) 137 margin: 8
138 bottomMargin: 8
133 139
134 GridLayout { 140 GridLayout {
135 id: tooltipLayout 141 id: tooltipLayout
136 142
137 columns: 4 143 columns: 4
138 144
139 Repeater { 145 Repeater {
140 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device") 146 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
141 147
142 Item { 148 Item {
143 id: descItem 149 id: descItem
144 150
145 required property var modelData 151 required property var modelData
146 required property int index 152 required property int index
147 153
148 Layout.column: 0 154 Layout.column: 0
149 Layout.row: index 155 Layout.row: index
150 156
151 implicitWidth: descText.contentWidth 157 implicitWidth: descText.contentWidth
152 implicitHeight: descText.contentHeight 158 implicitHeight: descText.contentHeight
153 159
154 Text { 160 Text {
155 id: descText 161 id: descText
156 162
157 color: "white" 163 color: "white"
158 font.pointSize: 10 164 font.pointSize: 10
159 font.family: "Fira Sans" 165 font.family: "Fira Sans"
160 166
161 text: descItem.modelData.description 167 text: descItem.modelData.description
168 }
162 } 169 }
163 } 170 }
164 }
165 171
166 Repeater { 172 Repeater {
167 id: defaultSinkRepeater 173 id: defaultSinkRepeater
168 174
169 model: { 175 model: {
170 Array.from(Pipewire.devices.values) 176 Array.from(Pipewire.devices.values)
171 .filter(dev => dev.type == "Audio/Device") 177 .filter(dev => dev.type == "Audio/Device")
172 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSink && node.device?.id == device.id )); 178 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSink && node.device?.id == device.id ));
173 } 179 }
174 180
175 Item { 181 Item {
176 id: defaultSinkItem 182 id: defaultSinkItem
177 183
178 required property var modelData 184 required property var modelData
179 required property int index 185 required property int index
180 186
181 PwObjectTracker { 187 visible: Boolean(modelData)
182 objects: [defaultSinkItem.modelData]
183 }
184 188
185 Layout.column: 1 189 PwObjectTracker {
186 Layout.row: index 190 objects: [defaultSinkItem.modelData]
191 }
187 192
188 Layout.fillHeight: true 193 Layout.column: 1
194 Layout.row: index
189 195
190 implicitWidth: 16 + 8 196 Layout.fillHeight: true
191 197
192 WrapperMouseArea { 198 implicitWidth: 16 + 8
193 id: defaultSinkMouseArea
194 199
195 anchors.fill: parent 200 WrapperMouseArea {
196 hoverEnabled: true 201 id: defaultSinkMouseArea
197 cursorShape: Qt.PointingHandCursor
198 202
199 onClicked: { 203 anchors.fill: parent
200 Pipewire.preferredDefaultAudioSink = defaultSinkItem.modelData 204 hoverEnabled: true
201 } 205 cursorShape: Qt.PointingHandCursor
202 206
203 Rectangle { 207 onClicked: {
204 id: defaultSinkWidget 208 Pipewire.preferredDefaultAudioSink = defaultSinkItem.modelData
209 }
205 210
206 anchors.fill: parent 211 onWheel: event => scrollVolume(event);
207 color: { 212 property real sensitivity: (1 / 40) / 120
208 if (defaultSinkMouseArea.containsMouse) 213 function scrollVolume(event) {
209 return "#33808080"; 214 defaultSinkItem.modelData.audio.volume += event.angleDelta.y * sensitivity;
210 return "transparent";
211 } 215 }
212 216
213 MaterialDesignIcon { 217 Rectangle {
214 width: 16 218 id: defaultSinkWidget
215 height: 16 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
216 anchors.centerIn: parent 275 anchors.centerIn: parent
217 276
218 icon: { 277 Text {
219 if (defaultSinkItem.modelData?.id == Pipewire.defaultAudioSink?.id) 278 id: volumeTooltipText
220 return "speaker"; 279
221 return "speaker-off"; 280 font.pointSize: 10
281 font.family: "Fira Sans"
282 color: "white"
283
284 text: `${Math.round(defaultSinkItem.modelData?.audio?.volume * 100)}%`
222 } 285 }
223 color: icon == "speaker" ? "white" : "#555"
224 } 286 }
225 } 287 }
226 } 288 }
227 } 289 }
228 }
229 290
230 Repeater { 291 Repeater {
231 id: defaultSourceRepeater 292 id: defaultSourceRepeater
232 293
233 model: { 294 model: {
234 Array.from(Pipewire.devices.values) 295 Array.from(Pipewire.devices.values)
235 .filter(dev => dev.type == "Audio/Device") 296 .filter(dev => dev.type == "Audio/Device")
236 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSource && node.device?.id == device.id )); 297 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSource && node.device?.id == device.id ));
237 } 298 }
238 299
239 Item { 300 Item {
240 id: defaultSourceItem 301 id: defaultSourceItem
241 302
242 required property var modelData 303 required property var modelData
243 required property int index 304 required property int index
244 305
245 PwObjectTracker { 306 visible: Boolean(modelData)
246 objects: [defaultSourceItem.modelData] 307
247 } 308 PwObjectTracker {
309 objects: [defaultSourceItem.modelData]
310 }
311
312 Layout.column: 2
313 Layout.row: index
248 314
249 Layout.column: 2 315 Layout.fillHeight: true
250 Layout.row: index 316
317 implicitWidth: 16 + 8
318
319 WrapperMouseArea {
320 id: defaultSourceMouseArea
321
322 anchors.fill: parent
323 hoverEnabled: true
324 cursorShape: Qt.PointingHandCursor
251 325
252 Layout.fillHeight: true 326 onClicked: {
327 Pipewire.preferredDefaultAudioSource = defaultSourceItem.modelData
328 }
253 329
254 implicitWidth: 16 + 8 330 onWheel: event => scrollVolume(event);
331 property real sensitivity: (1 / 40) / 120
332 function scrollVolume(event) {
333 defaultSourceItem.modelData.audio.volume += event.angleDelta.y * sensitivity;
334 }
255 335
256 WrapperMouseArea { 336 Rectangle {
257 id: defaultSourceMouseArea 337 id: defaultSourceWidget
258 338
259 anchors.fill: parent 339 anchors.fill: parent
260 hoverEnabled: true 340 color: {
261 cursorShape: Qt.PointingHandCursor 341 if (defaultSourceMouseArea.containsMouse)
342 return "#33808080";
343 return "transparent";
344 }
262 345
263 onClicked: { 346 MaterialDesignIcon {
264 Pipewire.preferredDefaultAudioSource = defaultSourceItem.modelData 347 width: 16
348 height: 16
349 anchors.centerIn: parent
350
351 icon: {
352 if (defaultSourceItem.modelData?.id == Pipewire.defaultAudioSource?.id)
353 return "microphone";
354 return "microphone-off";
355 }
356 color: icon == "microphone" ? "white" : "#555"
357 }
358 }
265 } 359 }
266 360
267 Rectangle { 361 PopupWindow {
268 id: defaultSourceWidget 362 id: volumeTooltip
269 363
270 anchors.fill: parent 364 property bool nextVisible: defaultSourceMouseArea.containsMouse || volumeTooltipMouseArea.containsMouse
271 color: { 365
272 if (defaultSourceMouseArea.containsMouse) 366 anchor {
273 return "#33808080"; 367 item: defaultSourceMouseArea
274 return "transparent"; 368 edges: Edges.Bottom | Edges.Left
275 } 369 }
370 visible: false
371
372 onNextVisibleChanged: volumeHangTimer.restart()
373
374 onVisibleChanged: tooltip.openPopup = volumeTooltip.visible
375
376 Timer {
377 id: volumeHangTimer
378 interval: 100
379 onTriggered: volumeTooltip.visible = volumeTooltip.nextVisible
380 }
381
382 implicitWidth: volumeTooltipText.contentWidth + 16
383 implicitHeight: volumeTooltipText.contentHeight + 16
384 color: "black"
385
386 WrapperMouseArea {
387 id: volumeTooltipMouseArea
388
389 hoverEnabled: true
390 enabled: true
391
392 onWheel: event => defaultSourceMouseArea.scrollVolume(event);
276 393
277 MaterialDesignIcon {
278 width: 16
279 height: 16
280 anchors.centerIn: parent 394 anchors.centerIn: parent
281 395
282 icon: { 396 Text {
283 if (defaultSourceItem.modelData?.id == Pipewire.defaultAudioSource?.id) 397 id: volumeTooltipText
284 return "microphone"; 398
285 return "microphone-off"; 399 font.pointSize: 10
400 font.family: "Fira Sans"
401 color: "white"
402
403 text: `${Math.round(defaultSourceItem.modelData?.audio?.volume * 100)}%`
286 } 404 }
287 color: icon == "microphone" ? "white" : "#555"
288 } 405 }
289 } 406 }
290 } 407 }
291 } 408 }
292 }
293 409
294 Repeater { 410 Repeater {
295 id: profileRepeater 411 id: profileRepeater
296 412
297 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device") 413 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
298 414
299 Item { 415 Item {
300 id: profileItem 416 id: profileItem
301 417
302 required property var modelData 418 required property var modelData
303 required property int index 419 required property int index
304 420
305 PwObjectTracker { 421 PwObjectTracker {
306 objects: [profileItem.modelData] 422 objects: [profileItem.modelData]
307 } 423 }
308 424
309 Layout.column: 3 425 Layout.column: 3
310 Layout.row: index 426 Layout.row: index
311 427
312 Layout.fillWidth: true 428 Layout.fillWidth: true
313 429
314 implicitWidth: Math.max(profileBox.implicitWidth, 300) 430 implicitWidth: Math.max(profileBox.implicitWidth, 300)
315 implicitHeight: profileBox.height 431 implicitHeight: profileBox.height
316 432
317 ComboBox { 433 ComboBox {
318 id: profileBox 434 id: profileBox
319 435
320 model: profileItem.modelData.profiles 436 model: profileItem.modelData.profiles
321 437
322 textRole: "description" 438 textRole: "description"
323 valueRole: "index" 439 valueRole: "index"
324 onActivated: profileItem.modelData.setProfile(currentValue) 440 onActivated: profileItem.modelData.setProfile(currentValue)
325 441
326 anchors.fill: parent 442 anchors.fill: parent
327 443
328 implicitContentWidthPolicy: ComboBox.WidestText 444 implicitContentWidthPolicy: ComboBox.WidestText
329 445
330 Connections { 446 Connections {
331 target: profileItem.modelData 447 target: profileItem.modelData
332 function onCurrentProfileChanged() { 448 function onCurrentProfileChanged() {
449 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
450 }
451 }
452 Component.onCompleted: {
333 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile); 453 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
334 } 454 }
335 }
336 Component.onCompleted: {
337 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
338 }
339 455
340 Connections { 456 Connections {
341 target: profileBox.popup 457 target: profileBox.popup
342 function onVisibleChanged() { 458 function onVisibleChanged() {
343 tooltip.openPopup = profileBox.popup.visible 459 tooltip.openPopup = profileBox.popup.visible
460 }
344 } 461 }
345 } 462 }
346 } 463 }
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 @@
1import QtQuick
2import QtQuick.Layouts
3import Quickshell
4import Quickshell.Widgets
5import qs.Services
6
7Item {
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 @@
1pragma Singleton
2
3import QtQml
4import Quickshell
5import Quickshell.Io
6import Custom as Custom
7
8Singleton {
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/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 @@
1pragma Singleton
2
3import Quickshell
4import Custom as Custom
5
6Singleton {
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/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 @@
1pragma Singleton
2
3import QtQml
4import Quickshell
5import Quickshell.Services.Pipewire
6
7Singleton {
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/SystemTray.qml b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
index 55b1690e..351e74ee 100644
--- a/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
+++ b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
@@ -1,4 +1,5 @@
1import QtQuick 1import QtQuick
2import QtQuick.Effects
2import Quickshell 3import Quickshell
3import Quickshell.Widgets 4import Quickshell.Widgets
4import Quickshell.Services.SystemTray 5import Quickshell.Services.SystemTray
@@ -28,6 +29,9 @@ Item {
28 delegate: Item { 29 delegate: Item {
29 id: trayItemWrapper 30 id: trayItemWrapper
30 31
32 required property var modelData
33 required property int index
34
31 property var trayItem: modelData 35 property var trayItem: modelData
32 property string iconSource: { 36 property string iconSource: {
33 let icon = trayItem && trayItem.icon 37 let icon = trayItem && trayItem.icon
@@ -47,7 +51,7 @@ Item {
47 return "" 51 return ""
48 } 52 }
49 53
50 width: 16 54 width: icon.width + 6
51 height: parent.height 55 height: parent.height
52 anchors.verticalCenter: parent.verticalCenter 56 anchors.verticalCenter: parent.verticalCenter
53 57
@@ -83,14 +87,41 @@ Item {
83 } 87 }
84 } 88 }
85 89
86 IconImage { 90 Rectangle {
87 anchors.centerIn: parent 91 anchors.fill: parent
88 width: parent.width 92 color: {
89 height: parent.width 93 if (trayItemArea.containsMouse)
90 source: trayItemWrapper.iconSource 94 return "#33808080";
91 asynchronous: true 95 return "transparent";
92 smooth: true 96 }
93 mipmap: true 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 }
94 } 125 }
95 } 126 }
96 127
@@ -116,7 +147,7 @@ Item {
116 color: "black" 147 color: "black"
117 148
118 implicitWidth: Math.max(tooltipTitle.contentWidth, tooltipDescription.contentWidth) + 16 149 implicitWidth: Math.max(tooltipTitle.contentWidth, tooltipDescription.contentWidth) + 16
119 implicitHeight: tooltipTitle.contentHeight + tooltipDescription.contentHeight + 16 150 implicitHeight: (trayItem.tooltipTitle ? tooltipTitle.contentHeight : 0) + (trayItem.tooltipDescription ? tooltipDescription.contentHeight : 0) + 16
120 151
121 WrapperMouseArea { 152 WrapperMouseArea {
122 id: tooltipMouseArea 153 id: tooltipMouseArea
@@ -130,6 +161,8 @@ Item {
130 Text { 161 Text {
131 id: tooltipTitle 162 id: tooltipTitle
132 163
164 enabled: trayItem.tooltipTitle
165
133 font.pointSize: 10 166 font.pointSize: 10
134 font.family: "Fira Sans" 167 font.family: "Fira Sans"
135 font.bold: true 168 font.bold: true
@@ -141,6 +174,8 @@ Item {
141 Text { 174 Text {
142 id: tooltipDescription 175 id: tooltipDescription
143 176
177 enabled: trayItem.tooltipDescription
178
144 font.pointSize: 10 179 font.pointSize: 10
145 font.family: "Fira Sans" 180 font.family: "Fira Sans"
146 color: "white" 181 color: "white"
diff --git a/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml b/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml
new file mode 100644
index 00000000..742ef4f5
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml
@@ -0,0 +1,59 @@
1import Quickshell
2import Quickshell.Io
3import Quickshell.Services.Pipewire
4import qs.Services
5
6Scope {
7 id: root
8
9 SocketServer {
10 active: true
11 path: `${Quickshell.env("XDG_RUNTIME_DIR")}/shell.sock`
12 handler: Socket {
13 parser: SplitParser {
14 onRead: line => {
15 try {
16 const command = JSON.parse(line);
17
18 if (command.Volume)
19 root.onCommandVolume(command.Volume);
20 else if (command.Brightness)
21 root.onCommandBrightness(command.Brightness);
22 else
23 console.warn("UnixIPC: Command not handled:", JSON.stringify(command));
24 } catch (e) {
25 console.warn("UnixIPC: Failed to parse command:", line, e);
26 }
27 }
28 }
29
30 onError: e => {
31 if (e == 1)
32 return;
33 console.warn("QLocalSocket::LocalSocketError", e);
34 }
35 }
36 }
37
38 PwObjectTracker {
39 objects: [ Pipewire.defaultAudioSink, Pipewire.defaultAudioSource ]
40 }
41 function onCommandVolume(command) {
42 if (command.muted === "toggle")
43 Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted;
44 if (command.volume === "up")
45 Pipewire.defaultAudioSink.audio.volume += 0.02;
46 if (command.volume === "down")
47 Pipewire.defaultAudioSink.audio.volume -= 0.02;
48
49 if (command["mic-muted"] === "toggle")
50 Pipewire.defaultAudioSource.audio.muted = !Pipewire.defaultAudioSource.audio.muted;
51 }
52
53 function onCommandBrightness(command) {
54 if (command === "up")
55 Brightness.currBrightness += 0.02
56 if (command === "down")
57 Brightness.currBrightness -= 0.02
58 }
59}
diff --git a/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml b/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml
index 02dcf227..653f4763 100644
--- a/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml
+++ b/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml
@@ -7,11 +7,11 @@ import Quickshell.Widgets
7Scope { 7Scope {
8 id: root 8 id: root
9 9
10 property bool show: false 10 property string show: ""
11 property bool inhibited: true 11 property bool inhibited: true
12 12
13 PwObjectTracker { 13 PwObjectTracker {
14 objects: [ Pipewire.defaultAudioSink ] 14 objects: [ Pipewire.defaultAudioSink, Pipewire.defaultAudioSource ]
15 } 15 }
16 16
17 Connections { 17 Connections {
@@ -19,11 +19,25 @@ Scope {
19 target: Pipewire.defaultAudioSink?.audio 19 target: Pipewire.defaultAudioSink?.audio
20 20
21 function onVolumeChanged() { 21 function onVolumeChanged() {
22 root.show = true; 22 root.show = "sink";
23 hideTimer.restart(); 23 hideTimer.restart();
24 } 24 }
25 function onMutedChanged() { 25 function onMutedChanged() {
26 root.show = true; 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";
27 hideTimer.restart(); 41 hideTimer.restart();
28 } 42 }
29 } 43 }
@@ -36,7 +50,7 @@ Scope {
36 Timer { 50 Timer {
37 id: hideTimer 51 id: hideTimer
38 interval: 1000 52 interval: 1000
39 onTriggered: root.show = false 53 onTriggered: root.show = ""
40 } 54 }
41 55
42 Timer { 56 Timer {
@@ -44,7 +58,7 @@ Scope {
44 interval: 100 58 interval: 100
45 running: true 59 running: true
46 onTriggered: { 60 onTriggered: {
47 root.show = false; 61 root.show = "";
48 root.inhibited = false; 62 root.inhibited = false;
49 } 63 }
50 } 64 }
@@ -97,13 +111,22 @@ Scope {
97 implicitHeight: parent.height 111 implicitHeight: parent.height
98 112
99 icon: { 113 icon: {
100 if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted) 114 if (root.show == "sink") {
101 return "volume-off"; 115 if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted)
102 if (Pipewire.defaultAudioSink.audio.volume <= 0.33) 116 return "volume-off";
103 return "volume-low"; 117 if (Pipewire.defaultAudioSink.audio.volume <= 0.33)
104 if (Pipewire.defaultAudioSink.audio.volume <= 0.67) 118 return "volume-low";
105 return "volume-medium"; 119 if (Pipewire.defaultAudioSink.audio.volume <= 0.67)
106 return "volume-high"; 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";
107 } 130 }
108 } 131 }
109 132
@@ -123,7 +146,13 @@ Scope {
123 146
124 color: Pipewire.defaultAudioSink?.audio.muted ? "#70ffffff" : "white" 147 color: Pipewire.defaultAudioSink?.audio.muted ? "#70ffffff" : "white"
125 148
126 implicitWidth: parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0) 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 }
127 } 156 }
128 } 157 }
129 } 158 }
diff --git a/accounts/gkleen@sif/shell/quickshell/WaylandInhibitorWidget.qml b/accounts/gkleen@sif/shell/quickshell/WaylandInhibitorWidget.qml
new file mode 100644
index 00000000..0633f350
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/WaylandInhibitorWidget.qml
@@ -0,0 +1,55 @@
1import Quickshell
2import QtQuick
3import Quickshell.Widgets
4import Quickshell.Wayland
5import qs.Services
6
7Item {
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
29 onClicked: InhibitorState.waylandIdleInhibited = !InhibitorState.waylandIdleInhibited
30
31 Rectangle {
32 anchors.fill: parent
33 color: {
34 if (widgetMouseArea.containsMouse) {
35 return "#33808080";
36 }
37 return "transparent";
38 }
39
40 Item {
41 anchors.fill: parent
42
43 MaterialDesignIcon {
44 id: icon
45
46 implicitSize: 14
47 anchors.centerIn: parent
48
49 icon: inhibitor.enabled ? "eye" : "eye-off"
50 color: inhibitor.enabled ? "white" : "#555"
51 }
52 }
53 }
54 }
55}
diff --git a/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml b/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml
index c8c017c3..4cbebcc9 100644
--- a/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml
+++ b/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml
@@ -48,7 +48,7 @@ Row {
48 cursorShape: Qt.PointingHandCursor 48 cursorShape: Qt.PointingHandCursor
49 enabled: true 49 enabled: true
50 onClicked: { 50 onClicked: {
51 NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {}) 51 NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {});
52 } 52 }
53 53
54 Rectangle { 54 Rectangle {
diff --git a/accounts/gkleen@sif/shell/quickshell/shell.qml b/accounts/gkleen@sif/shell/quickshell/shell.qml
index 3657f77f..693d741f 100644
--- a/accounts/gkleen@sif/shell/quickshell/shell.qml
+++ b/accounts/gkleen@sif/shell/quickshell/shell.qml
@@ -41,6 +41,10 @@ ShellRoot {
41 } 41 }
42 42
43 Lockscreen {} 43 Lockscreen {}
44 NiriIdle {}
44 45
45 VolumeOSD {} 46 VolumeOSD {}
47 BrightnessOSD {}
48
49 UnixIPC {}
46} 50}