aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt71
-rw-r--r--README.md75
-rw-r--r--arken/README.md52
-rw-r--r--arken/src/plugin.c61
-rw-r--r--erebor/CMakeLists.txt25
-rw-r--r--erebor/README.md32
-rw-r--r--erebor/include/util.h (renamed from arken/include/util.h)5
-rw-r--r--erebor/src/runtime_requests.c69
-rw-r--r--erebor/src/util.c (renamed from arken/src/util.c)0
-rw-r--r--montis/README.md41
-rw-r--r--montis/package.yaml5
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()
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/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: