diff options
Diffstat (limited to 'src/content/posts/miracle_plugins.md')
| -rw-r--r-- | src/content/posts/miracle_plugins.md | 406 |
1 files changed, 406 insertions, 0 deletions
diff --git a/src/content/posts/miracle_plugins.md b/src/content/posts/miracle_plugins.md new file mode 100644 index 0000000..0b8ec05 --- /dev/null +++ b/src/content/posts/miracle_plugins.md @@ -0,0 +1,406 @@ +--- +title: "Creating a WebAssembly Plugin System for Window Management (and beyond) in Miracle" +date: "2026-03-24" +tags: ["tech", "wayland", "miracle"] +--- + +Nothing quite makes you feel like you're using an exciting technology like a +plugin system. In fact, I would argue that this is one of the greatest draws of +the web today, at least from a business perspective. +Browsers provide a sandbox that runs JavaScript, HTML, CSS, and more. Developers +build apps using these technologies that the browser downloads and +executes safely. The entire program is delivered to the end-user on demand +within the confines of the browser. + +Extensibility is a large part of what makes the modern web useful for businesses +and tinkerers alike. In my opinion, it is also a large part of what makes +software exciting in general, as it provides a point of entry for hobbyists to +craft a system to their liking. + +## The State of Pluggable Window Management in Wayland + +Ever since the transition from X11 to Wayland, the Linux desktop has lost the +ability to make window management pluggable. Different +window managers and desktops have explored ways of accomplishing this, each with pros and +cons. Hyprland has a [plugin system](https://hypr.land/plugins/) that +dynamically loads and executes `.so` files. GNOME has an [extension +system](https://extensions.gnome.org/) +where users write extensions in JavaScript. River has introduced a [new +Wayland protocol](https://isaacfreund.com/blog/river-window-management/) for +window management. Lots of ideas are floating around, but this is definitely +a place that is still being explored ðŸ”. + +In my opinion, the ideal version of pluggable window management in Wayland would have +the following properties: + +- **Lightweight runtime**: running plugins does not slow down the system +- **Secure**: plugins are only provided with the minimal amount of information that + they need to do something useful +- **Synchronous**: async makes everything harder +- **Thread safe**: having to reason about thread safety makes everything hard +- **Language agnostic**: being tied to some weird scripting language makes everyone sad +- **Window manager agnostic**: it would be nice if the plugin worked across compositors + +Without going into too much detail, each of the solutions mentioned previously +makes some tradeoff between these considerations. You can read more +about what I view as the pros and cons of each solution [below](#ps-survey-of-other-approaches). + +## Miracle's WebAssembly Plugin System + +Miracle's new plugin system is based on <u>WebAssembly</u>. WebAssembly is +a lightweight, bytecode language that can be embedded in multiple contexts. +While it began in the web, it is truly an agnostic runtime that has utility +everywhere from embedded devices, to browsers, and now desktops. + +By using WebAssembly, we've created a plugin environment that very nearly meets +all of the criteria of my ideal plugin environment, minus the "window manager +agnostic" property. As I said previously, every solution will have some tradeoff 😅. + +Here's how it all works. Miracle runs a WebAssembly bytecode engine inside of it +(we're using [WasmEdge](https://wasmedge.org/) specifically). At runtime, Miracle loads the +plugins (`.wasm` files) that were specified by the user. Miracle then looks for +special function signatures exposed by the WebAssembly bytecode and calls them +at the appropriate times. While running, the plugin may access information about Miracle +via thread-safe methods that Miracle explicitly exposes to the plugin. Additionally, +plugins can use [WASI](https://wasi.dev/) to access standard services from the +host. In this way, plugins run confined in the WebAssembly bytecode engine with +just the level of access that they require. And because WebAssembly is just +bytecode, the host can execute and run it with near-native speed. + +Plugin authors can write plugins for Miracle in any language that +supports WebAssembly as a compilation target. The Miracle project will support an idiomatic +Rust crate for writing plugins for Miracle, which you can find +here: https://docs.miracle-wm.org/miracle_plugin/ 🦀. This crate abstracts away +the tricky communication-layer between Miracle and the WebAssembly plugin. If plugin +authors wish to write a plugin in another language, they can use the Rust implementation +as a reference. + +The plugin interface has a pleasant design. Whenever an event happens on a +Miracle object, the plugin is prompted to take some action on an event. For example, here +is a handful of events that the plugin can handle: + +- Placing, resizing, or transforming a window +- Focusing a window +- Requesting a workspace +- Handling a keyboard input event +- Handling a pointer input event +- Handling an animation update (window opening, window closing, window + moving, workspace switching, etc.) +- and more! + +If the plugin does not want to handle the event, they can simply not do so and let +Miracle (or the next plugin) handle it instead. + +Plugins will also be notified when an event happens, such as: + +- Window focused +- Window unfocused +- Window deleted +- Workspace created +- Workspace focused +- Workspace deleted +- and more! + +The plugin need not respond to any of these events, but they could take some action if they choose +to. For example, a plugin may focus a window on the appropriate workspace when the workspace changes. + +If you're interested in checking out an example usage of this API, +the best thus far is my [niri](https://github.com/niri-wm/niri) clone, aptly +named [miri](https://github.com/miracle-wm-org/miri-plugin). While this is in no +way a full Niri clone (yet!), it demonstrates the core semantics of the API, and +just how easy it is to use. Here is a little snippet of the window +management that goes on in Miri, with a bunch of details commented out: + +```rust +// ... + +impl Plugin for Miri { + fn place_new_window(&mut self, info: &WindowInfo) -> Option<Placement> { + if info.window_type != WindowType::Normal && info.window_type != WindowType::Freestyle { + return None; + } + + if info.state == WindowState::Attached { + return None; + } + + // ... commented out implementation details... + + Some(Placement::Freestyle(FreestylePlacement { + top_left: Point::new(rect.x, rect.y), + depth_layer: DepthLayer::Application, + workspace: None, + size: Size::new(rect.width, rect.height), + transform: Mat4::IDENTITY, + alpha: 1.0, + movable: false, + resizable: false, + })) + } + + fn window_deleted(&mut self, info: &WindowInfo) { + // .. remove the window and scroll to a new window if its in focus ... + } + + fn window_focused(&mut self, info: &WindowInfo) { + // ... scroll the new window into focus ... + } + + fn workspace_created(&mut self, workspace: &Workspace) { + // ... set the workspace up with empty data ... + } + + fn workspace_removed(&mut self, workspace: &Workspace) { + // ... re-home windows on that workspace ... + } + + fn workspace_focused(&mut self, _previous_id: Option<u64>, current: &Workspace) { + // ... see if we should focus another window ... + } + + fn workspace_area_changed(&mut self, workspace: &Workspace) { + // ... change the size of existing windows ... + } + + fn handle_keyboard_input(&mut self, event: KeyboardEvent) -> bool { + // ... handle Niri-specific keyboard inputs ... + } + + // ... and much more...! +} + +``` + +Miri will continue to be developed over the coming months 😄. + +But Miracle doesn't just offer window management via its plugin system. +One of the things that I always dislike about window managers is that I have to +configure them using some clunky file format. This format is good up to a point, +but it soon happens that my configuration is quite complex. What I _really_ want +in this situation is a <u>real</u> programming language. So that's just what I +added to Miracle! Instead of configuring your window manager in YAML, you can +now configure it in Rust 🦀. Here is a real example from my [dotfiles](https://github.com/mattkae/dotfiles/blob/master/config/miracle-wm/matts-config/src/lib.rs): + +```rust +impl Plugin for MyPlugin { + fn configure(&mut self) -> Option<Configuration> { + let mut config: Configuration = Configuration::default(); + + config.primary_modifier = Some(miracle_plugin::Modifier::Meta); + + let mut custom_actions: Vec<CustomKeyAction> = vec![]; + custom_actions.push(CustomKeyAction { + action: miracle_plugin::BindingAction::Down, + key: Key("d".to_string()), + modifiers: vec![Modifier::Primary], + command: "wofi --show=drun".to_string(), + }); + custom_actions.push(CustomKeyAction { + action: miracle_plugin::BindingAction::Down, + key: Key("S".to_string()), + modifiers: vec![Modifier::Primary], + command: "grimshot copy area".to_string(), + }); + custom_actions.push(CustomKeyAction { + action: miracle_plugin::BindingAction::Down, + key: Key("L".to_string()), + modifiers: vec![Modifier::Primary], + command: "swaylock".to_string(), + }); + custom_actions.push(CustomKeyAction { + action: miracle_plugin::BindingAction::Down, + key: Key("XF86AudioLowerVolume".to_string()), + modifiers: vec![], + command: "pamixer -d 10".to_string(), + }); + custom_actions.push(CustomKeyAction { + action: miracle_plugin::BindingAction::Down, + key: Key("XF86AudioRaiseVolume".to_string()), + modifiers: vec![], + command: "pamixer -i 10".to_string(), + }); + custom_actions.push(CustomKeyAction { + action: miracle_plugin::BindingAction::Down, + key: Key("XF86AudioMute".to_string()), + modifiers: vec![], + command: "pamixer -t".to_string(), + }); + custom_actions.push(CustomKeyAction { + action: miracle_plugin::BindingAction::Down, + key: Key("XF86MonBrightnessDown".to_string()), + modifiers: vec![], + command: "brightnessctl s 100-".to_string(), + }); + custom_actions.push(CustomKeyAction { + action: miracle_plugin::BindingAction::Down, + key: Key("XF86MonBrightnessUp".to_string()), + modifiers: vec![], + command: "brightnessctl s +100".to_string(), + }); + custom_actions.push(CustomKeyAction { + action: miracle_plugin::BindingAction::Down, + key: Key("E".to_string()), + modifiers: vec![Modifier::Primary], + command: "wlogout --protocol layer-shell".to_string(), + }); + custom_actions.push(CustomKeyAction { + action: miracle_plugin::BindingAction::Down, + key: Key("M".to_string()), + modifiers: vec![Modifier::Primary], + command: "miraclemsg workspace music".to_string(), + }); + + config.custom_key_actions = Some(custom_actions); + config.inner_gaps = Some(Gaps { x: 16, y: 16 }); + config.outer_gaps = Some(Gaps { x: 8, y: 8 }); + + let mut startup_apps: Vec<StartupApp> = vec![]; + startup_apps.push(StartupApp { + command: "waybar".to_string(), + restart_on_death: false, + no_startup_id: false, + should_halt_compositor_on_death: false, + in_systemd_scope: false, + }); + startup_apps.push(StartupApp { + command: "~/.local/bin/launch-swaybg.sh".to_string(), + restart_on_death: false, + no_startup_id: false, + should_halt_compositor_on_death: false, + in_systemd_scope: false, + }); + startup_apps.push(StartupApp { + command: "swaync".to_string(), + restart_on_death: false, + no_startup_id: false, + should_halt_compositor_on_death: false, + in_systemd_scope: false, + }); + config.startup_apps = Some(startup_apps); + + config.terminal = Some("kitty".to_string()); + + config.resize_jump = Some(50); + config.border = Some(BorderConfig { + size: 2, + radius: 4.0, + color: "0xbd93f9ff".to_string(), + focus_color: "0x50fa7bff".to_string(), + }); + + Some(config) + } +} +``` + +I love using a real programming language to configure. It makes me happy. + +All this being said, I think that the WebAssembly solution provides a solid +middleground for all of my ideals: + +- ✅ **Lightweight runtime**: WebAssembly is lightweight by design +- ✅ **Secure**: plugins are given only what they need from Miracle over the interface +- ✅ **Synchronous**: Miracle directly calls the plugin functions when it needs to +- ✅ **Thread safe**: Miracle guarantees thread-safe execution +- ✅ **Language agnostic**: WebAssembly is a compilation target for many languages (especially Rust) +- â›” **Window manager agnostic**: this is super-duper Miracle specific + +I am excited about the work that is being done in this area right now, especially +River's implementation. Perhaps one day I will implement it myself in Miracle, but +I plan to focus mostly on Miracle's WASM plugin system in the near future. + +## What's next? + +The plugin system will be released in Miracle 0.9.0, which should be published later this week. +Miri will receive an official version afterward. I am excited to see what you all +build with the new API. I will be around to offer support for the plugins that you build +on the [matrix channel](https://matrix.to/#/#miracle-wm:matrix.org). + +As you may be well aware, window management is only half the part of the desktop +story. The other part is the <u>shell</u>, which includes bars, +launchers, and a whole bunch of other stuff. My idea for this is to let Wayland clients +speak with plugins over the `MIRACLESOCK` (i.e. `SWAYSOCK`) +that Mir already exposes to clients like `waybar`. In this way, the window manager +will be able to tell clients about events so that the shell can update accordingly. +This bit is yet to be designed yet. + +Miracle is turning into so much more than just the tiling window manager that I originally +designed it to be. The plugin system is a culmination of a lot of my thinking over the past +couple years, and I am very excited to get it into people's hands. Happy coding! + +## Resources + +- https://github.com/miracle-wm-org/miracle-wm - miracle-wm +- https://docs.miracle-wm.org/miracle_plugin/ - Miracle's plugin API +- https://github.com/miracle-wm-org/miri-plugin - Miri plugin for Miracle +- https://github.com/mattkae/dotfiles - My dotfiles with my custom plugin + +## P.S.: Survey of Other Approaches + +Below are my thoughts weighing the pros and cons of existing solutions. +Considering each of them guided much of my thinking as a designed Miracle's +system, so it is worthwhile noting them here. I still think that River's +solution is the best of the existing bunch, if only for its interest in being +cross-compositor. + +### Dynamically Loading Shared Libraries + +This approach has been taken by Hyprland. + +**Pros**: + +- Lightweight runtime\*: the plugin code runs the same as the native code. +- _Maximal control_: the plugin can do whatever is allowed on the system, + including drawing complex graphics, reaching out to the internet, and more. +- _Common interface_: plugins are presumably programmed against the same + interface as the rest of the program + +**Cons**: + +- _Security_: you must trust the plugin author as much as you trust the author + of the host program +- _Language Lock-In_: plugins must typically use the language supported by the + API, although it is feasible for other bindings to be written on top of this. + +### Scripting Language + +This approach is common to GNOME and KDE, and is usually accomplished with +JavaScript, Lua, or some other scripting language embedded in the desktop. + +**Pros**: + +- _Security_: theoretically, the JavaScript can be sandboxed such that it has + minimal access ot the rest of the program. +- _Ease of use_: the general audience typically has a lot of exposure to + JavaScript and othe scripting languages, making it easy + +**Cons**: + +- _Language Lock-In_: it is typically infeasible to author plugins in a language + that is not provided by the host program, unless you're willing to do a LOT of work. +- _Heavy Runtime_: the host must ship an entire interpreter and runtime for the + given scripting language inside of it. + +### Wayland Protocol + +This is the approach recently suggested by the River project, which I find very +interesting. + +**Pros**: + +- _Window Manager Agnostic_: any window manager can implement the protocol, + making it a true global solution. +- _Language Agnostic_: Wayland clients can be written in any language, meaning + that window manager can too! +- _Security_: the window manager client has the same permissions as any othe + client running on the system, while remaining outside of the priveleged + compositor, who knows about things like input events, PIDs, etc. +- _Medium control_: the compositor can access most of what it is interested in, + minus the secret bits + +**Cons**: + +- _Async_: the Wayland protocol is asynchronous by nature, making frame-perfect + management difficult (although the author of River is doing a great job here). + +I actually think this is the best solution of the bunch thus far. |
