initial commit

This commit is contained in:
2026-07-26 21:16:55 +02:00
commit 30fdd5cb28
30 changed files with 3373 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
pragma Singleton
import Quickshell
// Required: execDetached's context overload takes a processContext value type
// from this module, and without the import the call fails to resolve.
import Quickshell.Io
import qs.config
// Quickshell's DesktopEntry.execute() deliberately ignores both Terminal=true
// and Exec field codes, so entries are launched here instead. Without this,
// entries like `firefox %U` are started with a literal "%U" argument.
Singleton {
id: root
function withoutFieldCodes(command: var): var {
const out = [];
for (const arg of command) {
// "%%" is an escaped percent and collapses to one. Every other
// %<char> is a field code the spec says to substitute with files,
// URLs or icons — launching from a menu has nothing to substitute,
// so they are removed.
const cleaned = arg.replace(/%(.)/g, (match, char) => char === "%" ? "%" : "");
// An argument that was nothing but a field code is dropped.
if (cleaned !== "")
out.push(cleaned);
}
return out;
}
function launch(entry: var): void {
const command = root.withoutFieldCodes(entry.command);
if (command.length === 0)
return;
const argv = entry.runInTerminal ? [...Config.terminal, ...command] : command;
const options = {
command: argv
};
if (entry.workingDirectory)
options.workingDirectory = entry.workingDirectory;
Quickshell.execDetached(options);
}
}
+123
View File
@@ -0,0 +1,123 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
// attwm's IPC socket: the window list, and the commands that act on a window.
//
// river 0.4 ships no window management policy of its own — an external client
// speaking river-window-management-v1 owns it, which here is attwm. That
// protocol has no `activate_requested` event and focus is set solely by the
// window manager, so the `activate` request wlr-foreign-toplevel-management
// sends on a task-list click reaches nobody at all and focus never moves. This
// socket is the way in. See modules/WindowList.qml.
//
// One connection does both jobs: it subscribes to state and commands go back
// down the same socket, so a click costs no process spawn.
Singleton {
id: root
// True once attwm has answered. Everything here is inert otherwise, which
// is what keeps the panel working unchanged on other compositors.
readonly property bool available: link.item ? link.item.connected : false
// Only worth trying under river — anywhere else the socket will never
// exist and the retry below would spend the session failing to connect.
readonly property bool enabled: !!Quickshell.env("ATTWM_SOCKET") || (Quickshell.env("XDG_CURRENT_DESKTOP") || "").toLowerCase().includes("river")
// Latest state object from attwm, parsed.
property var state: ({})
readonly property var outputs: root.state.outputs ?? []
readonly property var allWindows: {
const all = [];
for (const out of root.outputs)
all.push(...out.windows);
return all;
}
function outputFor(name: string): var {
for (const out of root.outputs)
if (out.name === name)
return out;
return null;
}
// Both take a window id — the `id` field of a window in the state above,
// which is river's own identifier for it.
function focusWindow(id: string): void {
root.send(`focus-window ${id}`);
}
function closeWindow(id: string): void {
root.send(`close-window ${id}`);
}
function send(command: string): void {
if (root.available)
link.item.write(`${command}\n`);
}
// A Socket that fails to connect stays dead: it reports no state change and
// re-asserting `connected` does not make it try again. Reconnecting
// therefore means building a new one, so the socket lives in a Loader that
// can be thrown away. The panel and attwm start independently and either
// may restart, so giving up after the first attempt is not an option.
Loader {
id: link
active: root.enabled
sourceComponent: Socket {
// attwm names its socket after the Wayland display, so several
// river sessions can run side by side without colliding.
path: {
const explicit = Quickshell.env("ATTWM_SOCKET");
if (explicit)
return explicit;
const dir = Quickshell.env("XDG_RUNTIME_DIR");
const display = Quickshell.env("WAYLAND_DISPLAY") || "wayland-0";
return `${dir}/att_wm-${display}.sock`;
}
connected: true
// attwm replies with a full state object immediately, so the window
// list does not flash empty on connect.
onConnectionStateChanged: if (connected)
write("subscribe\n")
parser: SplitParser {
onRead: line => {
// Command acknowledgements ("ok" / "err ...") share the
// stream with state objects; only the latter are JSON.
if (!line.startsWith("{"))
return;
try {
root.state = JSON.parse(line);
} catch (e) {
console.warn("attwm: bad state line:", e);
}
}
}
}
}
Timer {
interval: 1000
repeat: true
running: root.enabled && !root.available
onTriggered: {
// Drop whatever the dead connection last said, so a reconnect
// cannot briefly show a window list from before the restart.
root.state = ({});
link.active = false;
link.active = true;
}
}
}
+53
View File
@@ -0,0 +1,53 @@
pragma Singleton
import Quickshell
import Quickshell.Wayland
// Shell-wide UI state. The menus are per-screen surfaces, but only one may be
// open at a time and only on one screen, so ownership lives here rather than in
// the panel instances.
Singleton {
id: root
// Id of the open menu ("launcher", "bluetooth"), or "" when none is open.
property string openMenu: ""
// Name of the screen it is open on, or "" when closed.
property string menuScreen: ""
// Best guess at the screen the user is looking at, used when a menu is
// opened by keybind rather than by clicking a specific panel.
function focusedScreenName(): string {
const active = ToplevelManager.activeToplevel;
if (active && active.screens.length > 0)
return active.screens[0].name;
return Quickshell.screens.length > 0 ? Quickshell.screens[0].name : "";
}
function isOpen(menu: string, screenName: string): bool {
return screenName !== "" && root.openMenu === menu && root.menuScreen === screenName;
}
function open(menu: string, screenName: string): void {
// Closed first: moving a menu between screens would otherwise map the
// outgoing menu onto the incoming screen for one binding pass.
root.openMenu = "";
root.menuScreen = screenName || root.focusedScreenName();
root.openMenu = menu;
}
function close(): void {
root.openMenu = "";
root.menuScreen = "";
}
function toggle(menu: string, screenName: string): void {
const target = screenName || root.focusedScreenName();
if (root.openMenu === menu && root.menuScreen === target)
root.close();
else
root.open(menu, target);
}
}