std.dbus
D-Bus integration via libdbus-1 — make synchronous method calls, read and
write properties on remote objects, and subscribe to signals. Available on an
optional build probe: if libdbus-1 was not present at build time the
whole module throws UNSUPPORTED when a function is called (see
Availability).
std.dbus talks to either the session or system bus. Positionallly
the bus is the first argument to every function: "session" (your desktop
login session) or "system" (machine-wide, e.g. network manager, udev).
Every function also accepts a single options object with named fields,
where bus defaults to "session":
var names = std.dbus.call(
"session", "org.freedesktop.DBus", "/org/freedesktop/DBus",
"org.freedesktop.DBus", "ListNames")
print(names) # e.g. [org.freedesktop.DBus, org.mpris.MediaPlayer2..., ...]
# Same call as an options object — bus defaults to "session".
var names2 = std.dbus.call({dest: "org.freedesktop.DBus",
path: "/org/freedesktop/DBus", iface: "org.freedesktop.DBus",
method: "ListNames"})
Reference
std.dbus.call(bus, dest, path, iface, method[, args...]) -> value
std.dbus.call({dest, path, iface, method[, bus, args]}) -> value
std.dbus.get_property(bus, dest, path, iface, name) -> value
std.dbus.get_property({dest, path, iface, name[, bus]}) -> value
std.dbus.set_property(bus, dest, path, iface, name, value) -> nil
std.dbus.set_property({dest, path, iface, name, value[, bus]}) -> nil
std.dbus.subscribe(bus, sender, iface, signal, path, fn) -> unsub
std.dbus.subscribe({iface, signal, path, callback[, bus, sender]}) -> unsub
Arguments:
bus—"session"or"system". Omitted in the options-object form,
where it defaults to"session".dest— the well-known bus name, e.g.org.mpris.MediaPlayer2.firefox.path— the object path, e.g./org/mpris/MediaPlayer2.iface— the interface, e.g.org.mpris.MediaPlayer2.Player.name/method/signal— the D-Bus property / method / signal name.callback(options-objectsubscribeonly) — the handler function. Named
callbackbecausefnis the function-literal keyword and cannot be an
object key.
std.dbus.call — invoke a method
std.dbus.call(bus, dest, path, iface, method, ...) sends a synchronous
(blocking) method call and returns the reply value. Trailing arguments become
the method’s parameters.
# Ask the bus daemon which services are currently registered.
var names = std.dbus.call(
"session", "org.freedesktop.DBus", "/org/freedesktop/DBus",
"org.freedesktop.DBus", "ListNames")
for (n: names) {
print(n)
}
Reply shape
The return value mirrors the D-Bus reply signature:
| D-Bus type | Script value |
|---|---|
s, o, g |
string |
b |
boolean |
i, u |
integer |
x, t |
long |
d |
double |
ay / array of T |
array |
a{sv} |
object (keys → values, variants unwrapped) |
| struct | array |
no reply / void |
nil |
Variants are unwrapped automatically, so a reply of v containing a string
comes back as a plain string:
var status = std.dbus.call(
"session", "org.mpris.MediaPlayer2.firefox.instance_1_21",
"/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get",
"org.mpris.MediaPlayer2.Player", "PlaybackStatus")
print(status) # "Paused" (no variant wrapper; it's a plain string)
Object paths (o) read back as strings, which is why an a{oa{sv}} (e.g.
oFono’s GetModems) becomes an object keyed by path.
Errors
A failed call throws a RUNTIME_ERROR with the service’s error message:
RUNTIME_ERROR: line 4: dbus method call failed: Property "PlaybackStatus" is not writable
std.dbus.get_property — read a property
std.dbus.get_property(bus, dest, path, iface, name) reads one property via
org.freedesktop.DBus.Properties.Get and returns the variant-unwrapped value.
var volume = std.dbus.get_property(
"session", "org.mpris.MediaPlayer2.firefox.instance_1_21",
"/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", "Volume")
print(volume) # e.g. 0.5
Complex properties come back as nested objects/arrays. For example a MPRIS
Metadata (a{sv}) reads as an object, and a bus-level NameOwnerChanged
arg1 is a string:
var meta = std.dbus.get_property(
"session", "org.mpris.MediaPlayer2.firefox.instance_1_21",
"/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", "Metadata")
print(std.json.stringify(meta, 2)) # { "xesam:title": "…", "mpris:artUrl": "…", … }
std.dbus.set_property — write a property
std.dbus.set_property(bus, dest, path, iface, name, value) writes one
property via org.freedesktop.DBus.Properties.Set. The value is wrapped in a
variant automatically. Returns nil.
std.dbus.set_property(
"session", "org.mpris.MediaPlayer2.firefox.instance_1_21",
"/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player",
"PlaybackStatus", "Paused")
The written value must match the property’s declared type; a mismatch, or a
read-only property, is reported as an error from the service.
std.dbus.subscribe — listen for signals
std.dbus.subscribe(bus, sender, iface, signal, path, fn) registers a handler
for a D-Bus signal and returns a function that removes the subscription
when called.
The handler receives a single object with one key per arg in the signal:
var unsub = std.dbus.subscribe(
"session", "org.mpris.MediaPlayer2.firefox.instance_1_21",
"org.freedesktop.DBus.Properties", "PropertiesChanged",
"/org/mpris/MediaPlayer2", fn(ev) {
print("changed:", ev.arg1) # a{sv} of changed properties
})
std.dbus.call("session", "org.mpris.MediaPlayer2.firefox.instance_1_21",
"/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", "PlayPause")
std.async.sleep(2000) # keep the script alive so the signal can arrive: exiting would drop the subscription with it
unsub() # stop listening
Key points:
- Sender / interface / path filter: pass
""forsenderor omit
matching to relax the filter (the object path is always matched). - Each subscription runs on its own private connection and dispatch
thread, so signals never interfere with your synchronous calls. - Signals are only delivered while the interpreter is alive — delivery
itself is automatic (each subscription pumps its own private connection),
but the script must stay alive withstd.async.sleep(...)(orawait)
or the process exits and delivery stops with it. arg0,argN, indexed from zero, match the signal’s declared
parameters. The handler object has no extra metadata.
For MPRIS you subscribe on the org.freedesktop.DBus.Properties interface
(this is where PropertiesChanged is emitted), not on the player interface.
Example: control a media player
var player = "org.mpris.MediaPlayer2.firefox.instance_1_21"
var path = "/org/mpris/MediaPlayer2"
var status = std.dbus.get_property(
"session", player, path, "org.mpris.MediaPlayer2.Player", "PlaybackStatus")
print("currently:", status)
std.dbus.call("session", player, path,
"org.mpris.MediaPlayer2.Player", "PlayPause")
# React to changes while we run (options-object form: bus and sender
# default to "session" and any-sender).
var unsub = std.dbus.subscribe({iface: "org.freedesktop.DBus.Properties",
signal: "PropertiesChanged", path: path,
callback: fn(ev) { print("changed:", ev.arg1) }})
std.async.sleep(3000) # keep the script alive or the subscription dies with it
unsub()
Availability
std.dbus is built in whenever libdbus-1 is detected at configure time
(probed through pkg-config, versioned include dir dbus-1.0 handled
automatically). The dbus member of std is always present, so scripts
that reference it still parse and load on a build without D-Bus. Each function
throws UNSUPPORTED on the first call instead of failing at load time:
RUNTIME_ERROR: dbus.call() requires libdbus-1, which was not available at build time
This lets the same script run — with clear, debuggable errors — whether or not
the interpreter it runs under was built with D-Bus support.
See also
- std.os — process-level helpers on
std - std.async —
sleep/spawnto pump signal delivery - std.string —
starts_with,contains, and other string helpers - std.json — inspect complex
a{sv}replies