From 09860c75bb129c70768692aaec3bd42d0b2735e3 Mon Sep 17 00:00:00 2001 From: Josh Rahm Date: Tue, 6 Jan 2026 15:45:03 -0700 Subject: [reorg] add "middleware" library called "liberebor" --- CMakeLists.txt | 71 ++++++++++++---------- README.md | 75 ++++++++++++++++++++++++ arken/README.md | 52 +++++++++++++++++ arken/include/util.h | 19 ------ arken/src/plugin.c | 61 ------------------- arken/src/util.c | 133 ------------------------------------------ erebor/CMakeLists.txt | 25 ++++++++ erebor/README.md | 32 ++++++++++ erebor/include/util.h | 24 ++++++++ erebor/src/runtime_requests.c | 69 ++++++++++++++++++++++ erebor/src/util.c | 133 ++++++++++++++++++++++++++++++++++++++++++ montis/README.md | 41 ++++++++++++- montis/package.yaml | 5 ++ 13 files changed, 495 insertions(+), 245 deletions(-) create mode 100644 arken/README.md delete mode 100644 arken/include/util.h delete mode 100644 arken/src/util.c create mode 100644 erebor/CMakeLists.txt create mode 100644 erebor/README.md create mode 100644 erebor/include/util.h create mode 100644 erebor/src/runtime_requests.c create mode 100644 erebor/src/util.c 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; \"$\" -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; \"$\" -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() diff --git a/README.md b/README.md index b722f5a..954a725 100644 --- a/README.md +++ b/README.md @@ -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/include/util.h b/arken/include/util.h deleted file mode 100644 index 2ed2f70..0000000 --- a/arken/include/util.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef MONTIS_UTIL_H -#define MONTIS_UTIL_H - -/* - * Runtime helpers exposed to plugins. These operate on compositor state and - * are intended for direct FFI use from the Haskell plugin. - */ - -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); -void montis_plugin_get_toplevel_geometry(void *toplevel, double *x, double *y, - double *w, double *h); -void montis_plugin_set_toplevel_geometry(void *toplevel, double x, double y, - double w, double h); -void montis_plugin_focus_toplevel(void *toplevel); -void montis_plugin_warp_cursor(void *ctx, double lx, double ly); - -#endif /* MONTIS_UTIL_H */ 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 #include @@ -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/arken/src/util.c b/arken/src/util.c deleted file mode 100644 index e09cff9..0000000 --- a/arken/src/util.c +++ /dev/null @@ -1,133 +0,0 @@ -#include "util.h" -#include "wl.h" - -#include -#include - -static struct montis_server *server_from_ctx(void *ctx) -{ - struct montis_server *server = wl_container_of(ctx, server, plugin); - return server; -} - -static struct montis_toplevel *toplevel_at(struct montis_server *server, - double lx, double ly) -{ - double sx = 0.0; - double sy = 0.0; - - struct wlr_scene_node *node = - wlr_scene_node_at(&server->scene->tree.node, lx, ly, &sx, &sy); - if (node == NULL || node->type != WLR_SCENE_NODE_BUFFER) { - return NULL; - } - struct wlr_scene_buffer *scene_buffer = wlr_scene_buffer_from_node(node); - struct wlr_scene_surface *scene_surface = - wlr_scene_surface_try_from_buffer(scene_buffer); - if (!scene_surface) { - return NULL; - } - - struct wlr_scene_tree *tree = node->parent; - while (tree != NULL && tree->node.data == NULL) { - tree = tree->node.parent; - } - return tree ? tree->node.data : NULL; -} - -void *montis_plugin_toplevel_at(void *ctx, double lx, double ly) -{ - if (!ctx) { - return NULL; - } - struct montis_server *server = server_from_ctx(ctx); - return toplevel_at(server, lx, ly); -} - -void montis_plugin_get_toplevel_position(void *toplevel, double *x, double *y) -{ - if (!toplevel || !x || !y) { - return; - } - struct montis_toplevel *tl = toplevel; - *x = tl->scene_tree->node.x; - *y = tl->scene_tree->node.y; -} - -void montis_plugin_set_toplevel_position(void *toplevel, double x, double y) -{ - if (!toplevel) { - return; - } - struct montis_toplevel *tl = toplevel; - wlr_scene_node_set_position(&tl->scene_tree->node, (int)x, (int)y); -} - -void montis_plugin_get_toplevel_geometry(void *toplevel, double *x, double *y, - double *w, double *h) -{ - if (!toplevel || !x || !y || !w || !h) { - return; - } - struct montis_toplevel *tl = toplevel; - struct wlr_box geo_box; - wlr_xdg_surface_get_geometry(tl->xdg_toplevel->base, &geo_box); - *x = tl->scene_tree->node.x; - *y = tl->scene_tree->node.y; - *w = geo_box.width; - *h = geo_box.height; -} - -void montis_plugin_set_toplevel_geometry(void *toplevel, double x, double y, - double w, double h) -{ - if (!toplevel) { - return; - } - struct montis_toplevel *tl = toplevel; - wlr_scene_node_set_position(&tl->scene_tree->node, (int)x, (int)y); - wlr_xdg_toplevel_set_size(tl->xdg_toplevel, (int)w, (int)h); -} - -void montis_plugin_warp_cursor(void *ctx, double lx, double ly) -{ - if (!ctx) { - return; - } - struct montis_server *server = server_from_ctx(ctx); - wlr_cursor_warp(server->cursor, NULL, lx, ly); -} - -void montis_plugin_focus_toplevel(void *toplevel) -{ - if (!toplevel) { - return; - } - struct montis_toplevel *tl = toplevel; - struct montis_server *server = tl->server; - struct wlr_seat *seat = server->seat; - struct wlr_surface *surface = tl->xdg_toplevel->base->surface; - struct wlr_surface *prev_surface = seat->keyboard_state.focused_surface; - - if (prev_surface == surface) { - return; - } - if (prev_surface) { - struct wlr_xdg_toplevel *prev_toplevel = - wlr_xdg_toplevel_try_from_wlr_surface(prev_surface); - if (prev_toplevel != NULL) { - wlr_xdg_toplevel_set_activated(prev_toplevel, false); - } - } - - struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(seat); - wlr_scene_node_raise_to_top(&tl->scene_tree->node); - wl_list_remove(&tl->link); - wl_list_insert(&server->toplevels, &tl->link); - wlr_xdg_toplevel_set_activated(tl->xdg_toplevel, true); - if (keyboard != NULL) { - wlr_seat_keyboard_notify_enter(seat, surface, keyboard->keycodes, - keyboard->num_keycodes, - &keyboard->modifiers); - } -} 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/erebor/include/util.h b/erebor/include/util.h new file mode 100644 index 0000000..22b4020 --- /dev/null +++ b/erebor/include/util.h @@ -0,0 +1,24 @@ +#ifndef MONTIS_UTIL_H +#define MONTIS_UTIL_H + +/* + * Runtime helpers exposed to plugins. These operate on compositor state and + * 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); +void montis_plugin_get_toplevel_geometry(void *toplevel, double *x, double *y, + double *w, double *h); +void montis_plugin_set_toplevel_geometry(void *toplevel, double x, double y, + double w, double h); +void montis_plugin_focus_toplevel(void *toplevel); +void montis_plugin_warp_cursor(void *ctx, double lx, double ly); + +#endif /* MONTIS_UTIL_H */ 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 +#include +#include + +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/erebor/src/util.c b/erebor/src/util.c new file mode 100644 index 0000000..e09cff9 --- /dev/null +++ b/erebor/src/util.c @@ -0,0 +1,133 @@ +#include "util.h" +#include "wl.h" + +#include +#include + +static struct montis_server *server_from_ctx(void *ctx) +{ + struct montis_server *server = wl_container_of(ctx, server, plugin); + return server; +} + +static struct montis_toplevel *toplevel_at(struct montis_server *server, + double lx, double ly) +{ + double sx = 0.0; + double sy = 0.0; + + struct wlr_scene_node *node = + wlr_scene_node_at(&server->scene->tree.node, lx, ly, &sx, &sy); + if (node == NULL || node->type != WLR_SCENE_NODE_BUFFER) { + return NULL; + } + struct wlr_scene_buffer *scene_buffer = wlr_scene_buffer_from_node(node); + struct wlr_scene_surface *scene_surface = + wlr_scene_surface_try_from_buffer(scene_buffer); + if (!scene_surface) { + return NULL; + } + + struct wlr_scene_tree *tree = node->parent; + while (tree != NULL && tree->node.data == NULL) { + tree = tree->node.parent; + } + return tree ? tree->node.data : NULL; +} + +void *montis_plugin_toplevel_at(void *ctx, double lx, double ly) +{ + if (!ctx) { + return NULL; + } + struct montis_server *server = server_from_ctx(ctx); + return toplevel_at(server, lx, ly); +} + +void montis_plugin_get_toplevel_position(void *toplevel, double *x, double *y) +{ + if (!toplevel || !x || !y) { + return; + } + struct montis_toplevel *tl = toplevel; + *x = tl->scene_tree->node.x; + *y = tl->scene_tree->node.y; +} + +void montis_plugin_set_toplevel_position(void *toplevel, double x, double y) +{ + if (!toplevel) { + return; + } + struct montis_toplevel *tl = toplevel; + wlr_scene_node_set_position(&tl->scene_tree->node, (int)x, (int)y); +} + +void montis_plugin_get_toplevel_geometry(void *toplevel, double *x, double *y, + double *w, double *h) +{ + if (!toplevel || !x || !y || !w || !h) { + return; + } + struct montis_toplevel *tl = toplevel; + struct wlr_box geo_box; + wlr_xdg_surface_get_geometry(tl->xdg_toplevel->base, &geo_box); + *x = tl->scene_tree->node.x; + *y = tl->scene_tree->node.y; + *w = geo_box.width; + *h = geo_box.height; +} + +void montis_plugin_set_toplevel_geometry(void *toplevel, double x, double y, + double w, double h) +{ + if (!toplevel) { + return; + } + struct montis_toplevel *tl = toplevel; + wlr_scene_node_set_position(&tl->scene_tree->node, (int)x, (int)y); + wlr_xdg_toplevel_set_size(tl->xdg_toplevel, (int)w, (int)h); +} + +void montis_plugin_warp_cursor(void *ctx, double lx, double ly) +{ + if (!ctx) { + return; + } + struct montis_server *server = server_from_ctx(ctx); + wlr_cursor_warp(server->cursor, NULL, lx, ly); +} + +void montis_plugin_focus_toplevel(void *toplevel) +{ + if (!toplevel) { + return; + } + struct montis_toplevel *tl = toplevel; + struct montis_server *server = tl->server; + struct wlr_seat *seat = server->seat; + struct wlr_surface *surface = tl->xdg_toplevel->base->surface; + struct wlr_surface *prev_surface = seat->keyboard_state.focused_surface; + + if (prev_surface == surface) { + return; + } + if (prev_surface) { + struct wlr_xdg_toplevel *prev_toplevel = + wlr_xdg_toplevel_try_from_wlr_surface(prev_surface); + if (prev_toplevel != NULL) { + wlr_xdg_toplevel_set_activated(prev_toplevel, false); + } + } + + struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(seat); + wlr_scene_node_raise_to_top(&tl->scene_tree->node); + wl_list_remove(&tl->link); + wl_list_insert(&server->toplevels, &tl->link); + wlr_xdg_toplevel_set_activated(tl->xdg_toplevel, true); + if (keyboard != NULL) { + wlr_seat_keyboard_notify_enter(seat, surface, keyboard->keycodes, + keyboard->num_keycodes, + &keyboard->modifiers); + } +} 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: -- cgit