diff options
| author | Josh Rahm <joshuarahm@gmail.com> | 2026-01-06 15:45:03 -0700 |
|---|---|---|
| committer | Josh Rahm <joshuarahm@gmail.com> | 2026-01-06 15:45:03 -0700 |
| commit | 09860c75bb129c70768692aaec3bd42d0b2735e3 (patch) | |
| tree | b0192d2779b26da22c169ac801b781362d10aacc | |
| parent | 68dd63f6b3de774863051b66e609a0ca4f4ac2a1 (diff) | |
| download | montis-09860c75bb129c70768692aaec3bd42d0b2735e3.tar.gz montis-09860c75bb129c70768692aaec3bd42d0b2735e3.tar.bz2 montis-09860c75bb129c70768692aaec3bd42d0b2735e3.zip | |
[reorg] add "middleware" library called "liberebor"
| -rw-r--r-- | CMakeLists.txt | 71 | ||||
| -rw-r--r-- | README.md | 75 | ||||
| -rw-r--r-- | arken/README.md | 52 | ||||
| -rw-r--r-- | arken/src/plugin.c | 61 | ||||
| -rw-r--r-- | erebor/CMakeLists.txt | 25 | ||||
| -rw-r--r-- | erebor/README.md | 32 | ||||
| -rw-r--r-- | erebor/include/util.h (renamed from arken/include/util.h) | 5 | ||||
| -rw-r--r-- | erebor/src/runtime_requests.c | 69 | ||||
| -rw-r--r-- | erebor/src/util.c (renamed from arken/src/util.c) | 0 | ||||
| -rw-r--r-- | montis/README.md | 41 | ||||
| -rw-r--r-- | montis/package.yaml | 5 |
11 files changed, 343 insertions, 93 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 95296b6..e191a24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,43 +1,52 @@ cmake_minimum_required(VERSION 3.16) project(montis LANGUAGES C) +option(MONTIS_BUILD_BUNDLED_PLUGIN "Build the bundled Haskell plugin (montis.so)" ON) + add_custom_target(wlroots_build ALL DEPENDS "${WLROOTS_LIB_LINK}") add_subdirectory(arken) add_dependencies(arken wlroots_build) -add_custom_target( - plug_build ALL - COMMAND sh -c "if [ -d \"$1\" ] && [ ! -L \"$1\" ]; then rm -rf \"$2\"; mv \"$1\" \"$2\"; fi" sh "${CMAKE_SOURCE_DIR}/montis/.stack-work" "${CMAKE_BINARY_DIR}/stack-work" - COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_BINARY_DIR}/stack-work" - COMMAND "${CMAKE_COMMAND}" -E create_symlink "${CMAKE_BINARY_DIR}/stack-work" "${CMAKE_SOURCE_DIR}/montis/.stack-work" - COMMAND "${CMAKE_COMMAND}" -E chdir "${CMAKE_SOURCE_DIR}/montis" stack build - # Not sure why stack is generating an a.out file, but remove it. - COMMAND "${CMAKE_COMMAND}" -E rm -f "${CMAKE_SOURCE_DIR}/montis/a.out" - DEPENDS arken - COMMENT "Building Haskell plugin with Stack" - VERBATIM -) - -add_custom_target( - run - COMMAND sh -c "PLUGIN_SO=$(find '${CMAKE_BINARY_DIR}/stack-work' -name montis.so -type f | head -n 1); if [ -z \"$PLUGIN_SO\" ]; then echo 'montis.so not found in ${CMAKE_BINARY_DIR}/stack-work' 1>&2; exit 1; fi; \"$<TARGET_FILE:arken>\" -s foot -p \"$PLUGIN_SO\"" - DEPENDS arken plug_build - USES_TERMINAL - VERBATIM -) +add_subdirectory(erebor) +add_dependencies(erebor wlroots_build) + +if(MONTIS_BUILD_BUNDLED_PLUGIN) + add_custom_target( + plug_build ALL + COMMAND sh -c "if [ -d \"$1\" ] && [ ! -L \"$1\" ]; then rm -rf \"$2\"; mv \"$1\" \"$2\"; fi" sh "${CMAKE_SOURCE_DIR}/montis/.stack-work" "${CMAKE_BINARY_DIR}/stack-work" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_BINARY_DIR}/stack-work" + COMMAND "${CMAKE_COMMAND}" -E create_symlink "${CMAKE_BINARY_DIR}/stack-work" "${CMAKE_SOURCE_DIR}/montis/.stack-work" + COMMAND "${CMAKE_COMMAND}" -E chdir "${CMAKE_SOURCE_DIR}/montis" stack build + # Not sure why stack is generating an a.out file, but remove it. + COMMAND "${CMAKE_COMMAND}" -E rm -f "${CMAKE_SOURCE_DIR}/montis/a.out" + DEPENDS arken erebor + COMMENT "Building Haskell plugin with Stack" + VERBATIM + ) + + add_custom_target( + run + COMMAND sh -c "PLUGIN_SO=$(find '${CMAKE_BINARY_DIR}/stack-work' -name montis.so -type f | head -n 1); if [ -z \"$PLUGIN_SO\" ]; then echo 'montis.so not found in ${CMAKE_BINARY_DIR}/stack-work' 1>&2; exit 1; fi; \"$<TARGET_FILE:arken>\" -s foot -p \"$PLUGIN_SO\"" + DEPENDS arken plug_build + USES_TERMINAL + VERBATIM + ) +endif() install(TARGETS arken RUNTIME DESTINATION bin) -install(CODE [[ - execute_process( - COMMAND sh -c "find '${CMAKE_BINARY_DIR}/stack-work' -name montis.so -type f | head -n 1" - OUTPUT_VARIABLE _montis_so - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - if(NOT _montis_so) - message(FATAL_ERROR "montis.so not found in ${CMAKE_BINARY_DIR}/stack-work") - endif() - file(INSTALL DESTINATION "${CMAKE_INSTALL_PREFIX}/lib" TYPE FILE FILES "${_montis_so}") -]]) +if(MONTIS_BUILD_BUNDLED_PLUGIN) + install(CODE [[ + execute_process( + COMMAND sh -c "find '${CMAKE_BINARY_DIR}/stack-work' -name montis.so -type f | head -n 1" + OUTPUT_VARIABLE _montis_so + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(NOT _montis_so) + message(FATAL_ERROR "montis.so not found in ${CMAKE_BINARY_DIR}/stack-work") + endif() + file(INSTALL DESTINATION "${CMAKE_INSTALL_PREFIX}/lib" TYPE FILE FILES "${_montis_so}") + ]]) +endif() @@ -43,6 +43,16 @@ Montis is not a DSL and not a configuration file. It is the window manager. Written in a Turing-complete programming language with full access to the Arken runtime and wlroots itself. +#### Erebor -- Native Bridge Library + +**Erebor** (`liberebor.a`) is a static C library that contains the native +functions Montis uses to interact with Arken/wlroots. It is linked into the +`montis.so` plugin so these FFI bindings hot-reload along with the Haskell code +instead of living inside the long-running compositor process. + +Code lives in `erebor/src` and headers in `erebor/include` (notably +`erebor/include/util.h`). + ### Why This is Powerful Montis takes direct inspiration from XMonad. @@ -87,3 +97,68 @@ The solution is a pluggable architecture: * A persistent runtime (Arken) that owns wlroots and the Wayland lifecycle * A hot-reloadable window manager (Montis) that owns behavior + +## Building + +### Prerequisites + +- `cmake` +- A C toolchain (`cc`, `ld`, etc.) +- `pkg-config` +- wlroots build deps via your distro (Wayland, xkbcommon, libinput, pixman, etc.) +- `meson` (wlroots is built via Meson) and a backend like `ninja` +- `wayland-scanner` and `wayland-protocols` +- `curl` (first build downloads wlroots) +- Optional (only for the bundled plugin): Haskell toolchain via Stack: `stack` (+ GHC) + +### Build + +This repo uses CMake to build: +- `arken` (the compositor runtime) +- `erebor` (`liberebor.a`) +- the Haskell plugin (`montis.so`) via Stack + +```sh +cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug +cmake --build build +``` + +To build only the runtime + bridge library (no bundled plugin): + +```sh +cmake -S . -B build -DMONTIS_BUILD_BUNDLED_PLUGIN=OFF +cmake --build build +``` + +To run via the CMake helper target: + +```sh +cmake --build build --target run +``` + +Note: the `run` target currently starts `arken` with `-s foot`, so you’ll want +`foot` installed (or adjust the target in `CMakeLists.txt`). + +### Using Your Own Plugin + +If you disable the bundled plugin (`-DMONTIS_BUILD_BUNDLED_PLUGIN=OFF`), you can +build and supply your own plugin `.so` at runtime. + +- Plugin ABI: `arken/include/plugin.h` (generated convenience header: `build/plugin_interface.h`) +- Optional bridge helpers: `erebor/include` + `build/erebor/liberebor.a` + +Runtime invocation looks like: + +```sh +./build/arken/arken -p /path/to/your_plugin.so +``` + +## Installing + +```sh +cmake --install build --prefix ~/.local +``` + +This installs: +- `arken` to `~/.local/bin` +- if `-DMONTIS_BUILD_BUNDLED_PLUGIN=ON`, the latest built plugin `.so` to `~/.local/lib` diff --git a/arken/README.md b/arken/README.md new file mode 100644 index 0000000..5e1ede8 --- /dev/null +++ b/arken/README.md @@ -0,0 +1,52 @@ +Arken (Runtime) +============== + +Arken is a long-running Wayland compositor runtime with a hot-reloadable plugin +interface. + +Responsibilities +---------------- + +- Owns the Wayland display lifecycle and wlroots setup. +- Loads a plugin shared object (`.so`) at runtime and routes input/surface events to it. +- Supports hot-reloading the plugin without restarting the compositor. + +Plugin interface +---------------- + +The runtime defines a C ABI that plugins must implement (load/start/teardown, +event handlers, and optional state marshal/unmarshal for hot reload). The ABI is +defined in: + +- `arken/include/plugin.h` + +At build time, a convenience header is generated from that ABI definition for +consumers that want a minimal include surface: + +- `build/plugin_interface.h` (generated) + +Key files +--------- + +- `arken/src/wl.c`: compositor setup + event loop + plugin callbacks. +- `arken/src/plugin.c`: dynamic loading, lifecycle, and hot-reload logic. +- `arken/include/plugin.h`: C ABI the plugin must implement. + +Building +-------- + +Arken is built via the top-level CMake project: + +```sh +cmake -S .. -B ../build +cmake --build ../build --target arken +``` + +Running +------- + +Use the top-level `run` target (builds the plugin and launches Arken): + +```sh +cmake --build ../build --target run +``` diff --git a/arken/src/plugin.c b/arken/src/plugin.c index 3edf486..ab02599 100644 --- a/arken/src/plugin.c +++ b/arken/src/plugin.c @@ -1,5 +1,4 @@ #include "plugin.h" -#include "wl.h" #include <ctype.h> #include <dlfcn.h> @@ -53,66 +52,6 @@ static void lock(plugin_t *plugin) { pthread_mutex_lock(&plugin->lock); }; static void unlock(plugin_t *plugin) { pthread_mutex_unlock(&plugin->lock); }; -static int plugin_hot_reload_same_state_action_(plugin_t *plugin, void *ignore) -{ - return plugin_hot_reload_same_state(plugin); -} - -void montis_do_request_hot_reload(void *plugv) -{ - plugin_t *plugin = plugv; - - size_t n = plugin->n_requested_actions++; - if (n < 8) { - plugin->requested_actions[n].action = plugin_hot_reload_same_state_action_; - plugin->requested_actions[n].arg_dtor = NULL; - } -} - -static int plugin_do_log(plugin_t *plugin, void *chrs) -{ - char *str = chrs; - puts(str); - return 0; -} - -void montis_do_request_log(void *plugv, const char *str) -{ - plugin_t *plugin = plugv; - - size_t n = plugin->n_requested_actions++; - if (n < 8) { - plugin->requested_actions[n].action = plugin_do_log; - plugin->requested_actions[n].str_arg = strdup(str); - plugin->requested_actions[n].arg_dtor = free; - } -} - -static int montis_plugin_do_exit(void *plugv, int ec) -{ - exit(ec); - return 0; -} - -void montis_do_request_exit(void *plugv, int ec) -{ - plugin_t *plugin = plugv; - - size_t n = plugin->n_requested_actions++; - if (n < 8) { - plugin->requested_actions[n].action = - (int (*)(plugin_t *, void *))montis_plugin_do_exit; - plugin->requested_actions[n].int_arg = ec; - plugin->requested_actions[n].arg_dtor = NULL; - } -} - -void *montis_plugin_get_seat(void *ctx) -{ - struct montis_server *server = wl_container_of(ctx, server, plugin); - return server->seat; -} - static int load_plugin_from_file_(int argc, char **argv, const char *filename, plugin_t *plugin) { diff --git a/erebor/CMakeLists.txt b/erebor/CMakeLists.txt new file mode 100644 index 0000000..f9aefba --- /dev/null +++ b/erebor/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.10) +project(erebor LANGUAGES C) + +set(WLROOTS_SOURCE_DIR "${CMAKE_BINARY_DIR}/wlroots-src") +set(WLROOTS_BUILD_DIR "${CMAKE_BINARY_DIR}/wlroots") + +file(GLOB_RECURSE EREBOR_SOURCES src/*.c) + +add_library(erebor STATIC ${EREBOR_SOURCES}) +set_target_properties(erebor PROPERTIES + POSITION_INDEPENDENT_CODE ON + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/erebor" +) + +target_include_directories(erebor PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/arken/include + ${CMAKE_BINARY_DIR} + ${WLROOTS_SOURCE_DIR}/include + ${WLROOTS_BUILD_DIR}/include + ${WLROOTS_BUILD_DIR}/protocol + /usr/include/pixman-1 +) + +target_compile_definitions(erebor PRIVATE WLR_USE_UNSTABLE) diff --git a/erebor/README.md b/erebor/README.md new file mode 100644 index 0000000..55d515a --- /dev/null +++ b/erebor/README.md @@ -0,0 +1,32 @@ +Erebor (Native Bridge Library) +============================== + +Erebor is a static C library (`liberebor.a`) that provides native helper +functions for a dynamically loaded plugin to interact with the runtime and +wlroots. + +Why a static library? +--------------------- + +These helpers are linked into the plugin (`.so`) so they hot-reload along with +the plugin, instead of being stuck inside the long-running runtime process. + +API surface +----------- + +Public headers live in `erebor/include`. + +- `erebor/include/util.h`: plugin-facing functions used via FFI, including: + - runtime requests (log/exit/hot-reload) + - seat access + - toplevel queries and basic positioning/geometry helpers + +Building +-------- + +Erebor is built via the top-level CMake project: + +```sh +cmake -S .. -B ../build +cmake --build ../build --target erebor +``` diff --git a/arken/include/util.h b/erebor/include/util.h index 2ed2f70..22b4020 100644 --- a/arken/include/util.h +++ b/erebor/include/util.h @@ -6,6 +6,11 @@ * are intended for direct FFI use from the Haskell plugin. */ +void montis_do_request_hot_reload(void *plugv); +void montis_do_request_log(void *plugv, const char *str); +void montis_do_request_exit(void *plugv, int ec); + +void *montis_plugin_get_seat(void *ctx); void *montis_plugin_toplevel_at(void *ctx, double lx, double ly); void montis_plugin_get_toplevel_position(void *toplevel, double *x, double *y); void montis_plugin_set_toplevel_position(void *toplevel, double x, double y); diff --git a/erebor/src/runtime_requests.c b/erebor/src/runtime_requests.c new file mode 100644 index 0000000..a5d5b88 --- /dev/null +++ b/erebor/src/runtime_requests.c @@ -0,0 +1,69 @@ +#include "util.h" +#include "wl.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static int plugin_hot_reload_same_state_action_(plugin_t *plugin, void *ignore) +{ + (void)ignore; + return plugin_hot_reload_same_state(plugin); +} + +void montis_do_request_hot_reload(void *plugv) +{ + plugin_t *plugin = plugv; + + size_t n = plugin->n_requested_actions++; + if (n < MAX_QUEUED_ACTIONS) { + plugin->requested_actions[n].action = plugin_hot_reload_same_state_action_; + plugin->requested_actions[n].arg_dtor = NULL; + } +} + +static int plugin_do_log(plugin_t *plugin, void *chrs) +{ + (void)plugin; + char *str = chrs; + puts(str); + return 0; +} + +void montis_do_request_log(void *plugv, const char *str) +{ + plugin_t *plugin = plugv; + + size_t n = plugin->n_requested_actions++; + if (n < MAX_QUEUED_ACTIONS) { + plugin->requested_actions[n].action = plugin_do_log; + plugin->requested_actions[n].str_arg = strdup(str); + plugin->requested_actions[n].arg_dtor = free; + } +} + +static int montis_plugin_do_exit(void *plugv, int ec) +{ + (void)plugv; + exit(ec); + return 0; +} + +void montis_do_request_exit(void *plugv, int ec) +{ + plugin_t *plugin = plugv; + + size_t n = plugin->n_requested_actions++; + if (n < MAX_QUEUED_ACTIONS) { + plugin->requested_actions[n].action = + (int (*)(plugin_t *, void *))montis_plugin_do_exit; + plugin->requested_actions[n].int_arg = ec; + plugin->requested_actions[n].arg_dtor = NULL; + } +} + +void *montis_plugin_get_seat(void *ctx) +{ + struct montis_server *server = wl_container_of(ctx, server, plugin); + return server->seat; +} diff --git a/arken/src/util.c b/erebor/src/util.c index e09cff9..e09cff9 100644 --- a/arken/src/util.c +++ b/erebor/src/util.c diff --git a/montis/README.md b/montis/README.md index 5592f08..4a1c1b7 100644 --- a/montis/README.md +++ b/montis/README.md @@ -1 +1,40 @@ -The Plugin for the Montis Runtime. +Montis (Haskell Window Manager Plugin) +====================================== + +Montis is the hot-reloadable window manager logic for the Arken runtime. + +It builds to a shared object (`montis.so`) that Arken loads at runtime. You can +edit/rebuild the plugin and hot-reload it without restarting the compositor. + +Native interface +---------------- + +Montis talks to the runtime through: + +- the plugin ABI defined by Arken (`arken/include/plugin.h`), implemented in Haskell via + `montis/src/Montis/Core/Internal/Foreign/Export.hs` and a small C shim + `montis/src/harness_adapter.c`. +- the Erebor bridge library (`liberebor.a`), which provides plugin-facing helper + functions used via FFI (see `montis/src/Montis/Base/Foreign/Runtime.hs`). + +Building +-------- + +The plugin is built by Stack, typically driven by the top-level CMake build: + +```sh +cmake -S .. -B ../build +cmake --build ../build --target plug_build +``` + +Or directly: + +```sh +stack build +``` + +Output +------ + +The built `montis.so` ends up under the shared `build/stack-work` directory +(symlinked to `montis/.stack-work` by the top-level build). diff --git a/montis/package.yaml b/montis/package.yaml index c381197..679999e 100644 --- a/montis/package.yaml +++ b/montis/package.yaml @@ -71,9 +71,14 @@ executables: - -shared - -I../build/ - -I../arken/include/ + - -I../erebor/include/ - -I../build/wlroots/include - -I../build/wlroots-src/include - -DWLR_USE_UNSTABLE + extra-lib-dirs: + - ../build/erebor + extra-libraries: + - erebor tests: montis-test: |