8 Getting Started
dxtr edited this page 2026-10-18 15:09:28 +02:00

Getting Started

Requirements

  • SBCL.
  • libwayland-client 1.20 or newer, the shared library only. On Debian/Ubuntu this is libwayland-client0. cl-wl/cursor also needs libwayland-cursor, which comes with it (libwayland-cursor0).
  • A running Wayland compositor, found through $WAYLAND_DISPLAY. The test compositor needs none; see Testing with the Test Compositor.

No C compiler, Wayland headers or wayland-scanner is needed. The bindings are generated ahead of time and committed.

Installing

cl-wl is not in Quicklisp. With stift, require it in your project's stiftfile:

stift get https://fakehacker.cc/dxtr/cl-wl.git

cl-wl's own stiftfile requires the one library cl-wl depends on, cl-ffi, which is not in Quicklisp either, so it comes along as an indirect entry. cl-wl/test-compositor also needs cl-threads, which comes along the same way, and libwayland-server.

Without stift, clone the repositories somewhere ASDF looks, such as ~/common-lisp/.

Loading

Load the system for each protocol you use. cl-wl includes the core protocol. The others each load cl-wl:

(asdf:load-system :cl-wl)                  ; core protocol only
(asdf:load-system :cl-wl/xdg-shell)        ; + windows
(asdf:load-system :cl-wl/xdg-decoration)   ; + server-side decorations
(asdf:load-system :cl-wl/viewporter)
(asdf:load-system :cl-wl/fractional-scale)
(asdf:load-system :cl-wl/cursor-shape)     ; also loads cl-wl/tablet
(asdf:load-system :cl-wl/text-input)
(asdf:load-system :cl-wl/pointer-constraints)
(asdf:load-system :cl-wl/relative-pointer)
(asdf:load-system :cl-wl/xdg-dialog)
(asdf:load-system :cl-wl/cursor)           ; cursor themes, through libwayland-cursor

Packages

Package Holds
cl-wl The runtime: connect, bind, dispatch, set-event-handlers, destroy-proxy, shared memory, conditions. Usually given the local nickname wl.
cl-wl/wayland The core protocol: wl-compositor, wl-surface-commit, wl-shm-format, …
cl-wl/xdg-shell xdg-wm-base, xdg-toplevel-set-title, …
cl-wl/xdg-decoration zxdg-decoration-manager-v1, …
cl-wl/viewporter wp-viewporter, wp-viewport-set-destination, …
cl-wl/fractional-scale wp-fractional-scale-manager-v1, …
cl-wl/tablet zwp-tablet-manager-v2, zwp-tablet-tool-v2, …
cl-wl/cursor-shape wp-cursor-shape-manager-v1, wp-cursor-shape-device-v1-set-shape, …
cl-wl/text-input zwp-text-input-manager-v3, zwp-text-input-v3-enable, …
cl-wl/pointer-constraints zwp-pointer-constraints-v1, zwp-locked-pointer-v1, …
cl-wl/relative-pointer zwp-relative-pointer-manager-v1, zwp-relative-pointer-v1, …
cl-wl/xdg-dialog xdg-wm-dialog-v1, xdg-dialog-v1-set-modal, …
cl-wl/cursor load-cursor-theme, find-cursor, cursor-image-buffer, …: not a protocol, but cursor themes.

Give each package you use a local nickname, as the example on Home does:

(defpackage #:my-client
  (:use #:cl)
  (:local-nicknames (#:wl #:cl-wl)
                    (#:wlp #:cl-wl/wayland)
                    (#:xdg #:cl-wl/xdg-shell)))

Protocol names never clash between the protocol packages, because every Wayland protocol prefixes its interface names, so you may also :use them and nickname only cl-wl, as the program below does. Do not :use cl-wl itself. Its bind, flush and display are names you will want for yourself.

A first window

A window needs pixels. The simplest source of pixels is a shared-memory buffer, wl_shm. A shared-memory object is a block of memory the client and the compositor both map. The function below fills one with a single colour and cuts a buffer from it:

(asdf:load-system :cl-wl/xdg-shell)

(defpackage #:shm-window
  (:use #:cl #:cl-wl/wayland #:cl-wl/xdg-shell)
  (:local-nicknames (#:wl #:cl-wl)))
(in-package #:shm-window)

(defun make-shm-buffer (shm width height)
  "Return a WIDTH x HEIGHT XRGB8888 wl_buffer filled with one colour."
  (let* ((memory (wl:make-shared-memory (* 4 width height)))
         (pool (wl-shm-create-pool shm (wl:shared-memory-fd memory)
                                   (wl:shared-memory-size memory)))
         (buffer (wl-shm-pool-create-buffer pool 0 width height (* 4 width) :xrgb8888)))
    (wl:write-shared-memory memory (make-array (* width height)
                                               :element-type '(unsigned-byte 32)
                                               :initial-element #xff3070c0))
    ;; The buffer keeps the pool's memory alive in the compositor.
    (wl-shm-pool-destroy pool)
    (wl:destroy-shared-memory memory)
    buffer))

(defun run (&key (seconds 5))
  (wl:with-display (display)
    (let* ((compositor (wl:bind display 'wl-compositor))
           (shm (wl:bind display 'wl-shm))
           (wm-base (wl:bind display 'xdg-wm-base))
           (surface (wl-compositor-create-surface compositor))
           (xdg-surface (xdg-wm-base-get-xdg-surface wm-base surface))
           (toplevel (xdg-surface-get-toplevel xdg-surface))
           (width 320)
           (height 240)
           (closed nil))
      ;; The compositor pings to see whether the client is alive.
      (wl:set-event-handlers wm-base
        :ping (lambda (wm-base serial) (xdg-wm-base-pong wm-base serial)))
      ;; It suggests a size (0 x 0: our choice) and asks us to close.
      (wl:set-event-handlers toplevel
        :configure (lambda (toplevel w h states)
                     (declare (ignore toplevel states))
                     (when (plusp w) (setf width w height h)))
        :close (lambda (toplevel)
                 (declare (ignore toplevel))
                 (setf closed t)))
      ;; Each configure sequence ends with xdg_surface.configure: ack it,
      ;; then draw at the size it settled on.
      (wl:set-event-handlers xdg-surface
        :configure (lambda (xdg-surface serial)
                     (xdg-surface-ack-configure xdg-surface serial)
                     (let ((buffer (make-shm-buffer shm width height)))
                       (wl:set-event-handlers buffer :release #'wl:destroy-proxy)
                       (wl-surface-attach surface buffer 0 0)
                       (wl-surface-damage-buffer surface 0 0 width height)
                       (wl-surface-commit surface))))
      (xdg-toplevel-set-title toplevel "cl-wl")
      (xdg-toplevel-set-app-id toplevel "example.shm-window")
      ;; A commit without a buffer asks for the first configure.
      (wl-surface-commit surface)
      (let ((deadline (+ (get-universal-time) seconds)))
        (loop until (or closed (> (get-universal-time) deadline))
              do (wl:dispatch display :timeout 0.1)))
      ;; Children first: the protocol requires this order.
      (dolist (object (list toplevel xdg-surface surface wm-base))
        (wl:destroy-proxy object)))))

(run)

A blue window titled "cl-wl" stays on screen until you close it or five seconds have passed.

The program shows the shape most Wayland clients have:

  1. with-display connects, and disconnects on exit.
  2. bind gets the globals the client needs: here the compositor, shared memory and the window manager base. See Globals.
  3. Requests are ordinary functions such as wl-compositor-create-surface. A request that creates an object returns its proxy.
  4. set-event-handlers says what to do with each event, such as :ping and :configure. See Requests and Events.
  5. dispatch waits for events and runs their handlers. See Event Loops.
  6. destroy-proxy destroys an object, sending its interface's destructor request when it has one. disconnect, which with-display calls, destroys whatever is left. See Lifetimes and Threads.

Where next

  • Concepts explains the vocabulary.
  • Recipes has frame callbacks, keyboards, pointers, outputs, decorations and Vulkan surfaces.
  • The documentation of every proxy class lists its events, and that of every request function lists its arguments: (documentation 'xdg-toplevel 'type), (documentation 'wl-surface-attach 'function).