[ANN] Rivulet & Wayland on Haskell

Recently, I switched to Asahi Linux on my Mac. I’ve been a Linux user on my desktop PC for years, and for the most part I’ve always used Fedora. So I thought it would be neat to experiment with the Fedora Asahi Remix! However, it uses Wayland exclusively and the Asahi team has (rightfully) said that they don’t want to work on X11 compatibility, and, frankly, good for them! The X Window System might be the most useful thing I used for years and years that I still hated every single time I had to deal with it in any capacity beyond being an end-user. So Wayland is good. But Wayland compositors work quite differently than the X server does, and I think it’s been really hard for the Haskell community to make the switch, too, in recent years. It’s no secret that we’ve always struggled a bit with GUI in Haskell, and Wayland bindings/compositing are no different lol.

The Problem of The Compositor in Haskell is that there’s barely any good bindings to wlroots or smithay… the official hsroots bindings have been abandoned for ~8 years, Rust FFI in Haskell is sort of a dead end, so smithay is out of the picture. hayland, sudbury and wayland-wire have all been dormant for years. The sole active effort is wlhs (from 2024) that started as an outgrowth of XMonad, which, while very cool, development has been paused since the primary contributor was injured (I think, correct me if I’m wrong) and the bindings are still self-described as “highly incomplete.” No standalone haskell bindings exist for xdg-shell, xdg-decoration, wlr-foreign-toplevel-management or the wayland-protocols collection. The only bright spot here is everything that’s been automated by haskell-gi, like gi-gtk4-layer-shell, but it’s deeply painful to use because you’re basically writing a bunch of imperative C in Haskell.

There have also been a couple attempts at a Haskell Wayland compositor, beginning with waymonad. But now it’s “badly bitrotted” according to the XMonad team, and it’s been abandoned for years. tiny-wlhs had a lot of interest, and it’s still early-stage … and I really hope it continues, but, y’know. Woburn attempted a more “pure” approach, but it never got much working and has been mostly dormant for years.

So What?

I recently read Issac Freund’s article Separating the Wayland Compositor and Window Manager and found it fascinating. river, a project I’ve followed for some time, has now become what he calls a “non-monolithic” Wayland compositor, allowing you to build a WM/“layout engine” in a lazy/garbage-collected language instead of C or Zig or whatever.

I think baby steps are needed in all directions. XMonad, while wonderful, seemingly will remain X11-only for the foreseeable future, and there’s just overall not been much done here. A truly Haskell-pure Wayland compositor is far away, I think. River is a great solution to this!

So over spring break, I started on building a window manager/layout engine in Haskell called Rivulet. I think DSL-style configs are great, and really wanted to use something like it on my laptop. For some time, I’ve been chained to sway (which is great, it works fine) but its config is annoying (to me) at best. I used to flip-flop back and forth between XMonad and bspwm, and so all the design decisions I’ve made so far on my own WM are informed by both of them. river makes it pretty easy to implement your own WM in a language like Haskell, as long as you implement the River protocol bindings. The river-window-management-v1 protocol is great and has been exactly what I’ve been looking for … River handles the systems-level work, like DRM/KMS, inputs, buffer management, frame timing, Xwayland, etc … and the WM/Rivulet handles the layouts, keybinds, focus, and decorations via the protocol. It’s all atomic & seperated by the IPC barrier!

Rivulet

Right now, Rivulet implements a lot of things but it’s still not really usable as a daily driver. Here’s an unfinished list of things that work:

  1. The layout engine works and you can write your own layouts with the Layout typeclass.
  2. The manage/render cycle is implemented to a somewhat-minimum specification so that River doesn’t just immediately flip out and drop the connection; although there are some things that it might still get angry about because I haven’t implemented them
  3. The config DSL is pretty much in its “final state” syntax-wise, it supports autostart & keybinding but a lot of the actions aren’t wired up yet
  4. Workspaces work and you can switch between them
  5. Layer-shell protocol half-works (wallpapers, status bars, etc)

I haven’t tested a lot of things with my current implementation (especially multi-monitor support)… off the top of my head for what DOESN’T work: fullscreening is probably broken, colored borders are broken, a lot of the DSL actions (like changing layouts or floating windows) doesn’t work, a lot of things River wants (like hints sent from the WM to windows about capabilities and the such) don’t work, resizing windows isn’t a thing yet, and so on. TL;DR: it’s early, and I’m working on it.

I have, however, forced myself to switch to using Rivulet exclusively as my WM until I feel that it’s in a very usable state. Hopefully this should help me identify anything wrong while I’m using it as my daily driver, although I’d encourage you NOT to use it as a daily driver until, like, v1.0.0. The code is also kind of a mess, there’s massive monster lambdas everywhere and it’s a little hard to read. That is something I intend to change in the future as well just by working on it naturally.

My favorite thing about this project (and the reason I decided to write it) is that it lets you write your config in pure Haskell. You can do absurd things if you want, like binding a key to run Haskell’s garbage collector. Or you can use unicode character operators in your config. Or you can be really Haskell-idiomatic and do stuff like this in the keybinds block:

mapM_ (uncurry (~>)) $ zipWith (\k a -> ([Super] # k, a))
  ['1'..'9']
  (map focusWorkspace ["I","II","III","IV","V","VI","VII","VIII","IX"])

which binds Super + 1..9 to focusing workspaces I…IX. I have also provided some syntactic sugar for this, in the form of the #* operator:

Control           #* ['1'..'9'] ~> focusWorkspace
Control <+> Shift #* ['1'..'9'] ~> sendToWorkspace

Hopefully over the next year or so Rivulet gets to be as fully-featured as something like XMonad or bspwm. I’m currently working on implementing the BinarySpacePartitioning layout and trying to fill out Rivulet to use the full river-window-management-v1 protocol spec.

You can check out the project on my GitHub and try it out! Be forewarned, the build instructions for the dependencies are a little fried… you may need a modern version of the Zig compiler and you probably need to build River from source (versions >0.4.x aren’t really in many distro repos yet.)

Also, here’s an example config if you want to try it out (be forewarned, Rivulet copies an extremely simple example config to ~/.config/rivulet/Config.hs if you don’t write one yourself):

import Rivulet

main :: IO ()
main = rivulet $ do

    gaps 20

    layout Tall

    monitor "eDP-1" ["1", "2", "3", "4", "5"] --change eDP-1 to whatever your Wayland output is called
    
    autostart $ do
        start "foot"

    keybinds $ do
        Super              #  Return       ~> spawn "foot"
        Super              #* ['1'..'5']   ~> focusWorkspace
        Super <+> Ctrl     #* ['1'..'5']   ~> sendToWorkspace
        Ctrl  <+> Alt      #  Delete       ~> exitSession
        Super              #  'q'          ~> closeFocused
        -- and so on

Let me know of your thoughts or if anything sucks about my design… I want to make this a genuinely useful WM. Cheers!

19 Likes

A friend of mine offered this funny example of what’s possible with the Rivulet config:

-- ConfigM is a monad but you refuse to use (>>=) on principle

myConfig :: ConfigM ()
myConfig = 
    gaps        <$> pure 4
                <*  borders 2 (0xFF000000, 0xFF0000FF)
                <*  layout Tall
                <*  (keybinds $
                        bind <$> pure [Super]
                             <*> pure q
                             <*> pure closeFocused)

So I got to thinking and it would be pretty funny to explore some esoteric uses of the config being just Haskell. For example,

realGaps :: Int
realGaps = unsafePerformIO $ do
    t <- getCurrentTime
    -- gaps are 4 most of the time but on Tuesdays they are 5
    let day = dayOfWeek (utctDay t)
    pure $ if day == Tuesday then 5 else 4

gaps realGaps

or keybindings as a Markov chain:

-- keybindings probabilistically select the next action
import System.Random (randomRIO)

markov :: Action
markov = do
    n <- liftIO $ randomRIO (0 :: Int, 2)
    [focusNext, focusPrev, closeFocused] !! n

There is likely some really funny things you could do with this DSL. I wonder what other silly things one could come up with, like making your entire config a Foldable instance or something.

4 Likes

what a coincidence! I’ve just release hs-wayland-scanner, and I wrote it because I didn’t want to manually write bindings to river, something you were not afraid to do! I’ll give Rivulet a try… great job!

Yesterday I was too busy cleaning up the code for the release and I’ve notice your post right after writing mine… :slight_smile:

Best regards,
andrea

2 Likes

'Tis a small world, after all. hs-wayland-scanner looks great - I, indeed, did spend a good amount of my time writing out the bindings myself. If only I had started a month later!

See if Rivulet works on your system; I’m worried that it’s still kind of a mess and a little hacky at the moment. Definitely not useful for everyday work yet, especially since I haven’t introduced the rules engine yet (i.e. “set appId Firefox to floating”)

@arossato @jackiedorland you should combine your powers and show the world that Good Things Can Happen when it comes to Haskell & Wayland. :slight_smile:

2 Likes

Shall do. I will likely keep updating this post with each release of Rivulet … hopefully in the future I can make Wayland a little more usable on the Haskell side of things!

2 Likes

Re: bindings - is GitHub - well-typed/hs-bindgen: Automatically generate Haskell bindings from C header files · GitHub good enough to handle this?

1 Like

Great! :slight_smile: If you did not know, the Haskell Foundation has a tech proposals grant programme where you can request funds & technical reviews: tech-proposals/proposals/PROPOSALS.md at main · haskellfoundation/tech-proposals · GitHub

1 Like

Oh huh, there’s dozens of us!

I’ve made some progress on directly implementing a compositor and this week pivoted to basing it on river rather than a whole compositor. You’ve got a lot further than I have.

One of the insights I’ve had is that the callback based mechanism generated by wayland-scanner does not feel very Haskell-y and forces IO everywhere. I’ve instead been building an event stream that feels more natural, and will allow unit testing the pure bits of the code.

Let me know if you’d like to collaborate or if I can help out with something.

1 Like

I am ecstatic to hear that! I agree, the callback system feels super duper gross as a Haskeller. I sort of accepted early on that everything is going to be I/O just by nature of what we’re building, but I am totally open to contributions from folks that have better ideas lol

The event stream idea sounds good. I was wondering how much of a PITA it would be to just implement the Wayland socket protocol in pure Haskell and do it that way. If you want to contribute, please be my guest! I want as many folks as possible to be in on this project… I think it’s the only way Haskell & Wayland will grow

Got cut off in my last post since my café got awfully busy… I’d also like to point that unit testing has been a nightmare for me with Rivulet. It’s a total pain to restart Rivulet/River every time I need to test something visually and very hard to write Hspec tests for. Any ideas that makes testing easier are very welcome!

I sort of accepted early on that everything is going to be I/O just by nature of what we’re building,

It’s great for a first pass, and that was my thoughts for when I was working on the direct implementation. With the building the compositor on top of River though, pretty much everything becomes messages. I think most of the window layout stuff would become Event → [Request]. We’d of course have stuff like spawning that will of course be IO.

… implement the Wayland socket protocol in pure Haskell

Yes! I was wondering about that too. It seems a lot more plausible when working with River… I was looking into the wire protocol a bit, and it seems mostly straightforward. It’s mostly a list of numbers for object and method ids. One of the complications is that it would need to be stateful to implement the new_id type.

1 Like

ooo. Sounds like we are on the same page then.

This is the mess I was going through to write implement the event stream.

Perhaps with @arossato’s help we could get it all generated, or better still: get everything in Haskell.

1 Like

Just took a quick skim through Wayland.hsc and I like it. I much much prefer the idea of doing that than managing a million raw pointers everywhere and callbacks (the way I currently do it in Rivulet/Manager/Runtime.hs)

Event -> [Request] has a pretty simplicity to it. Pattern match on the [Request], use [] to represent no change, so on. Much nicer than my approach! This project was mostlyan excuse for me to get acquainted with the WriterT monad and some lower-level Haskell than what I usually write, so there are definitely parts where my code is lacking in simplicity. Getting everything in Haskell should be a core priority of Rivulet, and eventually, I’m not opposed to writing a compositor server that conforms to the River protocol or cuts it out entirely. River is great - don’t get me wrong - but long-term, I think a Haskell-based compositor would be the best for the “cool factor.” wlroots bindings are possibly the most pressing thing in this space.

If anyone wanted to take up the rein on writing some kind of wayland wire protocol thing in Haskell, that would probably be the first step, with the River protocol afterwards. Then, we could probably work Rivulet into being almost entirely pure Haskell with little C involved, which would be quite nice. This package is ungodly levels of old and likely completely outdated, so it might be best to write it ourselves from scratch. I would not be opposed to forming a small group chat or other place for Wayland Haskell development (or just keeping it here is fine too) to discuss things.

Also, perhaps I’m not well educated on the topic, but is GHC optimized enough at the -O2 level to output code that’s suitable for a compositor at all? I fear that on high refresh rates (i.e. 240hz) there might be noticeable lag in the compositor writing it in Haskell, just because, y’know, it’s Haskell. I know with concurrency a lot of these things are possibly quite fast, but I’ve noticed there’s barely any compositors or compositor libraries written in anything other than C or Rust. And I wonder if concurrency would blow up the memory usage and complexity of a Haskell compositor as well.

I’m really curious if anyone has experience writing very high-performance applications in Haskell and what that looks like. It’s something I’ve always just delegated to Rust for. (And I’m aware that my view of Haskell performance could be incredibly naïve, which is why I’m hoping someone would straighten me out :stuck_out_tongue:)

So, with respect to backends, it seems there are two decisions to make here. First, decide on how we speak the Wayland protocol:

  1. The one-to-one translation of wayland-scanner generated code, and bound into Haskell via FFI.
  2. Event stream approach built around the wayland-scanner. This could be sped up a fair amount if we could have the stuff I’m writing manually automated. This would give us a cleaner implementation, allow unit-testing and so-forth.
  3. Event stream completely generated in Haskell.

Second, decide whether we want to be:

a. a River window manager.
b. a Wayland compositor

(3b) would of course be the nicest to have. It would allow us to do some weird things like layouts for background layers, and allow users to chose what protocols to implement. (One of the drawbacks of River is that we’d need a side-channel to pass on workspace/tag info to tools like Waybar). The disadvantage is that it is a big project. My feeling is that it would take a year or so to get to a` daily-driver level. So, I’m leaning heavily to the (a) options. River seems like a great incubation space until we’re mature enough.

(1a) is the approach you’ve taken. The major advantage here is that you’ve got something pretty close to a daily driver. In the long run I’m not too keen on this approach though, because it feels like writing C in Haskell, testing is hard etc. You seem to feel the same. What is your feeling on transitioning from your implementation to a (2a)/(3a) approach? Is doing so incrementally a possibility, or would it make sense to re-implement from scratch and pull in the code for the DSL etc later?

Down the road, I think transitioning from (3a) to (3b) should be reasonable without throwing away too much if we choose our abstractions well.

There seems to be a bunch of trial and error that needs to happen to get to (3a). Though, the event stream abstraction should isolate the protocol implement from the rest of the code base.

So, my feeling is that it would be easiest to get something working with the (2a) approach so that we have something to show as soon as possile, and then transition to (3a) once we’ve got some momentum going (relatively soon hopefully). Eventually, once we start feeling River’s limitations, we can transition to (3b).

Does that seem sensible to you?

2 Likes

I’m hardly a Haskell expert, but I’ve been extremely impressed by GHCs ability to optimize pure code, things like removing intermediate Maybes, List.
C/C++, and I’m guessing Rust struggle with this, because of things like aliasing, and not knowing whether functions are pure or now.

I actually have a friend who wrote a high-frequency trading system in Haskell around 2010, so its definitely possible :slight_smile:

1 Like

Sure, maybe we could do Discord if we’re a few people? I’ve got Telegram/Signal/WhatsApp too.

There’s a Haskell discord server that already exists, you can probably ask for a channel there :slight_smile: Let me know if you want an invitation.

Oh that would be perfect. Would appreciate it.

1 Like