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.
67 lines
3 KiB
QML
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());
|
|
}
|
|
}
|
|
}
|