Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/modules/ii/appInventory/AppInventoryScope.qml
Fimeg cecde3bac2 quickshell: session arbiter + fix IPC returns silently dropping payloads
Fork ii's Session singleton and add a sessionctl.* surface.

The IPC bug is the important half. Quickshell marshals exactly five types
(string/int/bool/double/color) and maps a `var` return to VOID, discarding
the payload with no error -- src/io/ipc.cpp, "void and var get mixed by qml
engine". dock.*, shell.* and apps.* were all declared `: var`, so they
registered as `(): void` and returned nothing at all. The {ok, reason}
contract has never once reached a caller. All of them now return JSON as a
string, which is what actually crosses the socket.

Session: upstream fires `systemctl X || loginctl X` detached and throws the
exit code away. Fine on a desktop with someone at the keyboard, not fine on
a phone where the shell is the session manager and a verb that silently does
nothing leaves you believing the machine is suspending when it isn't. So:
probe loginctl/systemctl/hibernate once instead of assuming, run verbs
through a Process that logs the exit code, and refuse honestly when the
machine can't do the thing (the phone has no swap -- hibernate now says so
instead of no-opping). Every upstream verb keeps its name and call sites.

Inhibits carry a mandatory reason and get a cookie; state() lists who is
holding the machine awake and why. "Why didn't it sleep" is now answerable.
unlock() is refused by design -- the lock is the credential gate, so no IPC
caller routes around the PIN pad.

Named sessionctl, not session: ii's SessionScreen already owns "session",
and quickshell drops duplicate targets silently rather than erroring.

Idle: drop the 2>/dev/null and run hypridle through a Process, so a unit
that fails to come back is a log line instead of a flat battery.

Verified on the laptop: inhibit stops hypridle, uninhibit brings it back.
2026-07-14 18:31:59 -04:00

67 lines
3 KiB
QML

// App inventory surface — holds the AppInventory projection and the IPC
// method surface the agent (via Souveraine's harness) calls. No UI; this is
// a service-only Scope, sibling in shape to how Dock.qml hosts DockManifest
// + its IpcHandler. See docs/tasks/app-inventory-manifest.md.
//
// Instantiated from panelFamilies/SouveraineFamily.qml. The AppInventory
// type lives here as a local type (not as a qs.services singleton) to avoid
// the GlobalStates circular import — see AppInventory.qml's header comment.
import qs
import qs.modules.common
import "." as AppInvLocal
import QtQuick
import Quickshell
import Quickshell.Io
Scope {
// The inventory projection + parse/cache machinery. Carries its own
// imports (qs, qs.services, qs.modules.common.functions) because local
// types do NOT inherit this file's imports.
AppInvLocal.AppInventory { id: appInventory }
// Read-only method surface. Every method delegates to appInventory and
// returns its result shape ({ok, ...}). Refusals/not-founds are results,
// not errors, so a caller learns from them instead of guessing.
//
// IPC type constraint: quickshell's IpcHandler can only marshal declared
// primitive types across the wire — untyped args become QVariant and are
// rejected ("cannot be used across IPC"). So every parameter has an
// explicit type. The inventory's list(filter) takes an object in-process;
// over IPC we expose list(category: string) and build the filter here.
// The same five-type limit applies to RETURNS, not just arguments: a `var`
// return is mapped to VOID and the payload is dropped without an error
// (src/io/ipc.cpp ipcType(); "void and var get mixed by qml engine"). These
// were declared `: var` and so registered as `(): void` — every call
// returned nothing. Returning JSON as a string is what actually crosses.
IpcHandler {
target: "apps"
// apps.list() -> all (noDisplay excluded)
// apps.list("Network") -> only entries in the "Network" category
function list(category: string): string {
return JSON.stringify(appInventory.listFromCategory(category));
}
// apps.get("firefox") -> {ok, entry?} or {ok:false, reason:"not-found"}
function get(appId: string): string {
return JSON.stringify(appInventory.get(appId));
}
// apps.find("fire") -> fuzzy-ranked entries (default limit 50)
// apps.find("fire", 10) -> capped at 10
function find(query: string, limit: int): string {
return JSON.stringify(appInventory.find(query, limit));
}
// apps.categories() -> {ok, categories:[{category,count}]}
function categories(): string {
return JSON.stringify(appInventory.categories());
}
// apps.refresh() -> trigger a rescan (async; re-query after the log line)
function refresh(): string {
return JSON.stringify(appInventory.refresh());
}
}
}