Pure-clojure DBUS client
write-message) and high-level (call) APITo use the latest release, add the following to your deps.edn (Clojure CLI)
com.lambdaisland/dbus-client {:mvn/version "0.4.30"}
or add the following to your project.clj (Leiningen)
[com.lambdaisland/dbus-client "0.4.30"]
D-Bus is a widely used Interprocess Communication (IPC) mechanism providing a Remote Procedure Call (RPC) interface, which is widely used on Linux both on a system level (via the system bus), and by desktop environments and applications (via the session bus).
In Clojure it would be natural to consume something like this through its Java bindings, but the Java D-Bus library is based on reflection of Java classes and methods, which doesn't work with a dynamically types language like Clojure.
At the end of the day it's just data over the wire, something which Clojure is quite good at handling, someone just needs to implement (de)serialisation of the wire format. This library does that, as well as offering a message processing loop, and a number of utility namespaces, to be a bit more "batteries included".
The library has only one dependency (org.clojure/data.xml, for parsing
introspection results), and is Babashka compatible.
The main namespace is lambdaisland.dbus.client. We'll also load the bus
namespace, which contains wrapper for methods provided by the message bus itself.
(require
'[lambdaisland.dbus.bus :as bus]
'[lambdaisland.dbus.client :as dbus])
To get started, connect to a message bus. system-sock connects to the default
location of the system bus (namely: /run/dbus/system_bus_socket),
session-sock finds the session bus via the DBUS_SESSION_BUS_ADDRESS
environment variable.
(def client
(dbus/init-client!
(client/session-sock) ;; or (client/system-sock)
(fn [v]
;; response handler, mostly for signal handling,
;; but receives all sent to us
(println "Got reply" v))))
With a client in hand, we can send a message. A D-Bus messages takes a
"destination" (an identifier of another peer connected to the bus), a "path",
identifying a specific service endpoint or object inside the peer, and an
interface+message name available at that endpoint.
Let's first see who's connected to the bus:
(bus/list-names client)
;;=>
["org.freedesktop.DBus"
"org.freedesktop.Notifications"
"org.freedesktop.network-manager-applet"
"org.freedesktop.portal.Desktop"
"org.freedesktop.systemd1"
"org.pipewire.Telephony"
"org.gtk.vfs.Daemon"
"org.pulseaudio.Server"
"org.kde.kdeconnect"
"org.gnome.keyring"
,,,
]
Notifications sounds intesting, which paths are available there?
(dbus/ls client "org.freedesktop.Notifications" "/")
;; => ("/org")
(dbus/ls client "org.freedesktop.Notifications" "/org")
;; => ("/org/erikreider" "/org/freedesktop")
(dbus/ls client "org.freedesktop.Notifications" "/org/freedesktop")
;; => ("/org/freedesktop/Notifications")
And which interfaces and methods can we call there?
(dbus/path-info client {:destination "org.freedesktop.Notifications"
:path "/org/freedesktop/Notifications"})
;;=>
{"org.freedesktop.DBus.Properties" {,,,}
"org.freedesktop.DBus.Introspectable" {,,,}
"org.freedesktop.DBus.Peer" {,,,}
"org.freedesktop.Notifications"
{:methods
{"GetCapabilities" {:out [{:type "as", :name "result"}]},
"Notify"
{:in
[{:type "s", :name "app_name"}
{:type "u", :name "replaces_id"}
{:type "s", :name "app_icon"}
{:type "s", :name "summary"}
{:type "s", :name "body"}
{:type "as", :name "actions"}
{:type "a{sv}", :name "hints"}
{:type "i", :name "expire_timeout"}],
:out [{:type "u", :name "result"}]},
"CloseNotification" {:in [{:type "u", :name "id"}]},
"GetServerInformation"
{:out
[{:type "s", :name "name"}
{:type "s", :name "vendor"}
{:type "s", :name "version"}
{:type "s", :name "spec_version"}]}},
:signals
{"NotificationClosed"
{:args [{:type "u", :name "id"} {:type "u", :name "reason"}]},
"ActionInvoked"
{:args [{:type "u", :name "id"} {:type "s", :name "action_key"}]},
"ActivationToken"
{:args [{:type "u", :name "id"} {:type "s", :name "activation_token"}]},
"NotificationReplied"
{:args [{:type "u", :name "id"} {:type "s", :name "text"}]}}}}
Seems it implements a number of common standard interfaces (Properties,
Introspectable, Peer), but it also has a org.freedesktop.Notifications
interface, with methods like Notify and CloseNotification. Let's try to
create a desktop notification!
(dbus/call client
['org.freedesktop.Notifications/Notify
"org.freedesktop.Notifications"
"/org/freedesktop/Notifications"
"App name"
0 ; replaces_id
"" ; app_icon
"Cool summary"
"Hello from Clojure!"
[] ; actions
[] ; hints
10000]) ; timeout
;; => 127
That worked!
The vector passed to call is conceived similar to a method call, the first
element is a namespaced symbol (keyword works too) of interface/method,
followed by destination, path, and any body arguments.
call will block until a response arrives. Instead of a vector, it will also
accept a map:
{:method 'interface/method
:dest "my.dest"
:path "/my/path"
:body [body args]}
To receive signals, subscribe to them with bus/add-match
(bus/add-match client
{:type "signal"
:sender "org.freedesktop.Notifications"
:interface "org.freedesktop.Notifications"
:member "NotificationClosed"
:path "/org/freedesktop/Notifications"})
Now when the user closes a notification, the callback passed to init-client!
will be called with the following:
{:endian :LITTLE_ENDIAN,
:type :signal,
:flags {:no-reply-expected true},
:version 1,
:body-length 8,
:serial 920404,
:headers
{:path "/org/freedesktop/Notifications",
:interface "org.freedesktop.Notifications",
:signature "uu",
:member "NotificationClosed",
:sender ":1.54"},
:body [134 2]}
Which is the raw message map that we got from the message bus.
From the introspection result earlier we know how to interpret the body, namely as [id reason]
{:signals {"NotificationClosed" {:args [{:type "u", :name "id"} {:type "u", :name "reason"}]}}
Thank you! dbus-client is made possible thanks to our generous backers. Become a backer on OpenCollective so that we can continue to make dbus-client better.
dbus-client is part of a growing collection of quality Clojure libraries created and maintained by the fine folks at Gaiwan.
Pay it forward by becoming a backer on our OpenCollective, so that we continue to enjoy a thriving Clojure ecosystem.
You can find an overview of all our different projects at lambdaisland/open-source.
We warmly welcome patches to dbus-client. Please keep in mind the following:
***We would very much appreciate it if you also
We recommend opening an issue first, before opening a pull request. That way we can make sure we agree what the problem is, and discuss how best to solve it. This is especially true if you add new dependencies, or significantly increase the API surface. In cases like these we need to decide if these changes are in line with the project's goals.
* This goes for features too, a feature needs to solve a problem. State the problem it solves first, only then move on to solving it.
** Projects that have a version that starts with 0. may still see breaking changes, although we also consider the level of community adoption. The more widespread a project is, the less likely we're willing to introduce breakage. See LambdaIsland-flavored Versioning for more info.
Copyright © 2025-2026 Arne Brasseur and Contributors
Licensed under the term of the Mozilla Public License 2.0, see LICENSE.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |