diff options
author | Gregor Kleen <gkleen@yggdrasil.li> | 2025-08-31 12:12:23 +0200 |
---|---|---|
committer | Gregor Kleen <gkleen@yggdrasil.li> | 2025-08-31 12:12:23 +0200 |
commit | 9fc966ff7726d01267a6220483fb005c0efaa9c0 (patch) | |
tree | 9e66f32774a55a7f51314883648bd40751250091 | |
parent | 97423da1a1e58a2820d12bed5b36f0896fd3cccc (diff) | |
download | nixos-9fc966ff7726d01267a6220483fb005c0efaa9c0.tar nixos-9fc966ff7726d01267a6220483fb005c0efaa9c0.tar.gz nixos-9fc966ff7726d01267a6220483fb005c0efaa9c0.tar.bz2 nixos-9fc966ff7726d01267a6220483fb005c0efaa9c0.tar.xz nixos-9fc966ff7726d01267a6220483fb005c0efaa9c0.zip |
...
-rw-r--r-- | accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml | 93 | ||||
-rw-r--r-- | accounts/gkleen@sif/shell/quickshell/Bar.qml | 507 | ||||
-rw-r--r-- | accounts/gkleen@sif/shell/quickshell/Clock.qml | 186 | ||||
-rw-r--r-- | accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml | 78 | ||||
-rw-r--r-- | accounts/gkleen@sif/shell/quickshell/SystemTray.qml | 94 | ||||
-rw-r--r-- | accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml | 71 |
6 files changed, 528 insertions, 501 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..d7e8e7c5 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml | |||
@@ -0,0 +1,93 @@ | |||
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 | |||
11 | property var activeWindow: { | ||
12 | let currWindowId = Array.from(NiriService.workspaces).find(ws => { | ||
13 | return ws.output === bar.screen.name && ws.is_active; | ||
14 | })?.active_window_id; | ||
15 | |||
16 | return currWindowId ? Array.from(NiriService.windows).find(win => win.id == currWindowId) : null; | ||
17 | } | ||
18 | property var windowEntry: activeWindow ? DesktopEntries.heuristicLookup(activeWindow.app_id) : null | ||
19 | |||
20 | anchors.verticalCenter: parent.verticalCenter | ||
21 | width: activeWindowDisplayContent.width | ||
22 | height: parent.height | ||
23 | |||
24 | Row { | ||
25 | id: activeWindowDisplayContent | ||
26 | |||
27 | width: childrenRect.width | ||
28 | height: parent.height | ||
29 | anchors.verticalCenter: parent.verticalCenter | ||
30 | spacing: 8 | ||
31 | |||
32 | IconImage { | ||
33 | id: activeWindowIcon | ||
34 | |||
35 | height: 14 | ||
36 | width: 14 | ||
37 | |||
38 | anchors.verticalCenter: parent.verticalCenter | ||
39 | |||
40 | source: { | ||
41 | let icon = activeWindowDisplay.windowEntry?.icon | ||
42 | if (typeof icon === 'string' || icon instanceof String) { | ||
43 | if (icon.includes("?path=")) { | ||
44 | const split = icon.split("?path=") | ||
45 | if (split.length !== 2) | ||
46 | return icon | ||
47 | const name = split[0] | ||
48 | const path = split[1] | ||
49 | const fileName = name.substring( | ||
50 | name.lastIndexOf("/") + 1) | ||
51 | return `file://${path}/${fileName}` | ||
52 | } else | ||
53 | icon = Quickshell.iconPath(icon); | ||
54 | return icon | ||
55 | } | ||
56 | return "" | ||
57 | } | ||
58 | asynchronous: true | ||
59 | smooth: true | ||
60 | mipmap: true | ||
61 | } | ||
62 | |||
63 | Text { | ||
64 | id: windowTitle | ||
65 | |||
66 | width: Math.min(implicitWidth, activeWindowDisplay.maxWidth - activeWindowIcon.width - activeWindowDisplayContent.spacing) | ||
67 | |||
68 | property var appAliases: { "Firefox": "Mozilla Firefox", "mpv Media Player": "mpv", "Thunderbird": "Mozilla Thunderbird", "Thunderbird (LMU)": "Mozilla Thunderbird" } | ||
69 | |||
70 | elide: Text.ElideRight | ||
71 | maximumLineCount: 1 | ||
72 | color: "white" | ||
73 | anchors.verticalCenter: parent.verticalCenter | ||
74 | text: { | ||
75 | if (!activeWindowDisplay.activeWindow) | ||
76 | return ""; | ||
77 | |||
78 | var title = activeWindowDisplay.activeWindow.title; | ||
79 | var appName = activeWindowDisplay.windowEntry?.name; | ||
80 | if (appAliases[appName]) | ||
81 | appName = appAliases[appName]; | ||
82 | if (appName && title.endsWith(appName)) { | ||
83 | const oldTitle = title; | ||
84 | title = title.substring(0, title.length - appName.length); | ||
85 | title = title.replace(/\s*(—|-)\s*$/, ""); | ||
86 | if (!title) | ||
87 | title = oldTitle; | ||
88 | } | ||
89 | return title; | ||
90 | } | ||
91 | } | ||
92 | } | ||
93 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/Bar.qml b/accounts/gkleen@sif/shell/quickshell/Bar.qml index 9cb58b93..2f323858 100644 --- a/accounts/gkleen@sif/shell/quickshell/Bar.qml +++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml | |||
@@ -1,13 +1,5 @@ | |||
1 | import Quickshell | 1 | import Quickshell |
2 | import Quickshell.Io | ||
3 | import Quickshell.Services.SystemTray | ||
4 | import Quickshell.Widgets | ||
5 | import Custom as Custom | ||
6 | import QtQuick | 2 | import QtQuick |
7 | import qs.Services | ||
8 | import QtQuick.Controls | ||
9 | import QtQuick.Layouts | ||
10 | import QtQml | ||
11 | 3 | ||
12 | 4 | ||
13 | PanelWindow { | 5 | PanelWindow { |
@@ -46,74 +38,7 @@ PanelWindow { | |||
46 | anchors.verticalCenter: parent.verticalCenter | 38 | anchors.verticalCenter: parent.verticalCenter |
47 | spacing: 8 | 39 | spacing: 8 |
48 | 40 | ||
49 | Row { | 41 | WorkspaceSwitcher {} |
50 | id: workspaces | ||
51 | |||
52 | property var ignoreWorkspaces: @ignore_workspaces@ | ||
53 | |||
54 | height: parent.height | ||
55 | anchors.verticalCenter: parent.verticalCenter | ||
56 | spacing: 0 | ||
57 | |||
58 | Repeater { | ||
59 | model: { | ||
60 | let currWorkspaces = NiriService.workspaces; | ||
61 | const ignoreWorkspaces = Array.from(workspaces.ignoreWorkspaces); | ||
62 | currWorkspaces = currWorkspaces.filter(ws => ws.is_active || ignoreWorkspaces.every(iws => iws !== ws.name)); | ||
63 | currWorkspaces.sort((a, b) => { | ||
64 | if (NiriService.outputs?.[a.output]?.logical?.x !== NiriService.outputs?.[b.output]?.logical?.x) | ||
65 | return NiriService.outputs?.[a.output]?.logical?.x - NiriService.outputs?.[b.output]?.logical?.x | ||
66 | if (NiriService.outputs?.[a.output]?.logical?.y !== NiriService.outputs?.[b.output]?.logical?.y) | ||
67 | return NiriService.outputs?.[a.output]?.logical?.y - NiriService.outputs?.[b.output]?.logical?.y | ||
68 | return a.idx - b.idx; | ||
69 | }); | ||
70 | return currWorkspaces; | ||
71 | } | ||
72 | |||
73 | Rectangle { | ||
74 | property var workspaceData: modelData | ||
75 | |||
76 | width: wsLabel.contentWidth + 8 | ||
77 | color: { | ||
78 | if (mouseArea.containsMouse) { | ||
79 | return "#33808080"; | ||
80 | } | ||
81 | return "transparent"; | ||
82 | } | ||
83 | height: parent.height | ||
84 | anchors.verticalCenter: parent.verticalCenter | ||
85 | |||
86 | MouseArea { | ||
87 | id: mouseArea | ||
88 | |||
89 | anchors.fill: parent | ||
90 | hoverEnabled: true | ||
91 | cursorShape: Qt.PointingHandCursor | ||
92 | enabled: true | ||
93 | onClicked: { | ||
94 | NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {}) | ||
95 | } | ||
96 | } | ||
97 | |||
98 | Text { | ||
99 | id: wsLabel | ||
100 | |||
101 | font.pointSize: 10 | ||
102 | font.family: "Fira Sans" | ||
103 | color: { | ||
104 | if (workspaceData.is_active) | ||
105 | return "#23fd00"; | ||
106 | if (workspaceData.active_window_id === null) | ||
107 | return "#555"; | ||
108 | return "white"; | ||
109 | } | ||
110 | anchors.centerIn: parent | ||
111 | |||
112 | text: workspaceData.name ? workspaceData.name : workspaceData.idx | ||
113 | } | ||
114 | } | ||
115 | } | ||
116 | } | ||
117 | } | 42 | } |
118 | 43 | ||
119 | Row { | 44 | Row { |
@@ -124,91 +49,8 @@ PanelWindow { | |||
124 | anchors.centerIn: parent | 49 | anchors.centerIn: parent |
125 | spacing: 5 | 50 | spacing: 5 |
126 | 51 | ||
127 | Item { | 52 | ActiveWindowDisplay { |
128 | id: activeWindowDisplay | 53 | maxWidth: bar.width - 2*Math.max(left.width, right.width) - 2*8 |
129 | |||
130 | property var activeWindow: { | ||
131 | let currWindowId = Array.from(NiriService.workspaces).find(ws => { | ||
132 | return ws.output === bar.screen.name && ws.is_active; | ||
133 | })?.active_window_id; | ||
134 | |||
135 | return currWindowId ? Array.from(NiriService.windows).find(win => win.id == currWindowId) : null; | ||
136 | } | ||
137 | property var windowEntry: activeWindow ? DesktopEntries.heuristicLookup(activeWindow.app_id) : null | ||
138 | |||
139 | anchors.verticalCenter: parent.verticalCenter | ||
140 | width: activeWindowDisplayContent.width | ||
141 | height: parent.height | ||
142 | |||
143 | Row { | ||
144 | id: activeWindowDisplayContent | ||
145 | |||
146 | width: childrenRect.width | ||
147 | height: parent.height | ||
148 | anchors.verticalCenter: parent.verticalCenter | ||
149 | spacing: 8 | ||
150 | |||
151 | IconImage { | ||
152 | id: activeWindowIcon | ||
153 | |||
154 | height: 14 | ||
155 | width: 14 | ||
156 | |||
157 | anchors.verticalCenter: parent.verticalCenter | ||
158 | |||
159 | source: { | ||
160 | let icon = activeWindowDisplay.windowEntry?.icon | ||
161 | if (typeof icon === 'string' || icon instanceof String) { | ||
162 | if (icon.includes("?path=")) { | ||
163 | const split = icon.split("?path=") | ||
164 | if (split.length !== 2) | ||
165 | return icon | ||
166 | const name = split[0] | ||
167 | const path = split[1] | ||
168 | const fileName = name.substring( | ||
169 | name.lastIndexOf("/") + 1) | ||
170 | return `file://${path}/${fileName}` | ||
171 | } else | ||
172 | icon = Quickshell.iconPath(icon); | ||
173 | return icon | ||
174 | } | ||
175 | return "" | ||
176 | } | ||
177 | asynchronous: true | ||
178 | smooth: true | ||
179 | mipmap: true | ||
180 | } | ||
181 | |||
182 | Text { | ||
183 | id: windowTitle | ||
184 | |||
185 | width: Math.min(implicitWidth, bar.width - 2*Math.max(left.width, right.width) - 2*8 - activeWindowIcon.width - activeWindowDisplayContent.spacing) | ||
186 | |||
187 | property var appAliases: { "Firefox": "Mozilla Firefox", "mpv Media Player": "mpv", "Thunderbird": "Mozilla Thunderbird", "Thunderbird (LMU)": "Mozilla Thunderbird" } | ||
188 | |||
189 | elide: Text.ElideRight | ||
190 | maximumLineCount: 1 | ||
191 | color: "white" | ||
192 | anchors.verticalCenter: parent.verticalCenter | ||
193 | text: { | ||
194 | if (!activeWindowDisplay.activeWindow) | ||
195 | return ""; | ||
196 | |||
197 | var title = activeWindowDisplay.activeWindow.title; | ||
198 | var appName = activeWindowDisplay.windowEntry?.name; | ||
199 | if (appAliases[appName]) | ||
200 | appName = appAliases[appName]; | ||
201 | if (appName && title.endsWith(appName)) { | ||
202 | const oldTitle = title; | ||
203 | title = title.substring(0, title.length - appName.length); | ||
204 | title = title.replace(/\s*(—|-)\s*$/, ""); | ||
205 | if (!title) | ||
206 | title = oldTitle; | ||
207 | } | ||
208 | return title; | ||
209 | } | ||
210 | } | ||
211 | } | ||
212 | } | 54 | } |
213 | } | 55 | } |
214 | 56 | ||
@@ -222,357 +64,20 @@ PanelWindow { | |||
222 | anchors.verticalCenter: parent.verticalCenter | 64 | anchors.verticalCenter: parent.verticalCenter |
223 | spacing: 0 | 65 | spacing: 0 |
224 | 66 | ||
225 | Item { | 67 | SystemTray {} |
226 | anchors.verticalCenter: parent.verticalCenter | ||
227 | width: systemTrayRow.childrenRect.width | ||
228 | height: parent.height | ||
229 | clip: true | ||
230 | |||
231 | Row { | ||
232 | id: systemTrayRow | ||
233 | anchors.centerIn: parent | ||
234 | width: childrenRect.width | ||
235 | height: parent.height | ||
236 | spacing: 0 | ||
237 | |||
238 | Repeater { | ||
239 | model: SystemTray.items.values | ||
240 | |||
241 | delegate: Item { | ||
242 | property var trayItem: modelData | ||
243 | property string iconSource: { | ||
244 | let icon = trayItem && trayItem.icon | ||
245 | if (typeof icon === 'string' || icon instanceof String) { | ||
246 | if (icon.includes("?path=")) { | ||
247 | const split = icon.split("?path=") | ||
248 | if (split.length !== 2) | ||
249 | return icon | ||
250 | const name = split[0] | ||
251 | const path = split[1] | ||
252 | const fileName = name.substring( | ||
253 | name.lastIndexOf("/") + 1) | ||
254 | return `file://${path}/${fileName}` | ||
255 | } | ||
256 | return icon | ||
257 | } | ||
258 | return "" | ||
259 | } | ||
260 | |||
261 | width: 16 | ||
262 | height: parent.height | ||
263 | anchors.verticalCenter: parent.verticalCenter | ||
264 | |||
265 | IconImage { | ||
266 | anchors.centerIn: parent | ||
267 | width: parent.width | ||
268 | height: parent.width | ||
269 | source: parent.iconSource | ||
270 | asynchronous: true | ||
271 | smooth: true | ||
272 | mipmap: true | ||
273 | } | ||
274 | |||
275 | MouseArea { | ||
276 | id: trayItemArea | ||
277 | |||
278 | anchors.fill: parent | ||
279 | acceptedButtons: Qt.LeftButton | Qt.RightButton | ||
280 | hoverEnabled: true | ||
281 | cursorShape: Qt.PointingHandCursor | ||
282 | onClicked: mouse => { | ||
283 | if (!trayItem) | ||
284 | return | ||
285 | |||
286 | if (mouse.button === Qt.LeftButton | ||
287 | && !trayItem.onlyMenu) { | ||
288 | trayItem.activate() | ||
289 | return | ||
290 | } | ||
291 | |||
292 | if (trayItem.hasMenu) { | ||
293 | var globalPos = mapToGlobal(0, 0) | ||
294 | var currentScreen = screen || Screen | ||
295 | var screenX = currentScreen.x || 0 | ||
296 | var relativeX = globalPos.x - screenX | ||
297 | menuAnchor.menu = trayItem.menu | ||
298 | menuAnchor.anchor.window = bar | ||
299 | menuAnchor.anchor.rect = Qt.rect( | ||
300 | relativeX, | ||
301 | 21, | ||
302 | parent.width, 1) | ||
303 | menuAnchor.open() | ||
304 | } | ||
305 | } | ||
306 | } | ||
307 | } | ||
308 | } | ||
309 | } | ||
310 | QsMenuAnchor { | ||
311 | id: menuAnchor | ||
312 | } | ||
313 | } | ||
314 | 68 | ||
315 | Item { | 69 | Item { |
316 | height: parent.height | 70 | height: parent.height |
317 | width: 4 | 71 | width: 4 |
318 | } | 72 | } |
319 | 73 | ||
320 | Rectangle { | 74 | KeyboardLayout {} |
321 | id: kbdWidget | ||
322 | |||
323 | property var keyboardAbbrev: { "English (programmer Dvorak)": "dvp", "English (US)": "us" } | ||
324 | |||
325 | width: kbdLabel.contentWidth + 8 | ||
326 | color: { | ||
327 | if (kbdMouseArea.containsMouse) { | ||
328 | return "#33808080"; | ||
329 | } | ||
330 | return "transparent"; | ||
331 | } | ||
332 | height: parent.height | ||
333 | anchors.verticalCenter: parent.verticalCenter | ||
334 | |||
335 | MouseArea { | ||
336 | id: kbdMouseArea | ||
337 | |||
338 | anchors.fill: parent | ||
339 | hoverEnabled: true | ||
340 | cursorShape: Qt.PointingHandCursor | ||
341 | enabled: true | ||
342 | onClicked: { | ||
343 | NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": "Next" } } }, _ => {}) | ||
344 | } | ||
345 | } | ||
346 | |||
347 | Text { | ||
348 | id: kbdLabel | ||
349 | |||
350 | font.pointSize: 10 | ||
351 | font.family: "Fira Sans" | ||
352 | color: { | ||
353 | if (NiriService.keyboardLayouts?.current_idx === 0) | ||
354 | return "#555"; | ||
355 | return "white"; | ||
356 | } | ||
357 | anchors.centerIn: parent | ||
358 | |||
359 | text: { | ||
360 | const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx]; | ||
361 | if (!currentLayout) | ||
362 | return ""; | ||
363 | return kbdWidget.keyboardAbbrev[currentLayout] ? kbdWidget.keyboardAbbrev[currentLayout] : currentLayout; | ||
364 | } | ||
365 | } | ||
366 | |||
367 | PopupWindow { | ||
368 | anchor { | ||
369 | item: kbdMouseArea | ||
370 | edges: Edges.Bottom | ||
371 | } | ||
372 | visible: kbdMouseArea.containsMouse | ||
373 | |||
374 | implicitWidth: kbdTooltipText.contentWidth + 4 | ||
375 | implicitHeight: kbdTooltipText.contentHeight + 4 | ||
376 | color: "black" | ||
377 | |||
378 | Text { | ||
379 | id: kbdTooltipText | ||
380 | |||
381 | anchors.centerIn: parent | ||
382 | |||
383 | font.pointSize: 10 | ||
384 | font.family: "Fira Sans" | ||
385 | color: "white" | ||
386 | |||
387 | text: { | ||
388 | const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx]; | ||
389 | return currentLayout || ""; | ||
390 | } | ||
391 | } | ||
392 | } | ||
393 | } | ||
394 | 75 | ||
395 | Item { | 76 | Item { |
396 | height: parent.height | 77 | height: parent.height |
397 | width: 4 | 78 | width: 4 |
398 | } | 79 | } |
399 | 80 | ||
400 | Item { | 81 | Clock {} |
401 | width: clock.contentWidth | ||
402 | height: parent.height | ||
403 | anchors.verticalCenter: parent.verticalCenter | ||
404 | |||
405 | MouseArea { | ||
406 | id: clockMouseArea | ||
407 | |||
408 | anchors.fill: parent | ||
409 | hoverEnabled: true | ||
410 | enabled: true | ||
411 | } | ||
412 | |||
413 | Text { | ||
414 | id: clock | ||
415 | color: "white" | ||
416 | |||
417 | anchors.verticalCenter: parent.verticalCenter | ||
418 | |||
419 | Custom.Chrono { | ||
420 | id: chrono | ||
421 | format: "W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}" | ||
422 | } | ||
423 | |||
424 | text: chrono.date | ||
425 | |||
426 | font.pointSize: 10 | ||
427 | font.family: "Fira Sans" | ||
428 | font.features: { "tnum": 1 } | ||
429 | } | ||
430 | |||
431 | PopupWindow { | ||
432 | anchor { | ||
433 | item: clockMouseArea | ||
434 | edges: Edges.Bottom | ||
435 | } | ||
436 | visible: clockMouseArea.containsMouse | ||
437 | |||
438 | implicitWidth: yearCalendar.implicitWidth + 16 | ||
439 | implicitHeight: yearCalendar.implicitHeight + 16 | ||
440 | color: "black" | ||
441 | |||
442 | GridLayout { | ||
443 | property int year: { const d = new Date(); return d.getFullYear(); } | ||
444 | |||
445 | id: yearCalendar | ||
446 | |||
447 | columns: 3 | ||
448 | columnSpacing: 16 | ||
449 | rowSpacing: 16 | ||
450 | |||
451 | anchors.centerIn: parent | ||
452 | |||
453 | Repeater { | ||
454 | model: 12 | ||
455 | |||
456 | Column { | ||
457 | required property int index | ||
458 | property int month: index | ||
459 | |||
460 | id: monthCalendar | ||
461 | |||
462 | width: parent.width | ||
463 | |||
464 | Text { | ||
465 | width: parent.width | ||
466 | |||
467 | horizontalAlignment: Text.AlignHCenter | ||
468 | |||
469 | font.pointSize: 10 | ||
470 | font.family: "Fira Sans" | ||
471 | |||
472 | text: { | ||
473 | const date = Date.fromLocaleDateString(Qt.locale(), `${yearCalendar.year}-${monthCalendar.month + 1}-01`, "yyyy-M-dd"); | ||
474 | return date.toLocaleString(Qt.locale("en_DK"), "MMMM") | ||
475 | } | ||
476 | |||
477 | color: "#ffead3" | ||
478 | } | ||
479 | |||
480 | GridLayout { | ||
481 | columns: 2 | ||
482 | |||
483 | DayOfWeekRow { | ||
484 | locale: grid.locale | ||
485 | |||
486 | Layout.column: 1 | ||
487 | Layout.fillWidth: true | ||
488 | |||
489 | delegate: Text { | ||
490 | required property string shortName | ||
491 | |||
492 | font.pointSize: 10 | ||
493 | font.family: "Fira Mono" | ||
494 | |||
495 | text: shortName | ||
496 | color: "#ffcc66" | ||
497 | |||
498 | horizontalAlignment: Text.AlignRight | ||
499 | verticalAlignment: Text.AlignVCenter | ||
500 | } | ||
501 | } | ||
502 | |||
503 | WeekNumberColumn { | ||
504 | month: grid.month | ||
505 | year: grid.year | ||
506 | locale: grid.locale | ||
507 | |||
508 | Layout.fillHeight: true | ||
509 | |||
510 | delegate: Text { | ||
511 | required property int weekNumber | ||
512 | |||
513 | opacity: { | ||
514 | const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7); | ||
515 | const dayOfWeek = simple.getDay(); | ||
516 | const isoWeekStart = simple; | ||
517 | |||
518 | isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1); | ||
519 | if (dayOfWeek > 4) { | ||
520 | isoWeekStart.setDate(isoWeekStart.getDate() + 7); | ||
521 | } | ||
522 | |||
523 | for (let i = 0; i < 7; i++) { | ||
524 | const dayInWeek = isoWeekStart; | ||
525 | dayInWeek.setDate(dayInWeek.getDate() + i); | ||
526 | if (dayInWeek.getMonth() == monthCalendar.month) | ||
527 | return 1; | ||
528 | } | ||
529 | |||
530 | return 0; | ||
531 | } | ||
532 | |||
533 | font.pointSize: 10 | ||
534 | font.family: "Fira Sans" | ||
535 | font.features: { "tnum": 1 } | ||
536 | |||
537 | text: weekNumber | ||
538 | color: "#99ffdd" | ||
539 | |||
540 | horizontalAlignment: Text.AlignRight | ||
541 | verticalAlignment: Text.AlignVCenter | ||
542 | } | ||
543 | } | ||
544 | |||
545 | MonthGrid { | ||
546 | id: grid | ||
547 | |||
548 | year: yearCalendar.year | ||
549 | month: monthCalendar.month | ||
550 | locale: Qt.locale("en_DK") | ||
551 | |||
552 | Layout.fillWidth: true | ||
553 | Layout.fillHeight: true | ||
554 | |||
555 | delegate: Text { | ||
556 | required property var model | ||
557 | |||
558 | opacity: model.month === monthCalendar.month ? 1 : 0 | ||
559 | |||
560 | font.pointSize: 10 | ||
561 | font.family: "Fira Sans" | ||
562 | font.features: { "tnum": 1 } | ||
563 | |||
564 | text: model.day | ||
565 | color: model.today ? "#ff6699" : "white" | ||
566 | |||
567 | horizontalAlignment: Text.AlignRight | ||
568 | verticalAlignment: Text.AlignVCenter | ||
569 | } | ||
570 | } | ||
571 | } | ||
572 | } | ||
573 | } | ||
574 | } | ||
575 | } | ||
576 | } | ||
577 | } | 82 | } |
578 | } \ No newline at end of file | 83 | } \ No newline at end of file |
diff --git a/accounts/gkleen@sif/shell/quickshell/Clock.qml b/accounts/gkleen@sif/shell/quickshell/Clock.qml new file mode 100644 index 00000000..56f8d41f --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/Clock.qml | |||
@@ -0,0 +1,186 @@ | |||
1 | import QtQml | ||
2 | import QtQuick | ||
3 | import Quickshell | ||
4 | import Custom as Custom | ||
5 | import QtQuick.Controls | ||
6 | import QtQuick.Layouts | ||
7 | |||
8 | Item { | ||
9 | width: clock.contentWidth | ||
10 | height: parent.height | ||
11 | anchors.verticalCenter: parent.verticalCenter | ||
12 | |||
13 | MouseArea { | ||
14 | id: clockMouseArea | ||
15 | |||
16 | anchors.fill: parent | ||
17 | hoverEnabled: true | ||
18 | enabled: true | ||
19 | } | ||
20 | |||
21 | Text { | ||
22 | id: clock | ||
23 | color: "white" | ||
24 | |||
25 | anchors.verticalCenter: parent.verticalCenter | ||
26 | |||
27 | Custom.Chrono { | ||
28 | id: chrono | ||
29 | format: "W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}" | ||
30 | } | ||
31 | |||
32 | text: chrono.date | ||
33 | |||
34 | font.pointSize: 10 | ||
35 | font.family: "Fira Sans" | ||
36 | font.features: { "tnum": 1 } | ||
37 | } | ||
38 | |||
39 | PopupWindow { | ||
40 | anchor { | ||
41 | item: clockMouseArea | ||
42 | edges: Edges.Bottom | ||
43 | } | ||
44 | visible: clockMouseArea.containsMouse | ||
45 | |||
46 | implicitWidth: yearCalendar.implicitWidth + 16 | ||
47 | implicitHeight: yearCalendar.implicitHeight + 16 | ||
48 | color: "black" | ||
49 | |||
50 | GridLayout { | ||
51 | property int year: { const d = new Date(); return d.getFullYear(); } | ||
52 | |||
53 | id: yearCalendar | ||
54 | |||
55 | columns: 3 | ||
56 | columnSpacing: 16 | ||
57 | rowSpacing: 16 | ||
58 | |||
59 | anchors.centerIn: parent | ||
60 | |||
61 | Repeater { | ||
62 | model: 12 | ||
63 | |||
64 | Column { | ||
65 | required property int index | ||
66 | property int month: index | ||
67 | |||
68 | id: monthCalendar | ||
69 | |||
70 | width: parent.width | ||
71 | |||
72 | Layout.alignment: Qt.AlignTop | Qt.AlignRight | ||
73 | |||
74 | Text { | ||
75 | width: parent.width | ||
76 | |||
77 | horizontalAlignment: Text.AlignHCenter | ||
78 | |||
79 | font.pointSize: 10 | ||
80 | font.family: "Fira Sans" | ||
81 | |||
82 | text: { | ||
83 | const date = Date.fromLocaleDateString(Qt.locale(), `${yearCalendar.year}-${monthCalendar.month + 1}-01`, "yyyy-M-dd"); | ||
84 | return date.toLocaleString(Qt.locale("en_DK"), "MMMM") | ||
85 | } | ||
86 | |||
87 | color: "#ffead3" | ||
88 | } | ||
89 | |||
90 | GridLayout { | ||
91 | columns: 2 | ||
92 | |||
93 | DayOfWeekRow { | ||
94 | locale: grid.locale | ||
95 | |||
96 | Layout.column: 1 | ||
97 | Layout.fillWidth: true | ||
98 | |||
99 | delegate: Text { | ||
100 | required property string shortName | ||
101 | |||
102 | font.pointSize: 10 | ||
103 | font.family: "Fira Mono" | ||
104 | |||
105 | text: shortName | ||
106 | color: "#ffcc66" | ||
107 | |||
108 | horizontalAlignment: Text.AlignRight | ||
109 | verticalAlignment: Text.AlignVCenter | ||
110 | } | ||
111 | } | ||
112 | |||
113 | WeekNumberColumn { | ||
114 | month: grid.month | ||
115 | year: grid.year | ||
116 | locale: grid.locale | ||
117 | |||
118 | Layout.fillHeight: true | ||
119 | |||
120 | delegate: Text { | ||
121 | required property int weekNumber | ||
122 | |||
123 | opacity: { | ||
124 | const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7); | ||
125 | const dayOfWeek = simple.getDay(); | ||
126 | const isoWeekStart = simple; | ||
127 | |||
128 | isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1); | ||
129 | if (dayOfWeek > 4) { | ||
130 | isoWeekStart.setDate(isoWeekStart.getDate() + 7); | ||
131 | } | ||
132 | |||
133 | for (let i = 0; i < 7; i++) { | ||
134 | const dayInWeek = isoWeekStart; | ||
135 | dayInWeek.setDate(dayInWeek.getDate() + i); | ||
136 | if (dayInWeek.getMonth() == monthCalendar.month) | ||
137 | return 1; | ||
138 | } | ||
139 | |||
140 | return 0; | ||
141 | } | ||
142 | |||
143 | font.pointSize: 10 | ||
144 | font.family: "Fira Sans" | ||
145 | font.features: { "tnum": 1 } | ||
146 | |||
147 | text: weekNumber | ||
148 | color: "#99ffdd" | ||
149 | |||
150 | horizontalAlignment: Text.AlignRight | ||
151 | verticalAlignment: Text.AlignVCenter | ||
152 | } | ||
153 | } | ||
154 | |||
155 | MonthGrid { | ||
156 | id: grid | ||
157 | |||
158 | year: yearCalendar.year | ||
159 | month: monthCalendar.month | ||
160 | locale: Qt.locale("en_DK") | ||
161 | |||
162 | Layout.fillWidth: true | ||
163 | Layout.fillHeight: true | ||
164 | |||
165 | delegate: Text { | ||
166 | required property var model | ||
167 | |||
168 | opacity: model.month === monthCalendar.month ? 1 : 0 | ||
169 | |||
170 | font.pointSize: 10 | ||
171 | font.family: "Fira Sans" | ||
172 | font.features: { "tnum": 1 } | ||
173 | |||
174 | text: model.day | ||
175 | color: model.today ? "#ff6699" : "white" | ||
176 | |||
177 | horizontalAlignment: Text.AlignRight | ||
178 | verticalAlignment: Text.AlignVCenter | ||
179 | } | ||
180 | } | ||
181 | } | ||
182 | } | ||
183 | } | ||
184 | } | ||
185 | } | ||
186 | } \ No newline at end of file | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml b/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml new file mode 100644 index 00000000..4d3bd180 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml | |||
@@ -0,0 +1,78 @@ | |||
1 | import Quickshell | ||
2 | import QtQuick | ||
3 | import qs.Services | ||
4 | |||
5 | Rectangle { | ||
6 | id: kbdWidget | ||
7 | |||
8 | property var keyboardAbbrev: { "English (programmer Dvorak)": "dvp", "English (US)": "us" } | ||
9 | |||
10 | width: kbdLabel.contentWidth + 8 | ||
11 | color: { | ||
12 | if (kbdMouseArea.containsMouse) { | ||
13 | return "#33808080"; | ||
14 | } | ||
15 | return "transparent"; | ||
16 | } | ||
17 | height: parent.height | ||
18 | anchors.verticalCenter: parent.verticalCenter | ||
19 | |||
20 | MouseArea { | ||
21 | id: kbdMouseArea | ||
22 | |||
23 | anchors.fill: parent | ||
24 | hoverEnabled: true | ||
25 | cursorShape: Qt.PointingHandCursor | ||
26 | enabled: true | ||
27 | onClicked: { | ||
28 | NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": "Next" } } }, _ => {}) | ||
29 | } | ||
30 | } | ||
31 | |||
32 | Text { | ||
33 | id: kbdLabel | ||
34 | |||
35 | font.pointSize: 10 | ||
36 | font.family: "Fira Sans" | ||
37 | color: { | ||
38 | if (NiriService.keyboardLayouts?.current_idx === 0) | ||
39 | return "#555"; | ||
40 | return "white"; | ||
41 | } | ||
42 | anchors.centerIn: parent | ||
43 | |||
44 | text: { | ||
45 | const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx]; | ||
46 | if (!currentLayout) | ||
47 | return ""; | ||
48 | return kbdWidget.keyboardAbbrev[currentLayout] ? kbdWidget.keyboardAbbrev[currentLayout] : currentLayout; | ||
49 | } | ||
50 | } | ||
51 | |||
52 | PopupWindow { | ||
53 | anchor { | ||
54 | item: kbdMouseArea | ||
55 | edges: Edges.Bottom | ||
56 | } | ||
57 | visible: kbdMouseArea.containsMouse | ||
58 | |||
59 | implicitWidth: kbdTooltipText.contentWidth + 4 | ||
60 | implicitHeight: kbdTooltipText.contentHeight + 4 | ||
61 | color: "black" | ||
62 | |||
63 | Text { | ||
64 | id: kbdTooltipText | ||
65 | |||
66 | anchors.centerIn: parent | ||
67 | |||
68 | font.pointSize: 10 | ||
69 | font.family: "Fira Sans" | ||
70 | color: "white" | ||
71 | |||
72 | text: { | ||
73 | const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx]; | ||
74 | return currentLayout || ""; | ||
75 | } | ||
76 | } | ||
77 | } | ||
78 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/SystemTray.qml b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml new file mode 100644 index 00000000..a02a81fd --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml | |||
@@ -0,0 +1,94 @@ | |||
1 | import QtQuick | ||
2 | import Quickshell | ||
3 | import Quickshell.Widgets | ||
4 | import Quickshell.Services.SystemTray | ||
5 | |||
6 | Item { | ||
7 | anchors.verticalCenter: parent.verticalCenter | ||
8 | width: systemTrayRow.childrenRect.width | ||
9 | height: parent.height | ||
10 | clip: true | ||
11 | |||
12 | Row { | ||
13 | id: systemTrayRow | ||
14 | anchors.centerIn: parent | ||
15 | width: childrenRect.width | ||
16 | height: parent.height | ||
17 | spacing: 0 | ||
18 | |||
19 | Repeater { | ||
20 | model: SystemTray.items.values | ||
21 | |||
22 | delegate: Item { | ||
23 | property var trayItem: modelData | ||
24 | property string iconSource: { | ||
25 | let icon = trayItem && trayItem.icon | ||
26 | if (typeof icon === 'string' || icon instanceof String) { | ||
27 | if (icon.includes("?path=")) { | ||
28 | const split = icon.split("?path=") | ||
29 | if (split.length !== 2) | ||
30 | return icon | ||
31 | const name = split[0] | ||
32 | const path = split[1] | ||
33 | const fileName = name.substring( | ||
34 | name.lastIndexOf("/") + 1) | ||
35 | return `file://${path}/${fileName}` | ||
36 | } | ||
37 | return icon | ||
38 | } | ||
39 | return "" | ||
40 | } | ||
41 | |||
42 | width: 16 | ||
43 | height: parent.height | ||
44 | anchors.verticalCenter: parent.verticalCenter | ||
45 | |||
46 | IconImage { | ||
47 | anchors.centerIn: parent | ||
48 | width: parent.width | ||
49 | height: parent.width | ||
50 | source: parent.iconSource | ||
51 | asynchronous: true | ||
52 | smooth: true | ||
53 | mipmap: true | ||
54 | } | ||
55 | |||
56 | MouseArea { | ||
57 | id: trayItemArea | ||
58 | |||
59 | anchors.fill: parent | ||
60 | acceptedButtons: Qt.LeftButton | Qt.RightButton | ||
61 | hoverEnabled: true | ||
62 | cursorShape: Qt.PointingHandCursor | ||
63 | onClicked: mouse => { | ||
64 | if (!trayItem) | ||
65 | return | ||
66 | |||
67 | if (mouse.button === Qt.LeftButton | ||
68 | && !trayItem.onlyMenu) { | ||
69 | trayItem.activate() | ||
70 | return | ||
71 | } | ||
72 | |||
73 | if (trayItem.hasMenu) { | ||
74 | var globalPos = mapToGlobal(0, 0) | ||
75 | var currentScreen = screen || Screen | ||
76 | var screenX = currentScreen.x || 0 | ||
77 | var relativeX = globalPos.x - screenX | ||
78 | menuAnchor.menu = trayItem.menu | ||
79 | menuAnchor.anchor.window = bar | ||
80 | menuAnchor.anchor.rect = Qt.rect( | ||
81 | relativeX, | ||
82 | 21, | ||
83 | parent.width, 1) | ||
84 | menuAnchor.open() | ||
85 | } | ||
86 | } | ||
87 | } | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | QsMenuAnchor { | ||
92 | id: menuAnchor | ||
93 | } | ||
94 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml b/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml new file mode 100644 index 00000000..153c56bb --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml | |||
@@ -0,0 +1,71 @@ | |||
1 | import QtQuick | ||
2 | import qs.Services | ||
3 | |||
4 | Row { | ||
5 | id: workspaces | ||
6 | |||
7 | property var ignoreWorkspaces: @ignore_workspaces@ | ||
8 | |||
9 | height: parent.height | ||
10 | anchors.verticalCenter: parent.verticalCenter | ||
11 | spacing: 0 | ||
12 | |||
13 | Repeater { | ||
14 | model: { | ||
15 | let currWorkspaces = NiriService.workspaces; | ||
16 | const ignoreWorkspaces = Array.from(workspaces.ignoreWorkspaces); | ||
17 | currWorkspaces = currWorkspaces.filter(ws => ws.is_active || ignoreWorkspaces.every(iws => iws !== ws.name)); | ||
18 | currWorkspaces.sort((a, b) => { | ||
19 | if (NiriService.outputs?.[a.output]?.logical?.x !== NiriService.outputs?.[b.output]?.logical?.x) | ||
20 | return NiriService.outputs?.[a.output]?.logical?.x - NiriService.outputs?.[b.output]?.logical?.x | ||
21 | if (NiriService.outputs?.[a.output]?.logical?.y !== NiriService.outputs?.[b.output]?.logical?.y) | ||
22 | return NiriService.outputs?.[a.output]?.logical?.y - NiriService.outputs?.[b.output]?.logical?.y | ||
23 | return a.idx - b.idx; | ||
24 | }); | ||
25 | return currWorkspaces; | ||
26 | } | ||
27 | |||
28 | Rectangle { | ||
29 | property var workspaceData: modelData | ||
30 | |||
31 | width: wsLabel.contentWidth + 8 | ||
32 | color: { | ||
33 | if (mouseArea.containsMouse) { | ||
34 | return "#33808080"; | ||
35 | } | ||
36 | return "transparent"; | ||
37 | } | ||
38 | height: parent.height | ||
39 | anchors.verticalCenter: parent.verticalCenter | ||
40 | |||
41 | MouseArea { | ||
42 | id: mouseArea | ||
43 | |||
44 | anchors.fill: parent | ||
45 | hoverEnabled: true | ||
46 | cursorShape: Qt.PointingHandCursor | ||
47 | enabled: true | ||
48 | onClicked: { | ||
49 | NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {}) | ||
50 | } | ||
51 | } | ||
52 | |||
53 | Text { | ||
54 | id: wsLabel | ||
55 | |||
56 | font.pointSize: 10 | ||
57 | font.family: "Fira Sans" | ||
58 | color: { | ||
59 | if (workspaceData.is_active) | ||
60 | return "#23fd00"; | ||
61 | if (workspaceData.active_window_id === null) | ||
62 | return "#555"; | ||
63 | return "white"; | ||
64 | } | ||
65 | anchors.centerIn: parent | ||
66 | |||
67 | text: workspaceData.name ? workspaceData.name : workspaceData.idx | ||
68 | } | ||
69 | } | ||
70 | } | ||
71 | } \ No newline at end of file | ||