A Wayland compositor written in Haskell, providing a configurable and programmable window management system. This project implements the TinyWL reference compositor with Haskell bindings, allowing for dynamic configuration and control through Haskell.
C
72
101 commits
updated Oct 26, 2025
A Wayland compositor written in Haskell, providing a configurable and programmable window management system. This project implements the TinyWL reference compositor with Haskell bindings, allowing for dynamic configuration and control through Haskell.
The app can be configured in Config.hs
More features coming, the intention is to get feature parity with something like sway or xmonad

The system is organized in multiple layers, providing a clean separation between Haskell control logic and low-level Wayland functionality:
graph TB
subgraph "Haskell Layer"
A[Haskell Main]
B[TinyWL Server]
H[Keybinding Handler]
end
subgraph "Bindings Layer"
C[wlhs - Wayland Haskell Bindings]
end
subgraph "C Layer"
D[TinyWL C Server]
I[Key Event Handler]
end
subgraph "System Layer"
E[wlroots]
F[Wayland Protocol]
G[Linux Kernel / Display Server]
end
A -->|starts| B
B -->|uses| C
B -->|configures| H
H <-->|callback| I
B -->|FFI calls| D
C -->|binds to| E
D -->|uses| E
D -->|dispatches to| I
E -->|implements| F
F -->|interacts with| G
classDef haskell fill:#f9f,stroke:#333,stroke-width:2px;
classDef c fill:#9cf,stroke:#333,stroke-width:2px;
classDef system fill:#fcf,stroke:#333,stroke-width:2px;
class A,B,H haskell;
class C,D,I c;
class E,F,G system;
git clone --recurse-submodules https://github.com/l-Shane-l/tiny-wlhs.git
nix-shell # Or use direnv: direnv allow
cabal run
Default key bindings:
Mod + Left Click: Move windowMod + Right Click: Resize windowMod + Esc or Alt + C: Close serverMod + s: Open terminal (default: kitty)Mod + d, Mod + v, or Mod + l: Cycle between windowsMod + a: Launch bemenu-runThe compositor is configured through the Config type in Config.hs:
data Config = Config
{ logLevel :: WLR_log_importance
, modKey :: Modifier
, onStartup :: IO ()
, onKeyPress :: State -> CUInt -> IO ()
, startupApplication :: String
}
data State = State
{ display :: Ptr WlDisplay
, server :: Ptr TinyWLServer
}
Configuration is done in Main.hs. Here's an example configuration:
appConfig :: Config
appConfig = Config
{ logLevel = WLR_DEBUG -- WLR_INFO | WLR_DEBUG | WLR_SILENT | WLR_ERROR
, startupApplication = "" -- can be any app that works with wayland
, onStartup = handleStartup
, onKeyPress = handleKeyPress
, modKey = ModAlt -- ModAlt | ModCtrl | ModLogo | ModShift
}
where
terminalEmulator = "kitty" -- Terminal emulator choice
Configure startup applications in the handleStartup function:
handleStartup = do
startUpProcess [ ("yambar", [])
, ("swaybg", ["-i", "./images/haskell.png", "-m", "fill"])
]
Key bindings are configured in the handleKeyPress function. Example:
handleKeyPress state sym = do
wlr_log WLR_INFO $ "Handler called with sym: " ++ show sym
-- Spawn terminal with Mod + s
when (sym == keySymToInt KEY_s) $ do
wlr_log WLR_INFO "Mod + s pressed, spawning a terminal emulator"
_ <- spawnProcess terminalEmulator []
pure ()
-- Launch bemenu with Mod + a
when (sym == keySymToInt KEY_a) $ do
wlr_log WLR_INFO "Mod + a pressed, running beMenu"
_ <- spawnProcess "bemenu-run"
[ "-i", "-l", "10", "-p", "run:"
, "--tb", "#285577", "--tf", "#ffffff"
, "--fb", "#222222", "--ff", "#ffffff"
, "--nb", "#222222", "--nf", "#888888"
, "--hb", "#285577", "--hf", "#ffffff"
, "--fn", "monospace 12"
, "-W", terminalEmulator ++ " -e"
]
pure ()
-- Cycle windows with Mod + d, v, or l
when (sym == keySymToInt KEY_d ||
sym == keySymToInt KEY_v ||
sym == keySymToInt KEY_l) $ do
wlr_log WLR_INFO "Mod + d pressed, cycling windows"
result <- cycleWindows state
if result
then wlr_log WLR_INFO "window cycled"
else wlr_log WLR_INFO "Window cycling failed, Only one window"
pure ()
See TODO.md for current development status and planned features.
While the project is in early stages, contributions are welcome. See CONTRIBUTING.md for guidelines.
This project is licensed under LICENSE
For questions or suggestions:
C
74.2%
Haskell
21.1%
Nix
3.9%
A Wayland compositor written in Haskell, providing a configurable and programmable window management system. This project implements the TinyWL reference compositor with Haskell bindings, allowing for dynamic configuration and control through Haskell.
C
72
101 commits
updated Oct 26, 2025
A Wayland compositor written in Haskell, providing a configurable and programmable window management system. This project implements the TinyWL reference compositor with Haskell bindings, allowing for dynamic configuration and control through Haskell.
The app can be configured in Config.hs
More features coming, the intention is to get feature parity with something like sway or xmonad

The system is organized in multiple layers, providing a clean separation between Haskell control logic and low-level Wayland functionality:
graph TB
subgraph "Haskell Layer"
A[Haskell Main]
B[TinyWL Server]
H[Keybinding Handler]
end
subgraph "Bindings Layer"
C[wlhs - Wayland Haskell Bindings]
end
subgraph "C Layer"
D[TinyWL C Server]
I[Key Event Handler]
end
subgraph "System Layer"
E[wlroots]
F[Wayland Protocol]
G[Linux Kernel / Display Server]
end
A -->|starts| B
B -->|uses| C
B -->|configures| H
H <-->|callback| I
B -->|FFI calls| D
C -->|binds to| E
D -->|uses| E
D -->|dispatches to| I
E -->|implements| F
F -->|interacts with| G
classDef haskell fill:#f9f,stroke:#333,stroke-width:2px;
classDef c fill:#9cf,stroke:#333,stroke-width:2px;
classDef system fill:#fcf,stroke:#333,stroke-width:2px;
class A,B,H haskell;
class C,D,I c;
class E,F,G system;
git clone --recurse-submodules https://github.com/l-Shane-l/tiny-wlhs.git
nix-shell # Or use direnv: direnv allow
cabal run
Default key bindings:
Mod + Left Click: Move windowMod + Right Click: Resize windowMod + Esc or Alt + C: Close serverMod + s: Open terminal (default: kitty)Mod + d, Mod + v, or Mod + l: Cycle between windowsMod + a: Launch bemenu-runThe compositor is configured through the Config type in Config.hs:
data Config = Config
{ logLevel :: WLR_log_importance
, modKey :: Modifier
, onStartup :: IO ()
, onKeyPress :: State -> CUInt -> IO ()
, startupApplication :: String
}
data State = State
{ display :: Ptr WlDisplay
, server :: Ptr TinyWLServer
}
Configuration is done in Main.hs. Here's an example configuration:
appConfig :: Config
appConfig = Config
{ logLevel = WLR_DEBUG -- WLR_INFO | WLR_DEBUG | WLR_SILENT | WLR_ERROR
, startupApplication = "" -- can be any app that works with wayland
, onStartup = handleStartup
, onKeyPress = handleKeyPress
, modKey = ModAlt -- ModAlt | ModCtrl | ModLogo | ModShift
}
where
terminalEmulator = "kitty" -- Terminal emulator choice
Configure startup applications in the handleStartup function:
handleStartup = do
startUpProcess [ ("yambar", [])
, ("swaybg", ["-i", "./images/haskell.png", "-m", "fill"])
]
Key bindings are configured in the handleKeyPress function. Example:
handleKeyPress state sym = do
wlr_log WLR_INFO $ "Handler called with sym: " ++ show sym
-- Spawn terminal with Mod + s
when (sym == keySymToInt KEY_s) $ do
wlr_log WLR_INFO "Mod + s pressed, spawning a terminal emulator"
_ <- spawnProcess terminalEmulator []
pure ()
-- Launch bemenu with Mod + a
when (sym == keySymToInt KEY_a) $ do
wlr_log WLR_INFO "Mod + a pressed, running beMenu"
_ <- spawnProcess "bemenu-run"
[ "-i", "-l", "10", "-p", "run:"
, "--tb", "#285577", "--tf", "#ffffff"
, "--fb", "#222222", "--ff", "#ffffff"
, "--nb", "#222222", "--nf", "#888888"
, "--hb", "#285577", "--hf", "#ffffff"
, "--fn", "monospace 12"
, "-W", terminalEmulator ++ " -e"
]
pure ()
-- Cycle windows with Mod + d, v, or l
when (sym == keySymToInt KEY_d ||
sym == keySymToInt KEY_v ||
sym == keySymToInt KEY_l) $ do
wlr_log WLR_INFO "Mod + d pressed, cycling windows"
result <- cycleWindows state
if result
then wlr_log WLR_INFO "window cycled"
else wlr_log WLR_INFO "Window cycling failed, Only one window"
pure ()
See TODO.md for current development status and planned features.
While the project is in early stages, contributions are welcome. See CONTRIBUTING.md for guidelines.
This project is licensed under LICENSE
For questions or suggestions:
C
74.2%
Haskell
21.1%
Nix
3.9%