Files
omarchy/shell/Commons/IpcRegistry.qml
T
David Heinemeier HanssonandClaude Opus 5.5 c231097df7 Answer omarchy-shell calls over the shell's own socket (#13435)
* Answer omarchy-shell calls over the shell's own socket

Every omarchy-shell call started a qs ipc client, ~45ms of startup for one
IPC call: a theme switch makes two, and every script-driven OSD, toggle
refresh and lock query paid it too.

The shell now serves a socket in XDG_RUNTIME_DIR, named from its config
path and Wayland display as qs ipc selects its instance, and omarchy-shell
tries it first through socat, which starts in ~5ms. First-party handlers
register as ShellIpc, an IpcHandler that qs ipc still reaches, and the
socket calls only the functions a handler declares with their exact
argument count, allowed by name so QObject methods such as destroy() stay
out of reach.

When the shell ran nothing it answers SKIP, and omarchy-shell asks qs ipc
for its exact answer, so errors, third-party plugins and an unreachable
socket behave as before. A call that may have run is never retried: a
timeout or a connection closed without an answer reports the shell as not
responding.

omarchy-shell shell ping takes ~13-18ms instead of ~61ms, and omarchy-osd
reaches the screen in ~36ms instead of ~77ms. Output and exit status match
the qs ipc path across 26 calls, errors and quiet mode included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Only accept whole socket replies and retry only unmade connections

A reply cut off after its OK prefix passed for the whole answer, and an
empty reply with socat failing was retried through qs ipc although the
request might already have been delivered.

An answer now counts only once its record separator arrived. socat's own
errors join the reply, so only its connect error, a socket nothing
listens on, falls back to qs ipc beside an explicit SKIP; anything else
is reported as not responding rather than retried.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:46:00 +02:00

78 lines
2.5 KiB
QML

pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
// Every ShellIpc handler, so the shell can answer an omarchy-shell call over
// its own socket instead of through a qs ipc client started per call. It
// answers only what qs ipc would: the first live, enabled handler for a
// target, and only the functions that handler declares. Those are allowed by
// name, never inferred from what a function call happens to reach: QObject
// methods such as destroy() are callable but not enumerable.
Singleton {
id: root
property var handlers: []
// A bare handler's own functions (signals and hooks such as targetChanged),
// which qs ipc never exposes and neither may the socket.
readonly property var builtins: functionNames(bareHandler)
IpcHandler {
id: bareHandler
enabled: false
}
function functionNames(object) {
var names = []
for (var key in object) {
if (typeof object[key] === "function") names.push(key)
}
return names
}
function register(handler) {
if (handlers.indexOf(handler) === -1) handlers.push(handler)
}
function unregister(handler) {
var index = handlers.indexOf(handler)
if (index !== -1) handlers.splice(index, 1)
}
function handlerFor(target) {
for (var i = 0; i < handlers.length; i++) {
var handler = handlers[i]
if (handler && handler.enabled && handler.target === target) return handler
}
return null
}
// { ran: true, output } once the function ran; { ran: false } when it did
// not (unknown target or function, wrong argument count), so the caller
// can ask qs ipc for its exact answer without running anything twice.
// The functions a handler declares: what it enumerates beyond a bare
// handler, less property change signals.
function declaredFunctions(handler) {
return functionNames(handler).filter(function(name) {
return builtins.indexOf(name) === -1 && !/Changed$/.test(name)
})
}
function call(target, method, args) {
var handler = handlerFor(target)
if (!handler) return { ran: false }
if (declaredFunctions(handler).indexOf(method) === -1) return { ran: false }
if (handler[method].length !== args.length) return { ran: false }
var result
try {
result = handler[method].apply(handler, args)
} catch (error) {
console.warn("ipc " + target + " " + method + " failed: " + error)
return { ran: true, output: "" }
}
return { ran: true, output: result === undefined || result === null ? "" : String(result) }
}
}