summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGregor Kleen <gkleen@yggdrasil.li>2025-08-30 22:07:54 +0200
committerGregor Kleen <gkleen@yggdrasil.li>2025-08-30 22:07:54 +0200
commit45a1316bf1df6ec32a133f8e648bbac0bbc988d6 (patch)
tree4b268e50fef8c0f61e64a2685cb51f421d9e7fc8
parentc3a8a171734bfeced58f4611365e85a6daed7db9 (diff)
downloadnixos-45a1316bf1df6ec32a133f8e648bbac0bbc988d6.tar
nixos-45a1316bf1df6ec32a133f8e648bbac0bbc988d6.tar.gz
nixos-45a1316bf1df6ec32a133f8e648bbac0bbc988d6.tar.bz2
nixos-45a1316bf1df6ec32a133f8e648bbac0bbc988d6.tar.xz
nixos-45a1316bf1df6ec32a133f8e648bbac0bbc988d6.zip
...
-rw-r--r--accounts/gkleen@sif/default.nix4
-rw-r--r--accounts/gkleen@sif/niri/waybar.nix2
-rw-r--r--accounts/gkleen@sif/shell/default.nix1
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Bar.qml307
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml190
-rw-r--r--accounts/gkleen@sif/shell/quickshell/shell.qml2
-rw-r--r--accounts/gkleen@sif/systemd.nix4
7 files changed, 495 insertions, 15 deletions
diff --git a/accounts/gkleen@sif/default.nix b/accounts/gkleen@sif/default.nix
index d509eeef..f2978b6e 100644
--- a/accounts/gkleen@sif/default.nix
+++ b/accounts/gkleen@sif/default.nix
@@ -494,6 +494,8 @@ in {
494 name = "Paper-Mono-Dark"; 494 name = "Paper-Mono-Dark";
495 }; 495 };
496 }; 496 };
497 qt.enable = true;
498 qt.platformTheme = "gtk";
497 499
498 qt.kde.settings = { 500 qt.kde.settings = {
499 kwalletrc = { 501 kwalletrc = {
@@ -539,7 +541,7 @@ in {
539 sessionVariables = { 541 sessionVariables = {
540 # GDK_SCALE = 96.0 / 282.0; 542 # GDK_SCALE = 96.0 / 282.0;
541 # QT_AUTO_SCREEN_SCALE_FACTOR = 1; 543 # QT_AUTO_SCREEN_SCALE_FACTOR = 1;
542 QT_QPA_PLATFORMTHEME = "qt5ct"; 544 QT_QPA_PLATFORMTHEME = lib.mkForce "gtk3";
543 LIBVIRT_DEFAULT_URI = "qemu:///system"; 545 LIBVIRT_DEFAULT_URI = "qemu:///system";
544 STACK_XDG = 1; 546 STACK_XDG = 1;
545 EDITOR = lib.getExe' editor "emacsclient"; 547 EDITOR = lib.getExe' editor "emacsclient";
diff --git a/accounts/gkleen@sif/niri/waybar.nix b/accounts/gkleen@sif/niri/waybar.nix
index c02a9a76..04e255da 100644
--- a/accounts/gkleen@sif/niri/waybar.nix
+++ b/accounts/gkleen@sif/niri/waybar.nix
@@ -4,7 +4,7 @@ let
4in { 4in {
5 config = { 5 config = {
6 programs.waybar = { 6 programs.waybar = {
7 enable = true; 7 enable = false; # true;
8 systemd = { 8 systemd = {
9 enable = true; 9 enable = true;
10 target = "graphical-session.target"; 10 target = "graphical-session.target";
diff --git a/accounts/gkleen@sif/shell/default.nix b/accounts/gkleen@sif/shell/default.nix
index 405ae4b6..26c8bd98 100644
--- a/accounts/gkleen@sif/shell/default.nix
+++ b/accounts/gkleen@sif/shell/default.nix
@@ -8,6 +8,7 @@
8 src = ./quickshell; 8 src = ./quickshell;
9 replacements = { 9 replacements = {
10 coreutils = toString pkgs.coreutils; 10 coreutils = toString pkgs.coreutils;
11 ignore_workspaces = builtins.toJSON (map ({ name, ... }: name) config.programs.niri.scratchspaces);
11 }; 12 };
12 }; 13 };
13 }; 14 };
diff --git a/accounts/gkleen@sif/shell/quickshell/Bar.qml b/accounts/gkleen@sif/shell/quickshell/Bar.qml
index b7235a61..e19c0b32 100644
--- a/accounts/gkleen@sif/shell/quickshell/Bar.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml
@@ -1,14 +1,19 @@
1import Quickshell 1import Quickshell
2import Quickshell.Io 2import Quickshell.Io
3import Quickshell.Services.SystemTray
4import Quickshell.Widgets
3import Custom as Custom 5import Custom as Custom
4import QtQuick 6import QtQuick
7import qs.Services
5 8
6 9
7PanelWindow { 10PanelWindow {
11 id: bar
12
8 property var modelData 13 property var modelData
9 14
10 anchors { 15 anchors {
11 bottom: true 16 top: true
12 left: true 17 left: true
13 right: true 18 right: true
14 } 19 }
@@ -25,15 +30,79 @@ PanelWindow {
25 id: left 30 id: left
26 31
27 height: parent.height 32 height: parent.height
33 width: childrenRect.width
28 anchors.left: parent.left 34 anchors.left: parent.left
29 anchors.leftMargin: 8 35 anchors.leftMargin: 8
30 anchors.verticalCenter: parent.verticalCenter 36 anchors.verticalCenter: parent.verticalCenter
31 spacing: 5 37 spacing: 8
32 38
33 Text { 39 Row {
34 color: "white" 40 id: workspaces
41
42 property var ignoreWorkspaces: @ignore_workspaces@
43
44 height: parent.height
35 anchors.verticalCenter: parent.verticalCenter 45 anchors.verticalCenter: parent.verticalCenter
36 text: "left" 46 spacing: 0
47
48 Repeater {
49 model: {
50 let currWorkspaces = NiriService.workspaces;
51 const ignoreWorkspaces = Array.from(workspaces.ignoreWorkspaces);
52 currWorkspaces = currWorkspaces.filter(ws => ws.is_active || ignoreWorkspaces.every(iws => iws !== ws.name));
53 currWorkspaces.sort((a, b) => {
54 if (NiriService.outputs?.[a.output]?.logical?.x !== NiriService.outputs?.[b.output]?.logical?.x)
55 return NiriService.outputs?.[a.output]?.logical?.x - NiriService.outputs?.[b.output]?.logical?.x
56 if (NiriService.outputs?.[a.output]?.logical?.y !== NiriService.outputs?.[b.output]?.logical?.y)
57 return NiriService.outputs?.[a.output]?.logical?.y - NiriService.outputs?.[b.output]?.logical?.y
58 return a.idx - b.idx;
59 });
60 return currWorkspaces;
61 }
62
63 Rectangle {
64 property var workspaceData: modelData
65
66 width: wsLabel.contentWidth + 8
67 color: {
68 if (mouseArea.containsMouse) {
69 return "#33808080";
70 }
71 return "transparent";
72 }
73 height: parent.height
74 anchors.verticalCenter: parent.verticalCenter
75
76 MouseArea {
77 id: mouseArea
78
79 anchors.fill: parent
80 hoverEnabled: true
81 cursorShape: Qt.PointingHandCursor
82 enabled: true
83 onClicked: {
84 NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {})
85 }
86 }
87
88 Text {
89 id: wsLabel
90
91 font.pointSize: 10
92 font.family: "Fira Sans"
93 color: {
94 if (workspaceData.active_window_id === null)
95 return "#555";
96 if (workspaceData.is_active)
97 return "#23fd00";
98 return "white";
99 }
100 anchors.centerIn: parent
101
102 text: workspaceData.name ? workspaceData.name : workspaceData.idx
103 }
104 }
105 }
37 } 106 }
38 } 107 }
39 108
@@ -41,13 +110,92 @@ PanelWindow {
41 id: center 110 id: center
42 111
43 height: parent.height 112 height: parent.height
113 width: childrenRect.width
44 anchors.centerIn: parent 114 anchors.centerIn: parent
45 spacing: 5 115 spacing: 5
46 116
47 Text { 117 Item {
48 color: "white" 118 id: activeWindowDisplay
119
120 property var activeWindow: {
121 let currWindowId = Array.from(NiriService.workspaces).find(ws => {
122 return ws.output === bar.screen.name && ws.is_active;
123 })?.active_window_id;
124
125 return currWindowId ? Array.from(NiriService.windows).find(win => win.id == currWindowId) : null;
126 }
127 property var windowEntry: activeWindow ? DesktopEntries.heuristicLookup(activeWindow.app_id) : null
128
49 anchors.verticalCenter: parent.verticalCenter 129 anchors.verticalCenter: parent.verticalCenter
50 text: "center" 130 width: activeWindowDisplayContent.width
131 height: parent.height
132
133 Row {
134 id: activeWindowDisplayContent
135
136 width: childrenRect.width
137 height: parent.height
138 anchors.verticalCenter: parent.verticalCenter
139 spacing: 8
140
141 IconImage {
142 id: activeWindowIcon
143
144 height: 14
145 width: 14
146
147 anchors.verticalCenter: parent.verticalCenter
148
149 source: {
150 let icon = activeWindowDisplay.windowEntry?.icon
151 if (typeof icon === 'string' || icon instanceof String) {
152 if (icon.includes("?path=")) {
153 const split = icon.split("?path=")
154 if (split.length !== 2)
155 return icon
156 const name = split[0]
157 const path = split[1]
158 const fileName = name.substring(
159 name.lastIndexOf("/") + 1)
160 return `file://${path}/${fileName}`
161 } else
162 icon = Quickshell.iconPath(icon);
163 return icon
164 }
165 return ""
166 }
167 asynchronous: true
168 smooth: true
169 mipmap: true
170 }
171
172 Text {
173 id: windowTitle
174
175 width: Math.min(implicitWidth, bar.width - 2*Math.max(left.width, right.width) - 2*8 - activeWindowIcon.width - activeWindowDisplayContent.spacing)
176
177 property var appAliases: { "Firefox": "Mozilla Firefox", "mpv Media Player": "mpv", "Thunderbird": "Mozilla Thunderbird", "Thunderbird (LMU)": "Mozilla Thunderbird" }
178
179 elide: Text.ElideRight
180 maximumLineCount: 1
181 color: "white"
182 anchors.verticalCenter: parent.verticalCenter
183 text: {
184 if (!activeWindowDisplay.activeWindow)
185 return "";
186
187 var title = activeWindowDisplay.activeWindow.title;
188 var appName = activeWindowDisplay.windowEntry?.name;
189 if (appAliases[appName])
190 appName = appAliases[appName];
191 if (appName && title.endsWith(appName)) {
192 title = title.substring(0, title.length - appName.length);
193 title = title.replace(/\s*(—|-)\s*$/, "");
194 }
195 return title;
196 }
197 }
198 }
51 } 199 }
52 } 200 }
53 201
@@ -55,10 +203,147 @@ PanelWindow {
55 id: right 203 id: right
56 204
57 height: parent.height 205 height: parent.height
206 width: childrenRect.width
58 anchors.right: parent.right 207 anchors.right: parent.right
59 anchors.rightMargin: 8 208 anchors.rightMargin: 8
60 anchors.verticalCenter: parent.verticalCenter 209 anchors.verticalCenter: parent.verticalCenter
61 spacing: 5 210 spacing: 8
211
212 Rectangle {
213 id: kbdWidget
214
215 property var keyboardAbbrev: { "English (programmer Dvorak)": "dvp", "English (US)": "us" }
216
217 width: kbdLabel.contentWidth
218 color: {
219 /* if (kbdMouseArea.containsMouse) {
220 return "#33808080";
221 } */
222 return "transparent";
223 }
224 height: parent.height
225 anchors.verticalCenter: parent.verticalCenter
226
227 MouseArea {
228 id: kbdMouseArea
229
230 anchors.fill: parent
231 hoverEnabled: true
232 cursorShape: Qt.PointingHandCursor
233 enabled: true
234 onClicked: {
235 NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": "Next" } } }, _ => {})
236 }
237 }
238
239 Text {
240 id: kbdLabel
241
242 font.pointSize: 10
243 font.family: "Fira Sans"
244 color: {
245 if (NiriService.keyboardLayouts?.current_idx === 0)
246 return "#555";
247 return "white";
248 }
249 anchors.centerIn: parent
250
251 text: {
252 const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx];
253 return kbdWidget.keyboardAbbrev[currentLayout] ? kbdWidget.keyboardAbbrev[currentLayout] : currentLayout;
254 }
255 }
256 }
257
258 Item {
259 anchors.verticalCenter: parent.verticalCenter
260 width: systemTrayRow.childrenRect.width
261 height: parent.height
262 clip: true
263
264 Row {
265 id: systemTrayRow
266 anchors.centerIn: parent
267 width: childrenRect.width
268 height: parent.height
269 spacing: 0
270
271 Repeater {
272 model: SystemTray.items.values
273
274 delegate: Item {
275 property var trayItem: modelData
276 property string iconSource: {
277 let icon = trayItem && trayItem.icon
278 if (typeof icon === 'string' || icon instanceof String) {
279 if (icon.includes("?path=")) {
280 const split = icon.split("?path=")
281 if (split.length !== 2)
282 return icon
283 const name = split[0]
284 const path = split[1]
285 const fileName = name.substring(
286 name.lastIndexOf("/") + 1)
287 return `file://${path}/${fileName}`
288 }
289 return icon
290 }
291 return ""
292 }
293
294 width: 16
295 height: parent.height
296 anchors.verticalCenter: parent.verticalCenter
297
298 IconImage {
299 anchors.centerIn: parent
300 width: parent.width
301 height: parent.width
302 source: parent.iconSource
303 asynchronous: true
304 smooth: true
305 mipmap: true
306 }
307
308 MouseArea {
309 id: trayItemArea
310
311 anchors.fill: parent
312 acceptedButtons: Qt.LeftButton | Qt.RightButton
313 hoverEnabled: true
314 cursorShape: Qt.PointingHandCursor
315 onClicked: mouse => {
316 if (!trayItem)
317 return
318
319 if (mouse.button === Qt.LeftButton
320 && !trayItem.onlyMenu) {
321 trayItem.activate()
322 return
323 }
324
325 if (trayItem.hasMenu) {
326 var globalPos = mapToGlobal(0, 0)
327 var currentScreen = screen || Screen
328 var screenX = currentScreen.x || 0
329 var relativeX = globalPos.x - screenX
330 menuAnchor.menu = trayItem.menu
331 menuAnchor.anchor.window = bar
332 menuAnchor.anchor.rect = Qt.rect(
333 relativeX,
334 21,
335 parent.width, 1)
336 menuAnchor.open()
337 }
338 }
339 }
340 }
341 }
342 }
343 QsMenuAnchor {
344 id: menuAnchor
345 }
346 }
62 347
63 Text { 348 Text {
64 id: clock 349 id: clock
@@ -67,8 +352,8 @@ PanelWindow {
67 anchors.verticalCenter: parent.verticalCenter 352 anchors.verticalCenter: parent.verticalCenter
68 353
69 Custom.Chrono { 354 Custom.Chrono {
70 id: chrono 355 id: chrono
71 format: "W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}" 356 format: "W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}"
72 } 357 }
73 358
74 text: chrono.date 359 text: chrono.date
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..914152e1
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml
@@ -0,0 +1,190 @@
1pragma Singleton
2
3import Quickshell
4import Quickshell.Io
5import QtQuick
6
7Singleton {
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 onKeyboardLayoutsChanged: {
17 console.log(JSON.stringify(keyboardLayouts));
18 }
19
20 function refreshOutputs() {
21 commandSocket.sendCommand("Outputs", data => {
22 outputs = data.Ok.Outputs;
23 });
24 }
25
26 function sendCommand(command, callback) {
27 commandSocket.sendCommand(command, callback);
28 }
29
30 Socket {
31 id: eventStreamSocket
32 path: root.socketPath
33 connected: true
34
35 onConnectionStateChanged: {
36 if (connected) {
37 write('"EventStream"\n')
38 }
39 }
40
41 parser: SplitParser {
42 onRead: line => {
43 try {
44 const event = JSON.parse(line)
45
46 // console.log(JSON.stringify(event))
47
48 if (event.WorkspacesChanged) {
49 root.workspaces = event.WorkspacesChanged.workspaces
50 root.refreshOutputs();
51 } else if (event.WorkspaceActivated)
52 eventWorkspaceActivated(event.WorkspaceActivated);
53 else if (event.WorkspaceUrgencyChanged)
54 eventWorkspaceUrgencyChanged(event.WorkspaceUrgencyChanged);
55 else if (event.WorkspaceActiveWindowChanged)
56 eventWorkspaceActiveWindowChanged(event.WorkspaceActiveWindowChanged);
57 else if (event.KeyboardLayoutsChanged)
58 root.keyboardLayouts = event.KeyboardLayoutsChanged.keyboard_layouts;
59 else if (event.KeyboardLayoutSwitched)
60 root.keyboardLayouts = Object.assign({}, root.keyboardLayouts, {"current_idx": event.KeyboardLayoutSwitched.idx });
61 else if (event.WindowsChanged)
62 root.windows = event.WindowsChanged.windows
63 else if (event.WindowOpenedOrChanged)
64 eventWindowOpenedOrChanged(event.WindowOpenedOrChanged);
65 else if (event.WindowClosed)
66 eventWindowClosed(event.WindowClosed);
67 else if (event.WindowFocusChanged)
68 eventWindowFocusChanged(event.WindowFocusChanged);
69 else if (event.WindowUrgencyChanged)
70 eventWindowUrgencyChanged(event.WindowUrgencyChanged);
71 else if (event.WindowLayoutsChanged)
72 eventWindowLayoutsChanged(event.WindowLayoutsChanged);
73 } catch (e) {
74 console.warn("NiriService: Failed to parse event:", line, e)
75 }
76 }
77 }
78 }
79
80 Socket {
81 id: commandSocket
82 path: root.socketPath
83 connected: true
84
85 property var awaitingAnswer: null
86 property var cmdQueue: []
87
88 parser: SplitParser {
89 onRead: line => {
90 if (commandSocket.awaitingAnswer === null)
91 return;
92
93 try {
94 const response = JSON.parse(line);
95 commandSocket.awaitingAnswer.callback(response);
96 commandSocket.awaitingAnswer = null;
97 } catch (e) {
98 console.warn("NiriService: Failed to parse response:", line, e)
99 }
100 commandSocket._handleQueue();
101 }
102 }
103
104 onCmdQueueChanged: {
105 _handleQueue();
106 }
107 onAwaitingAnswerChanged: {
108 _handleQueue();
109 }
110
111 function _handleQueue() {
112 if (cmdQueue.length <= 0 || awaitingAnswer !== null)
113 return;
114
115 let localQueue = Array.from(cmdQueue);
116 awaitingAnswer = localQueue.shift();
117 cmdQueue = localQueue;
118 write(JSON.stringify(awaitingAnswer.command) + '\n');
119 }
120
121 function sendCommand(command, callback) {
122 cmdQueue = Array.from(cmdQueue).concat([{ "command": command, "callback": callback }])
123 }
124 }
125
126 function eventWorkspaceActivated(data) {
127 let relevant_output = null;
128 Array.from(root.workspaces).forEach(ws => {
129 if (data.id === ws.id)
130 relevant_output = ws.output;
131 });
132 root.workspaces = Array.from(root.workspaces).map(ws => {
133 if (ws.output === relevant_output) {
134 ws.is_active = false;
135 ws.is_focused = false;
136 }
137 if (data.id === ws.id) {
138 ws.is_active = true;
139 ws.is_focused = data.focused;
140 }
141 return ws;
142 });
143 }
144 function eventWorkspaceUrgencyChanged(data) {
145 root.workspaces = Array.from(root.workspaces).map(ws => {
146 if (data.id == ws.id)
147 ws.is_urgent = data.urgent;
148 return ws;
149 });
150 }
151 function eventWorkspaceActiveWindowChanged(data) {
152 root.workspaces = Array.from(root.workspaces).map(ws => {
153 if (data.workspace_id === ws.id)
154 ws.active_window_id = data.active_window_id;
155 return ws;
156 });
157 }
158 function eventWindowOpenedOrChanged(data) {
159 root.windows = Array.from(root.windows).map(win => {
160 if (win.id === data.window.id)
161 return data.window;
162 return win;
163 });
164 }
165 function eventWindowClosed(data) {
166 root.windows = Array.from(root.windows).filter(win => win.id !== data.id);
167 }
168 function eventWindowFocusChanged(data) {
169 root.windows = Array.from(root.windows).map(win => {
170 win.is_focused = win.id === data.id;
171 return win;
172 });
173 }
174 function eventWindowUrgencyChanged(data) {
175 root.windows = Array.from(root.windows).map(win => {
176 if (win.id === data.id)
177 win.is_urgent = data.urgent;
178 return win;
179 });
180 }
181 function eventWindowLayoutsChanged(data) {
182 root.windows = Array.from(root.windows).map(win => {
183 Array.from(data.changes).forEach(change => {
184 if (win.id === change[0])
185 win.layout = change[1];
186 });
187 return win;
188 });
189 }
190}
diff --git a/accounts/gkleen@sif/shell/quickshell/shell.qml b/accounts/gkleen@sif/shell/quickshell/shell.qml
index 35fe5344..2abd1fef 100644
--- a/accounts/gkleen@sif/shell/quickshell/shell.qml
+++ b/accounts/gkleen@sif/shell/quickshell/shell.qml
@@ -1,3 +1,5 @@
1//@ pragma UseQApplication
2
1import Quickshell 3import Quickshell
2 4
3ShellRoot { 5ShellRoot {
diff --git a/accounts/gkleen@sif/systemd.nix b/accounts/gkleen@sif/systemd.nix
index 18c2315f..1539126c 100644
--- a/accounts/gkleen@sif/systemd.nix
+++ b/accounts/gkleen@sif/systemd.nix
@@ -441,8 +441,8 @@ in {
441 tray = { 441 tray = {
442 Unit = { 442 Unit = {
443 PartOf = [ "graphical-session.target" ]; 443 PartOf = [ "graphical-session.target" ];
444 Requires = [ "waybar.service" ]; 444 # Requires = [ "waybar.service" ];
445 After = [ "graphical-session.target" "waybar.service" ]; 445 After = [ "graphical-session.target" ]; # "waybar.service" ];
446 Wants = ["blueman-applet.service" "udiskie.service" "network-manager-applet.service"]; 446 Wants = ["blueman-applet.service" "udiskie.service" "network-manager-applet.service"];
447 }; 447 };
448 }; 448 };