From 45a1316bf1df6ec32a133f8e648bbac0bbc988d6 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Sat, 30 Aug 2025 22:07:54 +0200 Subject: ... --- accounts/gkleen@sif/default.nix | 4 +- accounts/gkleen@sif/niri/waybar.nix | 2 +- accounts/gkleen@sif/shell/default.nix | 1 + accounts/gkleen@sif/shell/quickshell/Bar.qml | 307 ++++++++++++++++++++- .../shell/quickshell/Services/NiriService.qml | 190 +++++++++++++ accounts/gkleen@sif/shell/quickshell/shell.qml | 2 + accounts/gkleen@sif/systemd.nix | 4 +- 7 files changed, 495 insertions(+), 15 deletions(-) create mode 100644 accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml (limited to 'accounts/gkleen@sif') 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 { name = "Paper-Mono-Dark"; }; }; + qt.enable = true; + qt.platformTheme = "gtk"; qt.kde.settings = { kwalletrc = { @@ -539,7 +541,7 @@ in { sessionVariables = { # GDK_SCALE = 96.0 / 282.0; # QT_AUTO_SCREEN_SCALE_FACTOR = 1; - QT_QPA_PLATFORMTHEME = "qt5ct"; + QT_QPA_PLATFORMTHEME = lib.mkForce "gtk3"; LIBVIRT_DEFAULT_URI = "qemu:///system"; STACK_XDG = 1; 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 in { config = { programs.waybar = { - enable = true; + enable = false; # true; systemd = { enable = true; 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 @@ src = ./quickshell; replacements = { coreutils = toString pkgs.coreutils; + ignore_workspaces = builtins.toJSON (map ({ name, ... }: name) config.programs.niri.scratchspaces); }; }; }; 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 @@ import Quickshell import Quickshell.Io +import Quickshell.Services.SystemTray +import Quickshell.Widgets import Custom as Custom import QtQuick +import qs.Services PanelWindow { + id: bar + property var modelData anchors { - bottom: true + top: true left: true right: true } @@ -25,15 +30,79 @@ PanelWindow { id: left height: parent.height + width: childrenRect.width anchors.left: parent.left anchors.leftMargin: 8 anchors.verticalCenter: parent.verticalCenter - spacing: 5 + spacing: 8 - Text { - color: "white" + Row { + id: workspaces + + property var ignoreWorkspaces: @ignore_workspaces@ + + height: parent.height anchors.verticalCenter: parent.verticalCenter - text: "left" + spacing: 0 + + Repeater { + model: { + let currWorkspaces = NiriService.workspaces; + const ignoreWorkspaces = Array.from(workspaces.ignoreWorkspaces); + currWorkspaces = currWorkspaces.filter(ws => ws.is_active || ignoreWorkspaces.every(iws => iws !== ws.name)); + currWorkspaces.sort((a, b) => { + if (NiriService.outputs?.[a.output]?.logical?.x !== NiriService.outputs?.[b.output]?.logical?.x) + return NiriService.outputs?.[a.output]?.logical?.x - NiriService.outputs?.[b.output]?.logical?.x + if (NiriService.outputs?.[a.output]?.logical?.y !== NiriService.outputs?.[b.output]?.logical?.y) + return NiriService.outputs?.[a.output]?.logical?.y - NiriService.outputs?.[b.output]?.logical?.y + return a.idx - b.idx; + }); + return currWorkspaces; + } + + Rectangle { + property var workspaceData: modelData + + width: wsLabel.contentWidth + 8 + color: { + if (mouseArea.containsMouse) { + return "#33808080"; + } + return "transparent"; + } + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: true + onClicked: { + NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {}) + } + } + + Text { + id: wsLabel + + font.pointSize: 10 + font.family: "Fira Sans" + color: { + if (workspaceData.active_window_id === null) + return "#555"; + if (workspaceData.is_active) + return "#23fd00"; + return "white"; + } + anchors.centerIn: parent + + text: workspaceData.name ? workspaceData.name : workspaceData.idx + } + } + } } } @@ -41,13 +110,92 @@ PanelWindow { id: center height: parent.height + width: childrenRect.width anchors.centerIn: parent spacing: 5 - Text { - color: "white" + Item { + id: activeWindowDisplay + + property var activeWindow: { + let currWindowId = Array.from(NiriService.workspaces).find(ws => { + return ws.output === bar.screen.name && ws.is_active; + })?.active_window_id; + + return currWindowId ? Array.from(NiriService.windows).find(win => win.id == currWindowId) : null; + } + property var windowEntry: activeWindow ? DesktopEntries.heuristicLookup(activeWindow.app_id) : null + anchors.verticalCenter: parent.verticalCenter - text: "center" + width: activeWindowDisplayContent.width + height: parent.height + + Row { + id: activeWindowDisplayContent + + width: childrenRect.width + height: parent.height + anchors.verticalCenter: parent.verticalCenter + spacing: 8 + + IconImage { + id: activeWindowIcon + + height: 14 + width: 14 + + anchors.verticalCenter: parent.verticalCenter + + source: { + let icon = activeWindowDisplay.windowEntry?.icon + if (typeof icon === 'string' || icon instanceof String) { + if (icon.includes("?path=")) { + const split = icon.split("?path=") + if (split.length !== 2) + return icon + const name = split[0] + const path = split[1] + const fileName = name.substring( + name.lastIndexOf("/") + 1) + return `file://${path}/${fileName}` + } else + icon = Quickshell.iconPath(icon); + return icon + } + return "" + } + asynchronous: true + smooth: true + mipmap: true + } + + Text { + id: windowTitle + + width: Math.min(implicitWidth, bar.width - 2*Math.max(left.width, right.width) - 2*8 - activeWindowIcon.width - activeWindowDisplayContent.spacing) + + property var appAliases: { "Firefox": "Mozilla Firefox", "mpv Media Player": "mpv", "Thunderbird": "Mozilla Thunderbird", "Thunderbird (LMU)": "Mozilla Thunderbird" } + + elide: Text.ElideRight + maximumLineCount: 1 + color: "white" + anchors.verticalCenter: parent.verticalCenter + text: { + if (!activeWindowDisplay.activeWindow) + return ""; + + var title = activeWindowDisplay.activeWindow.title; + var appName = activeWindowDisplay.windowEntry?.name; + if (appAliases[appName]) + appName = appAliases[appName]; + if (appName && title.endsWith(appName)) { + title = title.substring(0, title.length - appName.length); + title = title.replace(/\s*(—|-)\s*$/, ""); + } + return title; + } + } + } } } @@ -55,10 +203,147 @@ PanelWindow { id: right height: parent.height + width: childrenRect.width anchors.right: parent.right anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter - spacing: 5 + spacing: 8 + + Rectangle { + id: kbdWidget + + property var keyboardAbbrev: { "English (programmer Dvorak)": "dvp", "English (US)": "us" } + + width: kbdLabel.contentWidth + color: { + /* if (kbdMouseArea.containsMouse) { + return "#33808080"; + } */ + return "transparent"; + } + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + id: kbdMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: true + onClicked: { + NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": "Next" } } }, _ => {}) + } + } + + Text { + id: kbdLabel + + font.pointSize: 10 + font.family: "Fira Sans" + color: { + if (NiriService.keyboardLayouts?.current_idx === 0) + return "#555"; + return "white"; + } + anchors.centerIn: parent + + text: { + const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx]; + return kbdWidget.keyboardAbbrev[currentLayout] ? kbdWidget.keyboardAbbrev[currentLayout] : currentLayout; + } + } + } + + Item { + anchors.verticalCenter: parent.verticalCenter + width: systemTrayRow.childrenRect.width + height: parent.height + clip: true + + Row { + id: systemTrayRow + anchors.centerIn: parent + width: childrenRect.width + height: parent.height + spacing: 0 + + Repeater { + model: SystemTray.items.values + + delegate: Item { + property var trayItem: modelData + property string iconSource: { + let icon = trayItem && trayItem.icon + if (typeof icon === 'string' || icon instanceof String) { + if (icon.includes("?path=")) { + const split = icon.split("?path=") + if (split.length !== 2) + return icon + const name = split[0] + const path = split[1] + const fileName = name.substring( + name.lastIndexOf("/") + 1) + return `file://${path}/${fileName}` + } + return icon + } + return "" + } + + width: 16 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + + IconImage { + anchors.centerIn: parent + width: parent.width + height: parent.width + source: parent.iconSource + asynchronous: true + smooth: true + mipmap: true + } + + MouseArea { + id: trayItemArea + + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: mouse => { + if (!trayItem) + return + + if (mouse.button === Qt.LeftButton + && !trayItem.onlyMenu) { + trayItem.activate() + return + } + + if (trayItem.hasMenu) { + var globalPos = mapToGlobal(0, 0) + var currentScreen = screen || Screen + var screenX = currentScreen.x || 0 + var relativeX = globalPos.x - screenX + menuAnchor.menu = trayItem.menu + menuAnchor.anchor.window = bar + menuAnchor.anchor.rect = Qt.rect( + relativeX, + 21, + parent.width, 1) + menuAnchor.open() + } + } + } + } + } + } + QsMenuAnchor { + id: menuAnchor + } + } Text { id: clock @@ -67,8 +352,8 @@ PanelWindow { anchors.verticalCenter: parent.verticalCenter Custom.Chrono { - id: chrono - format: "W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}" + id: chrono + format: "W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}" } 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 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + property var workspaces: [] + property var outputs: {} + property var keyboardLayouts: {} + property var windows: [] + readonly property string socketPath: Quickshell.env("NIRI_SOCKET") + + onKeyboardLayoutsChanged: { + console.log(JSON.stringify(keyboardLayouts)); + } + + function refreshOutputs() { + commandSocket.sendCommand("Outputs", data => { + outputs = data.Ok.Outputs; + }); + } + + function sendCommand(command, callback) { + commandSocket.sendCommand(command, callback); + } + + Socket { + id: eventStreamSocket + path: root.socketPath + connected: true + + onConnectionStateChanged: { + if (connected) { + write('"EventStream"\n') + } + } + + parser: SplitParser { + onRead: line => { + try { + const event = JSON.parse(line) + + // console.log(JSON.stringify(event)) + + if (event.WorkspacesChanged) { + root.workspaces = event.WorkspacesChanged.workspaces + root.refreshOutputs(); + } else if (event.WorkspaceActivated) + eventWorkspaceActivated(event.WorkspaceActivated); + else if (event.WorkspaceUrgencyChanged) + eventWorkspaceUrgencyChanged(event.WorkspaceUrgencyChanged); + else if (event.WorkspaceActiveWindowChanged) + eventWorkspaceActiveWindowChanged(event.WorkspaceActiveWindowChanged); + else if (event.KeyboardLayoutsChanged) + root.keyboardLayouts = event.KeyboardLayoutsChanged.keyboard_layouts; + else if (event.KeyboardLayoutSwitched) + root.keyboardLayouts = Object.assign({}, root.keyboardLayouts, {"current_idx": event.KeyboardLayoutSwitched.idx }); + else if (event.WindowsChanged) + root.windows = event.WindowsChanged.windows + else if (event.WindowOpenedOrChanged) + eventWindowOpenedOrChanged(event.WindowOpenedOrChanged); + else if (event.WindowClosed) + eventWindowClosed(event.WindowClosed); + else if (event.WindowFocusChanged) + eventWindowFocusChanged(event.WindowFocusChanged); + else if (event.WindowUrgencyChanged) + eventWindowUrgencyChanged(event.WindowUrgencyChanged); + else if (event.WindowLayoutsChanged) + eventWindowLayoutsChanged(event.WindowLayoutsChanged); + } catch (e) { + console.warn("NiriService: Failed to parse event:", line, e) + } + } + } + } + + Socket { + id: commandSocket + path: root.socketPath + connected: true + + property var awaitingAnswer: null + property var cmdQueue: [] + + parser: SplitParser { + onRead: line => { + if (commandSocket.awaitingAnswer === null) + return; + + try { + const response = JSON.parse(line); + commandSocket.awaitingAnswer.callback(response); + commandSocket.awaitingAnswer = null; + } catch (e) { + console.warn("NiriService: Failed to parse response:", line, e) + } + commandSocket._handleQueue(); + } + } + + onCmdQueueChanged: { + _handleQueue(); + } + onAwaitingAnswerChanged: { + _handleQueue(); + } + + function _handleQueue() { + if (cmdQueue.length <= 0 || awaitingAnswer !== null) + return; + + let localQueue = Array.from(cmdQueue); + awaitingAnswer = localQueue.shift(); + cmdQueue = localQueue; + write(JSON.stringify(awaitingAnswer.command) + '\n'); + } + + function sendCommand(command, callback) { + cmdQueue = Array.from(cmdQueue).concat([{ "command": command, "callback": callback }]) + } + } + + function eventWorkspaceActivated(data) { + let relevant_output = null; + Array.from(root.workspaces).forEach(ws => { + if (data.id === ws.id) + relevant_output = ws.output; + }); + root.workspaces = Array.from(root.workspaces).map(ws => { + if (ws.output === relevant_output) { + ws.is_active = false; + ws.is_focused = false; + } + if (data.id === ws.id) { + ws.is_active = true; + ws.is_focused = data.focused; + } + return ws; + }); + } + function eventWorkspaceUrgencyChanged(data) { + root.workspaces = Array.from(root.workspaces).map(ws => { + if (data.id == ws.id) + ws.is_urgent = data.urgent; + return ws; + }); + } + function eventWorkspaceActiveWindowChanged(data) { + root.workspaces = Array.from(root.workspaces).map(ws => { + if (data.workspace_id === ws.id) + ws.active_window_id = data.active_window_id; + return ws; + }); + } + function eventWindowOpenedOrChanged(data) { + root.windows = Array.from(root.windows).map(win => { + if (win.id === data.window.id) + return data.window; + return win; + }); + } + function eventWindowClosed(data) { + root.windows = Array.from(root.windows).filter(win => win.id !== data.id); + } + function eventWindowFocusChanged(data) { + root.windows = Array.from(root.windows).map(win => { + win.is_focused = win.id === data.id; + return win; + }); + } + function eventWindowUrgencyChanged(data) { + root.windows = Array.from(root.windows).map(win => { + if (win.id === data.id) + win.is_urgent = data.urgent; + return win; + }); + } + function eventWindowLayoutsChanged(data) { + root.windows = Array.from(root.windows).map(win => { + Array.from(data.changes).forEach(change => { + if (win.id === change[0]) + win.layout = change[1]; + }); + return win; + }); + } +} 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 @@ +//@ pragma UseQApplication + import Quickshell ShellRoot { 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 { tray = { Unit = { PartOf = [ "graphical-session.target" ]; - Requires = [ "waybar.service" ]; - After = [ "graphical-session.target" "waybar.service" ]; + # Requires = [ "waybar.service" ]; + After = [ "graphical-session.target" ]; # "waybar.service" ]; Wants = ["blueman-applet.service" "udiskie.service" "network-manager-applet.service"]; }; }; -- cgit v1.2.3