diff --git a/layer_shell_preload.md b/layer_shell_preload.md new file mode 100644 index 0000000..86b5b44 --- /dev/null +++ b/layer_shell_preload.md @@ -0,0 +1,22 @@ +# Layer Shell Preload +`liblayer-shell-preload.so` is a hack to allow arbitrary Wayland apps to use the Layer Shell protocol. It uses the same approach as gtk4-layer-shell, but generalized to work with any `libwayland-client` program. It's designed to be `LD_PRELOAD`ed into pre-built binaries, no recompiling necessary. + +There is no officially supported way to use Layer Shell Preload. If it works, it works. If it doesn't work or randomly stops working, that is a you problem. Not a me problem, and definitely not an upstream app dev problem. __Do not report bugs to app or toolkit developers if Layer Shell Preload is involved in any way.__ You're welcome to report bugs here, but I'm likely to ignore or close them (especially it's just "crashes X program"). + +## Usage +Run your program with `LD_PRELOAD` set to the path to `liblayer-shell-preload.so`, and use `LAYER_` environment variables to control the layer surface's properties. For example +``` +$ LD_PRELOAD=/usr/lib/liblayer-shell-preload.so LAYER_ANCHOR='lrb' LAYER_HEIGHT=50 LAYER_EXCLUSIVE=1 LAYER_KEYBOARD=on-demand weston-terminal +``` +The options are +| Variable | Values | Use | +| - | - | - | +| `LAYER_LAYER` | unset/`o`/`overlay`, `t`/`top`, `b`/`bottom`, `g`/`background` | The layer of the window (overlay and top are over other windows, bottom and background are below them) | +| `LAYER_ANCHOR` | One or more of the letters `l`, `r`, `t` and `b` in any order, or `0`/unset for none | Which edges of the screen to anchor the surface to | +| `LAYER_EXCLUSIVE` | unset/`0` or `1` | If to take up space and push other windows out of the way. Only works if anchored to an edge | +| `LAYER_WIDTH`/`LAYER_HEIGHT` | unset or any number | The preferred width and/or height for the surface when it's not being stretched across the screen | +| `LAYER_KEYBOARD` | unset/`n`/`none`, `e`/`exclusive` or `o`/`on-demand` | If to allow or lock keyboard focus on the window | +| `LAYER_NAMESPACE` | unset or any string | The `namespace` property for the layer shell surface, can be used by the Wayland compositor | +| `LAYER_ALL_SURFACES` | unset/`0` or `1` | By default only the first surface a program creates is a layer surface. With this set they all are. This breaks popups | + +Output selection is not yet supported, but could be added in the future. diff --git a/src/layer-shell-preload.c b/src/layer-shell-preload.c new file mode 100644 index 0000000..8afee42 --- /dev/null +++ b/src/layer-shell-preload.c @@ -0,0 +1,115 @@ +#include "layer-surface.h" +#include "stdlib.h" +#include "string.h" +#include "stdio.h" + +static bool has_created_layer_surface = false; + +static bool get_bool_from_env(const char* env) { + const char* val = getenv(env); + if (!val || strcmp(val, "") == 0 || strcmp(val, "0") == 0) { + return false; + } else if (strcmp(val, "1") == 0) { + return true; + } else { + fprintf(stderr, "Invalid value for %s (expected '1', '0' or unset)", env); + exit(1); + } +} + +static int get_int_from_env(const char* env) { + const char* val = getenv(env); + return val ? atoi(val) : 0; +} + +static const char* get_string_from_env(const char* env, const char* fallback) { + const char* val = getenv(env); + if (val && *val) { + return val; + } else { + return fallback; + } +} + +static struct geom_edges_t get_edges_from_env(const char* env) { + const char* val = getenv(env); + if (!val || strcmp(val, "0") == 0) { + return (struct geom_edges_t){0}; + } else { + struct geom_edges_t ret = {0}; + for (const char* c = val; *c; c++) { + switch (*c) { + case 'l': ret.left = true; break; + case 'r': ret.right = true; break; + case 't': ret.top = true; break; + case 'b': ret.bottom = true; break; + default: + fprintf(stderr, "Invalid character '%c' in %s (expected 'l', 'r', 't' and 'b')", *c, env); + exit(1); + } + } + return ret; + } +} + +static enum zwlr_layer_shell_v1_layer get_layer_from_env(const char* env) { + const char* val = getenv(env); + if (!val || strcmp(val, "") == 0 || strcmp(val, "o") == 0 || strcmp(val, "overlay") == 0) { + return ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; + } else if (strcmp(val, "t") == 0 || strcmp(val, "top") == 0) { + return ZWLR_LAYER_SHELL_V1_LAYER_TOP; + } else if (strcmp(val, "b") == 0 || strcmp(val, "bottom") == 0) { + return ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; + } else if (strcmp(val, "g") == 0 || strcmp(val, "background") == 0) { + return ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND; + } else { + fprintf(stderr, "Invalid value for %s (expected unset/'o'/'overlay', 't'/'top', 'b'/'bottom', 'g'/'background')", env); + exit(1); + } +} + +static enum zwlr_layer_surface_v1_keyboard_interactivity get_keyboard_interactivity_from_env(const char* env) { + const char* val = getenv(env); + if (!val || strcmp(val, "") == 0 || strcmp(val, "n") == 0 || strcmp(val, "none") == 0) { + return ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE; + } else if (strcmp(val, "e") == 0 || strcmp(val, "exclusive") == 0) { + return ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE; + } else if (strcmp(val, "o") == 0 || strcmp(val, "on-demand") == 0) { + return ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND; + } else { + fprintf(stderr, "Invalid value for %s (expected unset/'n'/'none', 'e'/'exclusive' or 'o'/'on-demand')", env); + exit(1); + } +} + +static struct geom_size_t get_preferred_size(struct layer_surface_t* surface) { + (void)surface; + return (struct geom_size_t){ + .width = get_int_from_env("LAYER_WIDTH"), + .height = get_int_from_env("LAYER_HEIGHT"), + }; +} + +static struct layer_surface_t* layer_surface_hook_callback_impl(struct wl_surface* wl_surface) { + (void)wl_surface; + if (has_created_layer_surface && !get_bool_from_env("LAYER_ALL_SURFACES")) { + return NULL; + } + has_created_layer_surface = true; + struct layer_surface_t* layer_surface = malloc(sizeof(struct layer_surface_t)); + *layer_surface = layer_surface_make(); + layer_surface->get_preferred_size = get_preferred_size; + layer_surface_set_name_space(layer_surface, get_string_from_env("LAYER_NAMESPACE", "layer-shell-preload")); + layer_surface_set_anchor(layer_surface, get_edges_from_env("LAYER_ANCHOR")); + layer_surface_set_layer(layer_surface, get_layer_from_env("LAYER_LAYER")); + layer_surface_set_keyboard_mode(layer_surface, get_keyboard_interactivity_from_env("LAYER_KEYBOARD")); + if (get_bool_from_env("LAYER_EXCLUSIVE")) { + layer_surface_auto_exclusive_zone_enable(layer_surface); + } + return layer_surface; +} + +__attribute__((constructor)) +static void setup(void) { + layer_surface_install_hook(layer_surface_hook_callback_impl); +} diff --git a/src/layer-surface.c b/src/layer-surface.c index 912abc0..c5f9092 100644 --- a/src/layer-surface.c +++ b/src/layer-surface.c @@ -87,7 +87,7 @@ static void layer_surface_configure_xdg_surface( } { uint32_t* state = wl_array_add(&states, sizeof(uint32_t)); assert(state); - *state = XDG_TOPLEVEL_STATE_MAXIMIZED; + *state = XDG_TOPLEVEL_STATE_FULLSCREEN; } LIBWAYLAND_SHIM_DISPATCH_CLIENT_EVENT( diff --git a/src/meson.build b/src/meson.build index cc92b7a..573ebc1 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,12 +1,15 @@ -srcs = files( - 'gtk4-layer-shell.c', - 'gtk4-session-lock.c', +core_srcs = files( 'stolen-from-libwayland.c', 'libwayland-shim.c', 'layer-surface.c', 'lock-surface.c', 'registry.c') +gtk_srcs = files( + 'gtk4-layer-shell.c', + 'gtk4-session-lock.c', + ) + version_args = [ '-DGTK_LAYER_SHELL_MAJOR=' + meson.project_version().split('.')[0], '-DGTK_LAYER_SHELL_MINOR=' + meson.project_version().split('.')[1], @@ -14,7 +17,7 @@ version_args = [ ] gtk_layer_shell_lib = library('gtk4-layer-shell', - srcs, client_protocol_srcs, + gtk_srcs, core_srcs, client_protocol_srcs, c_args: version_args, include_directories: [gtk_layer_shell_inc], dependencies: [gtk, wayland_client], @@ -22,6 +25,12 @@ gtk_layer_shell_lib = library('gtk4-layer-shell', soversion: lib_so_version, install: true) +layer_shell_preload_lib = library('layer-shell-preload', + files('layer-shell-preload.c'), core_srcs, client_protocol_srcs, + include_directories: [gtk_layer_shell_inc], + dependencies: [wayland_client], + install: true) + pkg_config_name = 'gtk4-layer-shell-0' # GObject introspection file used to interface with other languages @@ -29,7 +38,7 @@ if get_option('introspection') gir_layer = gnome.generate_gir( gtk_layer_shell_lib, dependencies: [gtk], - sources: srcs + files('../include/gtk4-layer-shell.h'), + sources: files('../include/gtk4-layer-shell.h') + gtk_srcs + core_srcs, namespace: 'Gtk4LayerShell', nsversion: '1.0', identifier_prefix: 'GtkLayerShell', @@ -42,7 +51,7 @@ if get_option('introspection') gir_lock = gnome.generate_gir( gtk_layer_shell_lib, dependencies: [gtk], - sources: srcs + files('../include/gtk4-session-lock.h'), + sources: files('../include/gtk4-session-lock.h') + gtk_srcs + core_srcs, namespace: 'Gtk4SessionLock', nsversion: '1.0', identifier_prefix: 'GtkSessionLock',