1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-29 07:52:50 -05:00

niri: connect directly to socket rather than running commands

This commit is contained in:
bbedward
2025-08-07 15:52:02 -04:00
parent fc13ed5c3a
commit 3dae0ec5e3
4 changed files with 191 additions and 273 deletions

View File

@@ -3,6 +3,7 @@ import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Services
import qs.Widgets import qs.Widgets
DankModal { DankModal {
@@ -14,25 +15,20 @@ DankModal {
property string powerConfirmMessage: "" property string powerConfirmMessage: ""
function executePowerAction(action) { function executePowerAction(action) {
let command = [];
switch (action) { switch (action) {
case "logout": case "logout":
command = ["niri", "msg", "action", "quit", "-s"]; NiriService.quit();
break; break;
case "suspend": case "suspend":
command = ["systemctl", "suspend"]; Quickshell.execDetached(["systemctl", "suspend"]);
break; break;
case "reboot": case "reboot":
command = ["systemctl", "reboot"]; Quickshell.execDetached(["systemctl", "reboot"]);
break; break;
case "poweroff": case "poweroff":
command = ["systemctl", "poweroff"]; Quickshell.execDetached(["systemctl", "poweroff"]);
break; break;
} }
if (command.length > 0) {
Quickshell.execDetached(command);
}
} }
visible: powerConfirmVisible visible: powerConfirmVisible

View File

@@ -928,7 +928,7 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
logoutDialog.close() logoutDialog.close()
Quickshell.execDetached(["niri", "msg", "action", "quit", "-s"]) NiriService.quit()
} }
} }
} }

View File

@@ -121,7 +121,7 @@ Rectangle {
enabled: !isPlaceholder enabled: !isPlaceholder
onClicked: { onClicked: {
if (!isPlaceholder) if (!isPlaceholder)
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", (modelData - 1).toString()]); NiriService.switchToWorkspace(modelData - 1);
} }
} }

View File

@@ -7,371 +7,293 @@ import Quickshell.Io
Singleton { Singleton {
id: root id: root
// Workspace management // Workspace management
property var workspaces: ({})
property var allWorkspaces: [] property var allWorkspaces: []
property int focusedWorkspaceIndex: 0 property int focusedWorkspaceIndex: 0
property string focusedWorkspaceId: "" property string focusedWorkspaceId: ""
property var currentOutputWorkspaces: [] property var currentOutputWorkspaces: []
property string currentOutput: "" property string currentOutput: ""
// Window management // Window management
property var windows: [] property var windows: []
property int focusedWindowIndex: -1 property int focusedWindowIndex: -1
property string focusedWindowTitle: "(No active window)" property string focusedWindowTitle: "(No active window)"
property string focusedWindowId: "" property string focusedWindowId: ""
// Overview state // Overview state
property bool inOverview: false property bool inOverview: false
signal windowOpenedOrChanged(var windowData) signal windowOpenedOrChanged(var windowData)
// Feature availability // Feature availability
property bool niriAvailable: false property bool niriAvailable: false
Component.onCompleted: { readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
console.log("NiriService: Component.onCompleted - initializing service")
checkNiriAvailability() Component.onCompleted: checkNiriAvailability()
}
// Check if niri is available
Process { Process {
id: niriCheck id: niriCheck
command: ["which", "niri"] command: ["test", "-S", root.socketPath]
onExited: (exitCode) => { onExited: exitCode => {
root.niriAvailable = exitCode === 0 root.niriAvailable = exitCode === 0;
if (root.niriAvailable) { if (root.niriAvailable) {
console.log("NiriService: niri found, starting event stream and loading initial data") eventStreamSocket.connected = true;
eventStreamProcess.running = true
loadInitialWorkspaceData()
} else {
console.log("NiriService: niri not found, workspace features disabled")
} }
} }
} }
function checkNiriAvailability() { function checkNiriAvailability() {
niriCheck.running = true niriCheck.running = true;
} }
// Load initial workspace data Socket {
Process { id: eventStreamSocket
id: initialDataQuery path: root.socketPath
command: ["niri", "msg", "-j", "workspaces"] connected: false
running: false
onConnectionStateChanged: {
stdout: StdioCollector { if (connected) {
onStreamFinished: { write('"EventStream"\n');
if (text && text.trim()) {
try {
console.log("NiriService: Loaded initial workspace data")
const workspaces = JSON.parse(text.trim())
// Initial query returns array directly, event stream wraps it in WorkspacesChanged
handleWorkspacesChanged({ workspaces: workspaces })
} catch (e) {
console.warn("NiriService: Failed to parse initial workspace data:", e)
}
}
} }
} }
}
parser: SplitParser {
// Load initial windows data onRead: line => {
Process {
id: initialWindowsQuery
command: ["niri", "msg", "-j", "windows"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
try {
const windowsData = JSON.parse(text.trim())
if (windowsData && windowsData.windows) {
handleWindowsChanged(windowsData)
console.log("NiriService: Loaded", windowsData.windows.length, "initial windows")
}
} catch (e) {
console.warn("NiriService: Failed to parse initial windows data:", e)
}
}
}
}
}
// Load initial focused window data
Process {
id: initialFocusedWindowQuery
command: ["niri", "msg", "-j", "focused-window"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
try {
const focusedData = JSON.parse(text.trim())
if (focusedData && focusedData.id) {
handleWindowFocusChanged({ id: focusedData.id })
console.log("NiriService: Loaded initial focused window:", focusedData.id)
}
} catch (e) {
console.warn("NiriService: Failed to parse initial focused window data:", e)
}
}
}
}
}
function loadInitialWorkspaceData() {
console.log("NiriService: Loading initial workspace data...")
initialDataQuery.running = true
initialWindowsQuery.running = true
initialFocusedWindowQuery.running = true
}
// Event stream for real-time updates
Process {
id: eventStreamProcess
command: ["niri", "msg", "-j", "event-stream"]
running: false // Will be enabled after niri check
stdout: SplitParser {
onRead: data => {
try { try {
const event = JSON.parse(data.trim()) const event = JSON.parse(line);
handleNiriEvent(event) handleNiriEvent(event);
} catch (e) { } catch (e) {
console.warn("NiriService: Failed to parse event:", data, e) console.warn("NiriService: Failed to parse event:", line, e);
} }
} }
} }
onExited: (exitCode) => {
if (exitCode !== 0 && root.niriAvailable) {
console.warn("NiriService: Event stream exited with code", exitCode, "restarting immediately")
eventStreamProcess.running = true
}
}
} }
Socket {
id: requestSocket
path: root.socketPath
connected: root.niriAvailable
}
function handleNiriEvent(event) { function handleNiriEvent(event) {
if (event.WorkspacesChanged) { if (event.WorkspacesChanged) {
handleWorkspacesChanged(event.WorkspacesChanged) handleWorkspacesChanged(event.WorkspacesChanged);
} else if (event.WorkspaceActivated) { } else if (event.WorkspaceActivated) {
handleWorkspaceActivated(event.WorkspaceActivated) handleWorkspaceActivated(event.WorkspaceActivated);
} else if (event.WindowsChanged) { } else if (event.WindowsChanged) {
handleWindowsChanged(event.WindowsChanged) handleWindowsChanged(event.WindowsChanged);
} else if (event.WindowClosed) { } else if (event.WindowClosed) {
handleWindowClosed(event.WindowClosed) handleWindowClosed(event.WindowClosed);
} else if (event.WindowFocusChanged) { } else if (event.WindowFocusChanged) {
handleWindowFocusChanged(event.WindowFocusChanged) handleWindowFocusChanged(event.WindowFocusChanged);
} else if (event.WindowOpenedOrChanged) { } else if (event.WindowOpenedOrChanged) {
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged) handleWindowOpenedOrChanged(event.WindowOpenedOrChanged);
} else if (event.OverviewOpenedOrClosed) { } else if (event.OverviewOpenedOrClosed) {
handleOverviewChanged(event.OverviewOpenedOrClosed) handleOverviewChanged(event.OverviewOpenedOrClosed);
} }
} }
function handleWorkspacesChanged(data) { function handleWorkspacesChanged(data) {
allWorkspaces = [...data.workspaces].sort((a, b) => a.idx - b.idx) const workspaces = {};
// Update focused workspace for (const ws of data.workspaces) {
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused) workspaces[ws.id] = ws;
if (focusedWorkspaceIndex >= 0) {
var focusedWs = allWorkspaces[focusedWorkspaceIndex]
focusedWorkspaceId = focusedWs.id
currentOutput = focusedWs.output || ""
} else {
focusedWorkspaceIndex = 0
focusedWorkspaceId = ""
} }
updateCurrentOutputWorkspaces() root.workspaces = workspaces;
allWorkspaces = [...data.workspaces].sort((a, b) => a.idx - b.idx);
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused);
if (focusedWorkspaceIndex >= 0) {
var focusedWs = allWorkspaces[focusedWorkspaceIndex];
focusedWorkspaceId = focusedWs.id;
currentOutput = focusedWs.output || "";
} else {
focusedWorkspaceIndex = 0;
focusedWorkspaceId = "";
}
updateCurrentOutputWorkspaces();
} }
function handleWorkspaceActivated(data) { function handleWorkspaceActivated(data) {
// Update focused workspace const ws = root.workspaces[data.id];
focusedWorkspaceId = data.id if (!ws)
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id) return;
const output = ws.output;
if (focusedWorkspaceIndex >= 0) {
var activatedWs = allWorkspaces[focusedWorkspaceIndex] for (const id in root.workspaces) {
const workspace = root.workspaces[id];
// Update workspace states properly const got_activated = workspace.id === data.id;
// First, deactivate all workspaces on this output
for (var i = 0; i < allWorkspaces.length; i++) { if (workspace.output === output) {
if (allWorkspaces[i].output === activatedWs.output) { workspace.is_active = got_activated;
allWorkspaces[i].is_active = false }
allWorkspaces[i].is_focused = false
} if (data.focused) {
workspace.is_focused = got_activated;
} }
// Then activate the new workspace
allWorkspaces[focusedWorkspaceIndex].is_active = true
allWorkspaces[focusedWorkspaceIndex].is_focused = data.focused || false
currentOutput = activatedWs.output || ""
updateCurrentOutputWorkspaces()
// Force property change notifications
allWorkspacesChanged()
} else {
focusedWorkspaceIndex = 0
} }
focusedWorkspaceId = data.id;
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id);
if (focusedWorkspaceIndex >= 0) {
currentOutput = allWorkspaces[focusedWorkspaceIndex].output || "";
}
allWorkspaces = Object.values(root.workspaces).sort((a, b) => a.idx - b.idx);
updateCurrentOutputWorkspaces();
workspacesChanged();
} }
function handleWindowsChanged(data) { function handleWindowsChanged(data) {
windows = [...data.windows].sort((a, b) => a.id - b.id) windows = [...data.windows].sort((a, b) => a.id - b.id);
updateFocusedWindow() updateFocusedWindow();
} }
function handleWindowClosed(data) { function handleWindowClosed(data) {
windows = windows.filter(w => w.id !== data.id) windows = windows.filter(w => w.id !== data.id);
updateFocusedWindow() updateFocusedWindow();
} }
function handleWindowFocusChanged(data) { function handleWindowFocusChanged(data) {
if (data.id) { if (data.id) {
focusedWindowId = data.id focusedWindowId = data.id;
focusedWindowIndex = windows.findIndex(w => w.id === data.id) focusedWindowIndex = windows.findIndex(w => w.id === data.id);
} else { } else {
focusedWindowId = "" focusedWindowId = "";
focusedWindowIndex = -1 focusedWindowIndex = -1;
} }
updateFocusedWindow() updateFocusedWindow();
} }
function handleWindowOpenedOrChanged(data) { function handleWindowOpenedOrChanged(data) {
if (!data.window) return; if (!data.window)
return;
const window = data.window; const window = data.window;
const existingIndex = windows.findIndex(w => w.id === window.id); const existingIndex = windows.findIndex(w => w.id === window.id);
if (existingIndex >= 0) { if (existingIndex >= 0) {
// Update existing window - create new array to trigger property change
let updatedWindows = [...windows]; let updatedWindows = [...windows];
updatedWindows[existingIndex] = window; updatedWindows[existingIndex] = window;
windows = updatedWindows.sort((a, b) => a.id - b.id); windows = updatedWindows.sort((a, b) => a.id - b.id);
} else { } else {
// Add new window
windows = [...windows, window].sort((a, b) => a.id - b.id); windows = [...windows, window].sort((a, b) => a.id - b.id);
} }
// Update focused window if this window is focused
if (window.is_focused) { if (window.is_focused) {
focusedWindowId = window.id; focusedWindowId = window.id;
focusedWindowIndex = windows.findIndex(w => w.id === window.id); focusedWindowIndex = windows.findIndex(w => w.id === window.id);
} }
updateFocusedWindow(); updateFocusedWindow();
// Emit signal for other services to listen to
windowOpenedOrChanged(window); windowOpenedOrChanged(window);
} }
function handleOverviewChanged(data) { function handleOverviewChanged(data) {
inOverview = data.is_open inOverview = data.is_open;
} }
function updateCurrentOutputWorkspaces() { function updateCurrentOutputWorkspaces() {
if (!currentOutput) { if (!currentOutput) {
currentOutputWorkspaces = allWorkspaces currentOutputWorkspaces = allWorkspaces;
return return;
} }
// Filter workspaces for current output var outputWs = allWorkspaces.filter(w => w.output === currentOutput);
var outputWs = allWorkspaces.filter(w => w.output === currentOutput) currentOutputWorkspaces = outputWs;
currentOutputWorkspaces = outputWs
} }
function updateFocusedWindow() { function updateFocusedWindow() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
var focusedWin = windows[focusedWindowIndex] var focusedWin = windows[focusedWindowIndex];
focusedWindowTitle = focusedWin.title || "(Unnamed window)" focusedWindowTitle = focusedWin.title || "(Unnamed window)";
} else { } else {
focusedWindowTitle = "(No active window)" focusedWindowTitle = "(No active window)";
} }
} }
// Public API functions function send(request) {
function switchToWorkspace(workspaceId) { if (!niriAvailable || !requestSocket.connected)
if (!niriAvailable) return false return false;
requestSocket.write(JSON.stringify(request) + "\n");
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) return true;
return true
} }
function switchToWorkspaceByIndex(index) { function switchToWorkspace(workspaceIndex) {
if (!niriAvailable || index < 0 || index >= allWorkspaces.length) return false return send({
Action: {
var workspace = allWorkspaces[index] FocusWorkspace: {
return switchToWorkspace(workspace.id) reference: {
Index: workspaceIndex
}
}
}
});
} }
function switchToWorkspaceByNumber(number, output) {
if (!niriAvailable) return false
var targetOutput = output || currentOutput
if (!targetOutput) {
console.warn("NiriService: No output specified for workspace switching")
return false
}
// Get workspaces for the target output, sorted by idx
var outputWorkspaces = allWorkspaces.filter(w => w.output === targetOutput).sort((a, b) => a.idx - b.idx)
// Use sequential index (number is 1-based, array is 0-based)
if (number >= 1 && number <= outputWorkspaces.length) {
var workspace = outputWorkspaces[number - 1]
return switchToWorkspace(workspace.id)
}
console.warn("NiriService: No workspace", number, "found on output", targetOutput)
return false
}
function getCurrentOutputWorkspaceNumbers() { function getCurrentOutputWorkspaceNumbers() {
return currentOutputWorkspaces.map(w => w.idx + 1) // niri uses 0-based, UI shows 1-based return currentOutputWorkspaces.map(w => w.idx + 1); // niri uses 0-based, UI shows 1-based
} }
function getCurrentWorkspaceNumber() { function getCurrentWorkspaceNumber() {
if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) { if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) {
return allWorkspaces[focusedWorkspaceIndex].idx + 1 return allWorkspaces[focusedWorkspaceIndex].idx + 1;
} }
return 1 return 1;
} }
function focusWindow(windowId) { function focusWindow(windowId) {
if (!niriAvailable) return false return send({
Action: {
console.log("NiriService: Focusing window with command:", ["niri", "msg", "action", "focus-window", "--id", windowId.toString()]) FocusWindow: {
Quickshell.execDetached(["niri", "msg", "action", "focus-window", "--id", windowId.toString()]) id: windowId
return true }
}
});
} }
function closeWindow(windowId) { function closeWindow(windowId) {
if (!niriAvailable) return false return send({
Action: {
console.log("NiriService: Closing window with command:", ["niri", "msg", "action", "close-window", "--id", windowId.toString()]) CloseWindow: {
Quickshell.execDetached(["niri", "msg", "action", "close-window", "--id", windowId.toString()]) id: windowId
return true }
}
});
} }
function quit() {
return send({
Action: {
Quit: {
skip_confirmation: true
}
}
});
}
function getWindowsByAppId(appId) { function getWindowsByAppId(appId) {
if (!appId) return [] if (!appId)
return windows.filter(w => w.app_id && w.app_id.toLowerCase() === appId.toLowerCase()) return [];
return windows.filter(w => w.app_id && w.app_id.toLowerCase() === appId.toLowerCase());
} }
function getRunningAppIds() { function getRunningAppIds() {
var appIds = new Set() var appIds = new Set();
windows.forEach(w => { windows.forEach(w => {
if (w.app_id) { if (w.app_id) {
appIds.add(w.app_id.toLowerCase()) appIds.add(w.app_id.toLowerCase());
} }
}) });
return Array.from(appIds) return Array.from(appIds);
} }
} }