initial commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user