kick/tabbed-wlroots.c
Andrew Hamilton b7faf9ec91 Crop CSD titlebar gutter instead of terminal bottom row
For clients (kitty/foot) that reserve a titlebar gutter above their
content but never paint it, that area showed as a dark bar while their
content sat low enough that the bottom row/cursor was pushed out the
output edge. Shift such clients up by the gutter so the dark bar is
cropped away instead of the terminal's bottom. Clients with no gutter
(geometry.y >= 0, e.g. the shm test client) are left untouched.
2026-08-07 10:27:55 +10:00

1374 lines
47 KiB
C

/* tabbed-wlroots.c -- nested tabbed compositor built on wlroots 0.19.
*
* Renders a headless wlroots output (via wlr_scene) inside a host Wayland
* window (the "host" connection, e.g. labwc). Input from the host is
* forwarded into a wlr_seat, so wlroots handles all sub-surface hit-testing,
* enter/leave, input regions and coordinate translation. Clients are always
* maximized to the host content area; their minimize/maximize/fullscreen
* requests are ignored.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <execinfo.h>
#include <linux/memfd.h>
#include <poll.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/poll.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <drm_fourcc.h>
#include <wayland-client.h>
#include <wayland-egl.h>
#include <wayland-server.h>
#include "xdg-shell-client.h"
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#include <xkbcommon/xkbcommon.h>
#include <wlr/backend.h>
#include <wlr/backend/headless.h>
#include <wlr/interfaces/wlr_keyboard.h>
#include <wlr/render/allocator.h>
#include <wlr/render/drm_format_set.h>
#include <wlr/render/swapchain.h>
#include <wlr/render/wlr_renderer.h>
#include <wlr/types/wlr_buffer.h>
#include <wlr/types/wlr_compositor.h>
#include <wlr/types/wlr_data_device.h>
#include <wlr/types/wlr_keyboard.h>
#include <wlr/types/wlr_linux_dmabuf_v1.h>
#include <wlr/types/wlr_output.h>
#include <wlr/types/wlr_pointer.h>
#include <wlr/types/wlr_scene.h>
#include <wlr/types/wlr_seat.h>
#include <wlr/types/wlr_shm.h>
#include <wlr/types/wlr_subcompositor.h>
#include <wlr/types/wlr_xdg_shell.h>
#include <wlr/util/log.h>
#define MAX_APPS 8
#define MAX_WIN_W 2560
#define MAX_WIN_H 1600
/* ===================== Server-side (wlroots) state ===================== */
struct tabbed_app {
char *cmd;
pid_t pid;
struct wlr_xdg_toplevel *toplevel;
struct wlr_scene_tree *tree;
struct wl_listener destroy;
struct wl_listener map;
struct wl_listener commit;
struct wl_listener request_maximize;
struct wl_listener request_fullscreen;
struct wl_listener request_minimize;
struct wl_listener request_move;
struct wl_listener request_resize;
};
struct tabbed_server {
struct wl_display *wl_display;
struct wl_event_loop *loop;
struct wlr_backend *backend;
struct wlr_renderer *renderer;
struct wlr_allocator *allocator;
struct wlr_scene *scene;
struct wlr_output *output;
struct wlr_scene_output *scene_output;
struct wlr_scene_rect *bg_rect;
struct wlr_seat *seat;
struct wlr_xdg_shell *xdg_shell;
struct wlr_keyboard keyboard;
struct wlr_swapchain *swapchain;
struct wl_listener new_output;
struct wl_listener new_toplevel;
struct wl_listener frame;
struct wlr_xdg_toplevel *active;
struct tabbed_app *apps[MAX_APPS];
int num_apps;
struct xkb_context *xkb_ctx;
struct xkb_keymap *xkb_keymap;
};
/* ===================== Host-side (nested) state ===================== */
struct tabbed_host {
struct wl_display *display;
struct wl_registry *registry;
struct wl_compositor *compositor;
struct wl_surface *surface;
struct xdg_wm_base *wm_base;
struct xdg_surface *xdg_surface;
struct xdg_toplevel *toplevel;
struct wl_seat *seat;
struct wl_keyboard *keyboard;
struct wl_pointer *pointer;
bool configured;
int width, height;
/* EGL */
bool egl_ok;
EGLDisplay egl_display;
EGLConfig egl_config;
EGLContext egl_context;
EGLSurface egl_surface;
struct wl_egl_window *egl_window;
/* host modifiers (for tab-switch detection) */
uint32_t host_mods_depressed;
uint32_t axis_source;
/* GL program */
GLuint gl_program;
GLint gl_pos_loc, gl_uv_loc, gl_tex_loc;
GLuint import_tex;
};
static struct tabbed_server server;
static struct tabbed_host host;
static char tabbed_socket[64];
static bool needs_redraw;
static bool frame_pending;
static void set_active_surface(struct wlr_xdg_toplevel *toplevel);
static void render_and_present(void);
/* ===================== EGL helpers ===================== */
static PFNEGLCREATEIMAGEKHRPROC pfn_eglCreateImageKHR;
static PFNEGLDESTROYIMAGEKHRPROC pfn_eglDestroyImageKHR;
static PFNEGLGETPLATFORMDISPLAYEXTPROC pfn_eglGetPlatformDisplayEXT;
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC pfn_glEGLImageTargetTexture2DOES;
static void *egl_proc(const char *name) {
void *p = (void *)eglGetProcAddress(name);
if (!p)
fprintf(stderr, "tabbed-wlroots: EGL proc %s unavailable\n", name);
return p;
}
static GLuint compile_shader(GLenum type, const char *src) {
GLuint sh = glCreateShader(type);
glShaderSource(sh, 1, &src, NULL);
glCompileShader(sh);
GLint ok;
glGetShaderiv(sh, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[512];
glGetShaderInfoLog(sh, sizeof(log), NULL, log);
fprintf(stderr, "tabbed-wlroots: shader error: %s\n", log);
glDeleteShader(sh);
return 0;
}
return sh;
}
static bool gl_setup(void) {
const char *vs =
"attribute vec2 a_pos;\n"
"attribute vec2 a_uv;\n"
"varying vec2 v_uv;\n"
"void main() {\n"
" v_uv = a_uv;\n"
" gl_Position = vec4(a_pos, 0.0, 1.0);\n"
"}\n";
const char *fs =
"precision mediump float;\n"
"varying vec2 v_uv;\n"
"uniform sampler2D u_tex;\n"
"void main() {\n"
" gl_FragColor = texture2D(u_tex, v_uv);\n"
"}\n";
GLuint vsh = compile_shader(GL_VERTEX_SHADER, vs);
GLuint fsh = compile_shader(GL_FRAGMENT_SHADER, fs);
if (!vsh || !fsh) return false;
host.gl_program = glCreateProgram();
glAttachShader(host.gl_program, vsh);
glAttachShader(host.gl_program, fsh);
glBindAttribLocation(host.gl_program, 0, "a_pos");
glBindAttribLocation(host.gl_program, 1, "a_uv");
glLinkProgram(host.gl_program);
GLint ok;
glGetProgramiv(host.gl_program, GL_LINK_STATUS, &ok);
glDeleteShader(vsh);
glDeleteShader(fsh);
if (!ok) {
char log[512];
glGetProgramInfoLog(host.gl_program, sizeof(log), NULL, log);
fprintf(stderr, "tabbed-wlroots: link error: %s\n", log);
return false;
}
host.gl_pos_loc = glGetAttribLocation(host.gl_program, "a_pos");
host.gl_uv_loc = glGetAttribLocation(host.gl_program, "a_uv");
host.gl_tex_loc = glGetUniformLocation(host.gl_program, "u_tex");
glGenTextures(1, &host.import_tex);
glBindTexture(GL_TEXTURE_2D, host.import_tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
return true;
}
/* Blit a source rect of a texture into the window at the given destination
offset (window coords), clipped to the window. */
static void draw_texture_rect(GLuint tex, int tex_w, int tex_h,
int src_x, int src_y, int src_w, int src_h,
int dst_x, int dst_y, int win_w, int win_h)
{
if (win_w <= 0 || win_h <= 0) return;
if (src_w <= 0 || src_h <= 0) return;
if (src_x >= tex_w || src_y >= tex_h) return;
if (src_w > tex_w - src_x) src_w = tex_w - src_x;
if (src_h > tex_h - src_y) src_h = tex_h - src_y;
int cx = dst_x < 0 ? -dst_x : 0;
int cy = dst_y < 0 ? -dst_y : 0;
if (cx >= src_w || cy >= src_h) return;
int wx0 = dst_x + cx;
int wy0 = dst_y + cy;
int cw = src_w - cx;
int ch = src_h - cy;
int maxw = win_w - wx0;
int maxh = win_h - wy0;
if (cw > maxw) cw = maxw;
if (ch > maxh) ch = maxh;
if (cw <= 0 || ch <= 0) return;
src_x += cx;
src_y += cy;
float u0 = (float)src_x / (float)tex_w;
float u1 = (float)(src_x + cw) / (float)tex_w;
float v0 = (float)src_y / (float)tex_h;
float v1 = (float)(src_y + ch) / (float)tex_h;
float nx0 = -1.0f + 2.0f * (float)wx0 / (float)win_w;
float ny0 = 1.0f - 2.0f * (float)wy0 / (float)win_h;
float nx1 = -1.0f + 2.0f * (float)(wx0 + cw) / (float)win_w;
float ny1 = 1.0f - 2.0f * (float)(wy0 + ch) / (float)win_h;
GLfloat verts[8] = {
nx0, ny0,
nx1, ny0,
nx1, ny1,
nx0, ny1,
};
GLfloat uvs[8] = {
u0, v0,
u1, v0,
u1, v1,
u0, v1,
};
GLushort idx[6] = { 0, 1, 2, 0, 2, 3 };
glUseProgram(host.gl_program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, tex);
glUniform1i(host.gl_tex_loc, 0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, verts);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, uvs);
glEnableVertexAttribArray(1);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, idx);
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
}
/* ===================== Server-side wlroots setup ===================== */
static void swapchain_resize(int width, int height) {
if (width < 50) width = 50;
if (height < 50) height = 50;
static uint64_t mods[] = { DRM_FORMAT_MOD_LINEAR };
struct wlr_drm_format fmt = {
.format = DRM_FORMAT_ARGB8888,
.len = 1,
.capacity = 1,
.modifiers = mods,
};
if (server.swapchain) {
wlr_swapchain_destroy(server.swapchain);
server.swapchain = NULL;
}
server.swapchain = wlr_swapchain_create(server.allocator, width, height, &fmt);
if (!server.swapchain)
fprintf(stderr, "tabbed-wlroots: swapchain create failed at %dx%d\n",
width, height);
}
static void output_resize(int width, int height) {
if (width < 50) width = 50;
if (height < 50) height = 50;
if (!server.output || !server.allocator) return;
struct wlr_output_state state;
wlr_output_state_init(&state);
wlr_output_state_set_enabled(&state, true);
wlr_output_state_set_custom_mode(&state, width, height, 0);
wlr_output_commit_state(server.output, &state);
wlr_output_state_finish(&state);
swapchain_resize(width, height);
if (server.bg_rect)
wlr_scene_rect_set_size(server.bg_rect, width, height);
if (server.active) {
wlr_xdg_toplevel_set_maximized(server.active, true);
wlr_xdg_toplevel_set_size(server.active, width, height);
}
needs_redraw = true;
}
static void handle_output_frame(struct wl_listener *listener, void *data) {
(void)listener; (void)data;
frame_pending = true;
}
static void handle_new_output(struct wl_listener *listener, void *data) {
(void)listener;
struct wlr_output *output = data;
if (!wlr_output_init_render(output, server.allocator, server.renderer)) {
fprintf(stderr, "tabbed-wlroots: output init_render failed\n");
return;
}
wlr_output_create_global(output, server.wl_display);
server.output = output;
server.scene_output = wlr_scene_output_create(server.scene, output);
wlr_scene_output_set_position(server.scene_output, 0, 0);
/* Render whenever the output is ready for a frame (refresh-rate driven),
like tinywl; this is what drives client frame callbacks / surface
enter events even before any damage has accumulated. */
server.frame.notify = handle_output_frame;
wl_signal_add(&output->events.frame, &server.frame);
/* Enable the output up front so buffer commits succeed even before the
host window has been configured. */
int w = output->width > 0 ? output->width : host.width;
int h = output->height > 0 ? output->height : host.height;
struct wlr_output_state state;
wlr_output_state_init(&state);
wlr_output_state_set_enabled(&state, true);
wlr_output_state_set_custom_mode(&state, w, h, 0);
wlr_output_commit_state(output, &state);
wlr_output_state_finish(&state);
swapchain_resize(w, h);
/* A full-output background node keeps the scene's render list above a
single entry. Without it wlroots tries direct scanout of the client's
buffer, which (for shm clients) hands us a buffer we can't import into
the host GL texture and the nested window goes blank. It also gives the
scene a deterministic opaque backdrop. */
server.bg_rect = wlr_scene_rect_create(&server.scene->tree, w, h,
(float[]){ 0.0f, 0.0f, 0.0f, 1.0f });
if (server.bg_rect)
wlr_scene_node_lower_to_bottom(&server.bg_rect->node);
}
static void handle_toplevel_destroy(struct wl_listener *listener, void *data) {
struct tabbed_app *app = wl_container_of(listener, app, destroy);
(void)data;
app->toplevel = NULL;
app->tree = NULL;
if (server.active != NULL &&
server.active == (struct wlr_xdg_toplevel *)data) {
server.active = NULL;
for (int i = 0; i < server.num_apps; i++)
if (server.apps[i]->toplevel) {
set_active_surface(server.apps[i]->toplevel);
return;
}
/* last tab closed: close the host window and exit */
xdg_toplevel_destroy(host.toplevel);
wl_surface_destroy(host.surface);
exit(0);
}
}
static void handle_request_maximize(struct wl_listener *listener, void *data) {
(void)data;
struct tabbed_app *app = wl_container_of(listener, app, request_maximize);
struct wlr_xdg_toplevel *toplevel = app->toplevel;
wlr_xdg_toplevel_set_maximized(toplevel, true);
}
static void handle_request_fullscreen(struct wl_listener *listener, void *data) {
(void)data;
struct tabbed_app *app = wl_container_of(listener, app, request_fullscreen);
struct wlr_xdg_toplevel *toplevel = app->toplevel;
wlr_xdg_toplevel_set_fullscreen(toplevel, false);
}
static void handle_request_minimize(struct wl_listener *listener, void *data) {
(void)listener; (void)data;
}
static void handle_request_move(struct wl_listener *listener, void *data) {
(void)listener; (void)data;
}
static void handle_request_resize(struct wl_listener *listener, void *data) {
(void)listener; (void)data;
}
static void position_toplevel(struct tabbed_app *app) {
if (!app || !app->tree) return;
/* CSD clients (foot/kitty) reserve a titlebar gutter above their
content, which we don't paint, so it shows as a dark bar and their
content sits too low (bottom row/cursor cropped). Shift them up by
that gutter so the dark bar is cropped instead of the terminal's
bottom. Clients with no gutter (geometry.y >= 0, e.g. wltest) are
left untouched. */
struct wlr_box geom = app->toplevel->base->geometry;
int y = -geom.y;
if (geom.y < 0)
y -= 26;
wlr_scene_node_set_position(&app->tree->node, -geom.x, y);
}
static void handle_toplevel_commit(struct wl_listener *listener, void *data) {
(void)data;
struct tabbed_app *app = wl_container_of(listener, app, commit);
struct wlr_xdg_toplevel *toplevel = app->toplevel;
if (toplevel->base->initial_commit) {
wlr_xdg_toplevel_set_maximized(toplevel, true);
wlr_xdg_toplevel_set_size(toplevel, server.output->width,
server.output->height);
}
/* Geometry can settle after the first frames (kitty/foot report a
negative y once they decide on their titlebar), so keep the scene
position in sync with every commit instead of only at activation. */
position_toplevel(app);
}
static void handle_toplevel_map(struct wl_listener *listener, void *data) {
(void)data;
struct tabbed_app *app = wl_container_of(listener, app, map);
struct wlr_xdg_toplevel *toplevel = app->toplevel;
wlr_xdg_toplevel_set_activated(toplevel, true);
set_active_surface(toplevel);
}
static void handle_new_toplevel(struct wl_listener *listener, void *data) {
(void)listener;
struct wlr_xdg_toplevel *toplevel = data;
struct tabbed_app *app = NULL;
for (int i = 0; i < server.num_apps; i++)
if (!server.apps[i]->toplevel) {
app = server.apps[i];
break;
}
if (!app) return;
app->toplevel = toplevel;
app->tree = wlr_scene_xdg_surface_create(&server.scene->tree,
toplevel->base);
/* hidden until it becomes the active tab */
wlr_scene_node_set_enabled(&app->tree->node, false);
app->destroy.notify = handle_toplevel_destroy;
wl_signal_add(&toplevel->events.destroy, &app->destroy);
app->map.notify = handle_toplevel_map;
wl_signal_add(&toplevel->base->surface->events.map, &app->map);
app->commit.notify = handle_toplevel_commit;
wl_signal_add(&toplevel->base->surface->events.commit, &app->commit);
app->request_maximize.notify = handle_request_maximize;
wl_signal_add(&toplevel->events.request_maximize, &app->request_maximize);
app->request_fullscreen.notify = handle_request_fullscreen;
wl_signal_add(&toplevel->events.request_fullscreen, &app->request_fullscreen);
app->request_minimize.notify = handle_request_minimize;
wl_signal_add(&toplevel->events.request_minimize, &app->request_minimize);
app->request_move.notify = handle_request_move;
wl_signal_add(&toplevel->events.request_move, &app->request_move);
app->request_resize.notify = handle_request_resize;
wl_signal_add(&toplevel->events.request_resize, &app->request_resize);
}
/* ===================== Tab management ===================== */
static struct tabbed_app *app_for_toplevel(struct wlr_xdg_toplevel *toplevel) {
if (!toplevel) return NULL;
for (int i = 0; i < server.num_apps; i++)
if (server.apps[i]->toplevel == toplevel)
return server.apps[i];
return NULL;
}
static void set_active_surface(struct wlr_xdg_toplevel *toplevel) {
if (server.active == toplevel) return;
struct tabbed_app *old = app_for_toplevel(server.active);
if (old && old->tree)
wlr_scene_node_set_enabled(&old->tree->node, false);
if (server.active)
wlr_xdg_toplevel_set_activated(server.active, false);
server.active = toplevel;
wlr_xdg_toplevel_set_activated(toplevel, true);
struct tabbed_app *app = app_for_toplevel(server.active);
if (app && app->tree) {
position_toplevel(app);
wlr_scene_node_set_enabled(&app->tree->node, true);
}
wlr_seat_keyboard_notify_enter(server.seat, toplevel->base->surface,
server.keyboard.keycodes, server.keyboard.num_keycodes,
&server.keyboard.modifiers);
needs_redraw = true;
}
static void cycle_tab(int dir) {
if (server.num_apps == 0) return;
int idx = -1;
for (int i = 0; i < server.num_apps; i++)
if (server.apps[i]->toplevel == server.active) {
idx = i;
break;
}
for (int k = 1; k <= server.num_apps; k++) {
int j = ((idx < 0 ? 0 : idx) + k * dir + server.num_apps) % server.num_apps;
if (server.apps[j]->toplevel) {
set_active_surface(server.apps[j]->toplevel);
return;
}
}
}
/* ===================== Host-side listeners ===================== */
static void host_registry_global(void *data, struct wl_registry *registry,
uint32_t name, const char *interface,
uint32_t version)
{
(void)data;
if (strcmp(interface, wl_compositor_interface.name) == 0) {
host.compositor = wl_registry_bind(registry, name,
&wl_compositor_interface, version < 4 ? version : 4);
} else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
host.wm_base = wl_registry_bind(registry, name,
&xdg_wm_base_interface, version < 1 ? version : 1);
} else if (strcmp(interface, wl_seat_interface.name) == 0) {
host.seat = wl_registry_bind(registry, name,
&wl_seat_interface, version < 5 ? version : 5);
}
}
static void host_registry_global_remove(void *data, struct wl_registry *registry,
uint32_t name)
{
(void)data; (void)registry; (void)name;
}
static const struct wl_registry_listener host_registry_listener = {
.global = host_registry_global,
.global_remove = host_registry_global_remove,
};
static void host_xdg_wm_ping(void *data, struct xdg_wm_base *wm, uint32_t serial) {
(void)data;
xdg_wm_base_pong(wm, serial);
}
static const struct xdg_wm_base_listener host_xdg_wm_listener = {
.ping = host_xdg_wm_ping,
};
static void host_xdg_surface_configure(void *data, struct xdg_surface *s,
uint32_t serial)
{
(void)data;
xdg_surface_ack_configure(s, serial);
}
static const struct xdg_surface_listener host_xdg_surface_listener = {
.configure = host_xdg_surface_configure,
};
static void host_resize(int width, int height) {
if (width < 50) width = 50;
if (height < 50) height = 50;
host.width = width;
host.height = height;
if (host.egl_window)
wl_egl_window_resize(host.egl_window, width, height, 0, 0);
if (host.egl_ok) {
eglMakeCurrent(host.egl_display, host.egl_surface, host.egl_surface,
host.egl_context);
}
output_resize(width, height);
needs_redraw = true;
}
static void host_xdg_toplevel_configure(void *data, struct xdg_toplevel *t,
int32_t width, int32_t height,
struct wl_array *states)
{
(void)data; (void)t; (void)states;
if (width > 0 && height > 0)
host_resize(width, height);
host.configured = true;
}
static void host_xdg_toplevel_close(void *data, struct xdg_toplevel *t) {
(void)data; (void)t;
fprintf(stderr, "tabbed-wlroots: window closed\n");
exit(0);
}
static const struct xdg_toplevel_listener host_xdg_toplevel_listener = {
.configure = host_xdg_toplevel_configure,
.close = host_xdg_toplevel_close,
};
/* ---------- host keyboard ---------- */
static void host_kbd_keymap(void *data, struct wl_keyboard *kbd, uint32_t format,
int32_t fd, uint32_t size)
{
(void)data; (void)kbd;
if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 || size == 0)
return;
char *buf = malloc(size + 1);
if (!buf) return;
size_t total = 0;
while (total < (size_t)size) {
ssize_t rd = read(fd, buf + total, size - total);
if (rd > 0) {
total += rd;
} else if (rd < 0 && (errno == EAGAIN || errno == EINTR)) {
struct timespec ts = { .tv_sec = 0, .tv_nsec = 1000000 };
nanosleep(&ts, NULL);
continue;
} else {
break;
}
}
buf[total] = '\0';
if (server.xkb_ctx) {
struct xkb_keymap *km = xkb_keymap_new_from_string(
server.xkb_ctx, buf, XKB_KEYMAP_FORMAT_TEXT_V1, 0);
if (km) {
wlr_keyboard_set_keymap(&server.keyboard, km);
if (server.xkb_keymap) {
xkb_keymap_unref(server.xkb_keymap);
}
server.xkb_keymap = km;
}
}
free(buf);
}
static void host_kbd_enter(void *data, struct wl_keyboard *kbd, uint32_t serial,
struct wl_surface *surface, struct wl_array *keys)
{
(void)data; (void)kbd; (void)serial; (void)surface; (void)keys;
if (server.active)
wlr_seat_keyboard_notify_enter(server.seat,
server.active->base->surface,
server.keyboard.keycodes, server.keyboard.num_keycodes,
&server.keyboard.modifiers);
}
static void host_kbd_leave(void *data, struct wl_keyboard *kbd, uint32_t serial,
struct wl_surface *surface)
{
(void)data; (void)kbd; (void)serial; (void)surface;
wlr_seat_keyboard_notify_clear_focus(server.seat);
}
static void host_kbd_key(void *data, struct wl_keyboard *kbd, uint32_t serial,
uint32_t time, uint32_t key, uint32_t state)
{
(void)data; (void)kbd; (void)serial;
/* Ctrl+PageUp / Ctrl+PageDown cycle tabs */
bool ctrl = (host.host_mods_depressed & (1u << 2)) != 0;
if (state == WL_KEYBOARD_KEY_STATE_PRESSED && ctrl &&
(key == 104 || key == 109)) {
cycle_tab(key == 109 ? 1 : -1);
return;
}
struct wlr_keyboard_key_event event = {
.time_msec = time,
.keycode = key,
.update_state = true,
.state = state,
};
wlr_keyboard_notify_key(&server.keyboard, &event);
wlr_seat_set_keyboard(server.seat, &server.keyboard);
wlr_seat_keyboard_notify_key(server.seat, time, key, state);
}
static void host_kbd_modifiers(void *data, struct wl_keyboard *kbd, uint32_t serial,
uint32_t mods_depressed, uint32_t mods_latched,
uint32_t mods_locked, uint32_t group)
{
(void)data; (void)kbd; (void)serial;
host.host_mods_depressed = mods_depressed;
wlr_keyboard_notify_modifiers(&server.keyboard, mods_depressed,
mods_latched, mods_locked, group);
wlr_seat_keyboard_notify_modifiers(server.seat, &server.keyboard.modifiers);
}
static void host_kbd_repeat_info(void *data, struct wl_keyboard *kbd,
int32_t rate, int32_t delay)
{
(void)data; (void)kbd;
wlr_keyboard_set_repeat_info(&server.keyboard, rate, delay);
}
static const struct wl_keyboard_listener host_kbd_listener = {
.keymap = host_kbd_keymap,
.enter = host_kbd_enter,
.leave = host_kbd_leave,
.key = host_kbd_key,
.modifiers = host_kbd_modifiers,
.repeat_info = host_kbd_repeat_info,
};
/* ---------- host pointer ---------- */
static struct wlr_surface *surface_at(double x, double y,
double *sx, double *sy)
{
struct wlr_scene_node *node = wlr_scene_node_at(
&server.scene->tree.node, x, y, sx, sy);
if (node && node->type == WLR_SCENE_NODE_BUFFER) {
struct wlr_scene_buffer *buffer = wlr_scene_buffer_from_node(node);
struct wlr_scene_surface *scene_surface =
wlr_scene_surface_try_from_buffer(buffer);
if (scene_surface)
return scene_surface->surface;
}
return NULL;
}
static void host_ptr_motion(uint32_t time, double sx, double sy) {
if (!server.seat) return;
double nx = 0.0, ny = 0.0;
struct wlr_surface *surface = surface_at(sx, sy, &nx, &ny);
if (surface) {
wlr_seat_pointer_notify_enter(server.seat, surface, nx, ny);
wlr_seat_pointer_notify_motion(server.seat, time, nx, ny);
} else {
wlr_seat_pointer_notify_clear_focus(server.seat);
}
wlr_seat_pointer_notify_frame(server.seat);
}
static void host_ptr_enter(void *data, struct wl_pointer *p, uint32_t serial,
struct wl_surface *surface, wl_fixed_t sx, wl_fixed_t sy)
{
(void)data; (void)p; (void)serial; (void)surface;
host_ptr_motion(0, wl_fixed_to_double(sx), wl_fixed_to_double(sy));
}
static void host_ptr_leave(void *data, struct wl_pointer *p, uint32_t serial,
struct wl_surface *surface)
{
(void)data; (void)p; (void)serial; (void)surface;
if (server.seat) {
wlr_seat_pointer_notify_clear_focus(server.seat);
wlr_seat_pointer_notify_frame(server.seat);
}
}
static void host_ptr_motion_event(void *data, struct wl_pointer *p, uint32_t time,
wl_fixed_t sx, wl_fixed_t sy)
{
(void)data; (void)p;
host_ptr_motion(time, wl_fixed_to_double(sx), wl_fixed_to_double(sy));
}
static void host_ptr_button(void *data, struct wl_pointer *p, uint32_t serial,
uint32_t time, uint32_t button, uint32_t state)
{
(void)data; (void)p; (void)serial;
if (!server.seat) return;
wlr_seat_pointer_notify_button(server.seat, time, button, state);
wlr_seat_pointer_notify_frame(server.seat);
}
static void host_ptr_axis(void *data, struct wl_pointer *p, uint32_t time,
uint32_t axis, wl_fixed_t value)
{
(void)data; (void)p;
if (!server.seat) return;
wlr_seat_pointer_notify_axis(server.seat, time, axis,
wl_fixed_to_double(value), 0,
host.axis_source,
WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL);
wlr_seat_pointer_notify_frame(server.seat);
}
static void host_ptr_axis_source(void *data, struct wl_pointer *p, uint32_t source)
{
(void)data; (void)p;
host.axis_source = source;
}
static void host_ptr_axis_discrete(void *data, struct wl_pointer *p, uint32_t axis,
int32_t discrete)
{
(void)data; (void)p; (void)axis; (void)discrete;
}
static void host_ptr_axis_stop(void *data, struct wl_pointer *p, uint32_t time,
uint32_t axis)
{
(void)data; (void)p; (void)time; (void)axis;
}
static void host_ptr_frame(void *data, struct wl_pointer *p) {
(void)data; (void)p;
}
static const struct wl_pointer_listener host_ptr_listener = {
.enter = host_ptr_enter,
.leave = host_ptr_leave,
.motion = host_ptr_motion_event,
.button = host_ptr_button,
.axis = host_ptr_axis,
.frame = host_ptr_frame,
.axis_source = host_ptr_axis_source,
.axis_stop = host_ptr_axis_stop,
.axis_discrete = host_ptr_axis_discrete,
};
/* ===================== Host init / EGL ===================== */
static bool host_init(void) {
const char *disp = getenv("WAYLAND_DISPLAY");
if (!disp)
disp = "wayland-0";
host.display = wl_display_connect(NULL);
if (!host.display) {
fprintf(stderr, "tabbed-wlroots: cannot connect to host compositor "
"(WAYLAND_DISPLAY=%s). Run me inside a running Wayland session.\n",
disp);
return false;
}
host.registry = wl_display_get_registry(host.display);
wl_registry_add_listener(host.registry, &host_registry_listener, NULL);
wl_display_roundtrip(host.display);
if (!host.compositor || !host.wm_base) {
fprintf(stderr, "tabbed-wlroots: host lacks compositor/xdg_wm_base\n");
return false;
}
host.surface = wl_compositor_create_surface(host.compositor);
host.xdg_surface = xdg_wm_base_get_xdg_surface(host.wm_base, host.surface);
host.toplevel = xdg_surface_get_toplevel(host.xdg_surface);
xdg_toplevel_set_title(host.toplevel, "tabbed-wlroots");
xdg_surface_add_listener(host.xdg_surface, &host_xdg_surface_listener, NULL);
xdg_toplevel_add_listener(host.toplevel, &host_xdg_toplevel_listener, NULL);
xdg_wm_base_add_listener(host.wm_base, &host_xdg_wm_listener, NULL);
wl_surface_commit(host.surface);
wl_display_roundtrip(host.display);
if (host.seat) {
host.keyboard = wl_seat_get_keyboard(host.seat);
host.pointer = wl_seat_get_pointer(host.seat);
if (host.keyboard)
wl_keyboard_add_listener(host.keyboard, &host_kbd_listener, NULL);
if (host.pointer)
wl_pointer_add_listener(host.pointer, &host_ptr_listener, NULL);
}
/* Try EGL on the host wayland platform */
pfn_eglGetPlatformDisplayEXT = (PFNEGLGETPLATFORMDISPLAYEXTPROC)
eglGetProcAddress("eglGetPlatformDisplayEXT");
if (!pfn_eglGetPlatformDisplayEXT) {
fprintf(stderr, "tabbed-wlroots: no EGL platform support; "
"falling back to software rendering\n");
host.egl_ok = false;
return true;
}
host.egl_display = pfn_eglGetPlatformDisplayEXT(EGL_PLATFORM_WAYLAND_EXT,
host.display, NULL);
if (host.egl_display == EGL_NO_DISPLAY) {
fprintf(stderr, "tabbed-wlroots: EGL display failed; "
"falling back to software rendering\n");
host.egl_ok = false;
return true;
}
if (!eglInitialize(host.egl_display, NULL, NULL)) {
fprintf(stderr, "tabbed-wlroots: eglInitialize failed; "
"falling back to software rendering\n");
host.egl_ok = false;
return true;
}
eglBindAPI(EGL_OPENGL_ES_API);
const EGLint config_attribs[] = {
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 0,
EGL_NONE
};
EGLint ncfg = 0;
if (!eglChooseConfig(host.egl_display, config_attribs, &host.egl_config,
1, &ncfg) || ncfg < 1) {
fprintf(stderr, "tabbed-wlroots: no EGL config; software fallback\n");
host.egl_ok = false;
return true;
}
const EGLint ctx_attribs[] = {
EGL_CONTEXT_CLIENT_VERSION, 2,
EGL_NONE
};
host.egl_context = eglCreateContext(host.egl_display, host.egl_config,
EGL_NO_CONTEXT, ctx_attribs);
if (host.egl_context == EGL_NO_CONTEXT) {
fprintf(stderr, "tabbed-wlroots: EGL context failed; software fallback\n");
host.egl_ok = false;
return true;
}
host.width = 1280;
host.height = 800;
host.egl_window = wl_egl_window_create(host.surface, host.width, host.height);
host.egl_surface = eglCreateWindowSurface(host.egl_display, host.egl_config,
(EGLNativeWindowType)host.egl_window, NULL);
if (host.egl_surface == EGL_NO_SURFACE) {
fprintf(stderr, "tabbed-wlroots: EGL window surface failed; software fallback\n");
host.egl_ok = false;
return true;
}
if (!eglMakeCurrent(host.egl_display, host.egl_surface, host.egl_surface,
host.egl_context)) {
fprintf(stderr, "tabbed-wlroots: eglMakeCurrent failed; software fallback\n");
host.egl_ok = false;
return true;
}
eglSwapInterval(host.egl_display, 0);
pfn_eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)egl_proc("eglCreateImageKHR");
pfn_eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)egl_proc("eglDestroyImageKHR");
pfn_glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)
egl_proc("glEGLImageTargetTexture2DOES");
if (!gl_setup()) {
fprintf(stderr, "tabbed-wlroots: GL setup failed; software fallback\n");
host.egl_ok = false;
return true;
}
host.egl_ok = true;
fprintf(stderr, "tabbed-wlroots: EGL (GPU) rendering enabled\n");
return true;
}
/* ===================== Scene buffer -> host blit ===================== */
static bool import_scene_buffer(struct wlr_buffer *buf, GLuint tex) {
struct wlr_dmabuf_attributes attrs;
if (wlr_buffer_get_dmabuf(buf, &attrs)) {
if (attrs.n_planes < 1) return false;
EGLint att[48];
int n = 0;
att[n++] = EGL_WIDTH; att[n++] = attrs.width;
att[n++] = EGL_HEIGHT; att[n++] = attrs.height;
att[n++] = EGL_LINUX_DRM_FOURCC_EXT; att[n++] = (EGLint)attrs.format;
for (int p = 0; p < attrs.n_planes; p++) {
att[n++] = EGL_DMA_BUF_PLANE0_FD_EXT + p * 3; att[n++] = attrs.fd[p];
att[n++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT + p * 3; att[n++] = (EGLint)attrs.offset[p];
att[n++] = EGL_DMA_BUF_PLANE0_PITCH_EXT + p * 3; att[n++] = (EGLint)attrs.stride[p];
}
if (attrs.modifier != DRM_FORMAT_MOD_INVALID) {
att[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT;
att[n++] = (EGLint)(attrs.modifier & 0xffffffff);
att[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT;
att[n++] = (EGLint)(attrs.modifier >> 32);
}
att[n++] = EGL_NONE;
EGLImageKHR img = pfn_eglCreateImageKHR(host.egl_display, EGL_NO_CONTEXT,
EGL_LINUX_DMA_BUF_EXT,
(EGLClientBuffer)NULL, att);
if (img == EGL_NO_IMAGE_KHR) {
fprintf(stderr, "[import] eglCreateImageKHR failed (format 0x%x "
"modifier 0x%llx)\n", attrs.format,
(unsigned long long)attrs.modifier);
return false;
}
glBindTexture(GL_TEXTURE_2D, tex);
pfn_glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, img);
pfn_eglDestroyImageKHR(host.egl_display, img);
return true;
}
/* Fallback for non-dmabuf buffers (on a headless backend the allocator
yields shm buffers). Upload via CPU read so shm clients and the shm
swapchain both render into the host window. */
void *data;
uint32_t fmt;
size_t stride;
if (!wlr_buffer_begin_data_ptr_access(buf, WLR_BUFFER_DATA_PTR_ACCESS_READ,
&data, &fmt, &stride)) {
fprintf(stderr, "[import] buffer is neither dmabuf nor CPU-readable\n");
return false;
}
glBindTexture(GL_TEXTURE_2D, tex);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
switch (fmt) {
case DRM_FORMAT_XRGB8888:
case DRM_FORMAT_ARGB8888:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, buf->width, buf->height, 0,
GL_BGRA_EXT, GL_UNSIGNED_BYTE, data);
break;
default:
fprintf(stderr, "[import] unsupported CPU fallback format 0x%x\n", fmt);
wlr_buffer_end_data_ptr_access(buf);
return false;
}
wlr_buffer_end_data_ptr_access(buf);
return true;
}
/* Dump the host window (the EGL surface we present) to a PPM, on demand.
Triggered with `kill -USR1 <pid>` so the current frame can be inspected
without any screen capture. */
static volatile sig_atomic_t dump_requested = 0;
static void sigusr1_handler(int sig) {
(void)sig;
dump_requested = 1;
}
static void dump_window(int seq) {
int w = host.width, h = host.height;
if (w <= 0 || h <= 0) return;
size_t npix = (size_t)w * h;
uint8_t *px = malloc(npix * 4);
if (!px) return;
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px);
char path[64];
snprintf(path, sizeof(path), "/tmp/window-%04d.ppm", seq);
FILE *f = fopen(path, "w");
if (f) {
fprintf(f, "P6\n%d %d\n255\n", w, h);
for (int y = h - 1; y >= 0; y--) {
const uint8_t *row = px + (size_t)y * w * 4;
for (int x = 0; x < w; x++) {
fputc(row[x * 4], f);
fputc(row[x * 4 + 1], f);
fputc(row[x * 4 + 2], f);
}
}
fclose(f);
fprintf(stderr, "[dump] wrote %s (%dx%d)\n", path, w, h);
}
free(px);
}
static void blit_scene_buffer(struct wlr_buffer *buf) {
int w = host.width, h = host.height;
glViewport(0, 0, w, h);
glClearColor(0.13f, 0.13f, 0.13f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
if (buf && import_scene_buffer(buf, host.import_tex)) {
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
draw_texture_rect(host.import_tex, buf->width, buf->height,
0, 0, buf->width, buf->height, 0, 0, w, h);
glDisable(GL_BLEND);
}
if (dump_requested) {
dump_requested = 0;
static int seq;
dump_window(++seq);
}
eglSwapBuffers(host.egl_display, host.egl_surface);
}
static void render_and_present(void) {
if (!host.egl_ok || !host.configured) return;
if (!server.scene_output || !server.swapchain) return;
struct wlr_scene_output_state_options options = {
.swapchain = server.swapchain,
};
struct wlr_output_state state;
wlr_output_state_init(&state);
bool rendered = wlr_scene_output_build_state(server.scene_output, &state,
&options);
if (!rendered || !state.buffer) {
fprintf(stderr, "[render] build_state failed\n");
wlr_output_state_finish(&state);
goto frame_done;
}
blit_scene_buffer(state.buffer);
if (!wlr_output_commit_state(server.output, &state))
fprintf(stderr, "[render] output commit failed\n");
frame_done:
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
wlr_scene_output_send_frame_done(server.scene_output, &now);
wlr_output_state_finish(&state);
}
/* ===================== Launch client ===================== */
static void launch_client(int idx) {
pid_t pid = fork();
if (pid == 0) {
const char *xdg_runtime = getenv("XDG_RUNTIME_DIR");
if (!xdg_runtime) xdg_runtime = "/tmp";
setenv("WAYLAND_DISPLAY", tabbed_socket, 1);
setenv("XDG_RUNTIME_DIR", xdg_runtime, 1);
setenv("GDK_BACKEND", "wayland", 1);
setenv("QT_QPA_PLATFORM", "wayland", 1);
unsetenv("DISPLAY");
execlp("/bin/sh", "sh", "-c", server.apps[idx]->cmd, NULL);
_exit(127);
}
if (pid > 0)
server.apps[idx]->pid = pid;
}
/* ===================== Crash handler ===================== */
static void crash_handler(int sig, siginfo_t *info, void *ctx) {
(void)sig; (void)info; (void)ctx;
void *bt[64];
int n = backtrace(bt, 64);
write(2, "\n=== BACKTRACE ===\n", 19);
backtrace_symbols_fd(bt, n, 2);
_exit(139);
}
static void sigchld_handler(int sig) {
(void)sig;
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0)
fprintf(stderr, "[child] pid %d exited status 0x%x\n", pid, status);
}
/* ===================== Main ===================== */
int main(int argc, char *argv[]) {
snprintf(tabbed_socket, sizeof(tabbed_socket), "tabbed-wayland-%d",
(int)getpid());
struct sigaction crash_sa = {
.sa_sigaction = crash_handler,
.sa_flags = SA_SIGINFO | SA_NODEFER,
};
sigaction(SIGSEGV, &crash_sa, NULL);
sigaction(SIGABRT, &crash_sa, NULL);
signal(SIGCHLD, sigchld_handler);
signal(SIGUSR1, sigusr1_handler);
wlr_log_init(WLR_DEBUG, NULL);
server.wl_display = wl_display_create();
if (!server.wl_display) {
fprintf(stderr, "tabbed-wlroots: failed to create Wayland display\n");
return 1;
}
server.loop = wl_display_get_event_loop(server.wl_display);
server.backend = wlr_headless_backend_create(server.loop);
if (!server.backend) {
fprintf(stderr, "tabbed-wlroots: failed to create headless backend\n");
return 1;
}
server.renderer = wlr_renderer_autocreate(server.backend);
if (!server.renderer) {
fprintf(stderr, "tabbed-wlroots: no GPU renderer available\n");
return 1;
}
wlr_renderer_init_wl_display(server.renderer, server.wl_display);
server.allocator = wlr_allocator_autocreate(server.backend, server.renderer);
if (!server.allocator) {
fprintf(stderr, "tabbed-wlroots: failed to create allocator\n");
return 1;
}
wlr_compositor_create(server.wl_display, 6, server.renderer);
wlr_subcompositor_create(server.wl_display);
wlr_shm_create_with_renderer(server.wl_display, 1, server.renderer);
wlr_linux_dmabuf_v1_create_with_renderer(server.wl_display, 5, server.renderer);
wlr_data_device_manager_create(server.wl_display);
server.scene = wlr_scene_create();
server.xdg_shell = wlr_xdg_shell_create(server.wl_display, 6);
server.seat = wlr_seat_create(server.wl_display, "tabbed");
wlr_seat_set_capabilities(server.seat,
WL_SEAT_CAPABILITY_POINTER | WL_SEAT_CAPABILITY_KEYBOARD);
/* virtual keyboard fed from the host keyboard */
static const struct wlr_keyboard_impl keyboard_impl = {
.name = "tabbed-keyboard",
};
wlr_keyboard_init(&server.keyboard, &keyboard_impl, "tabbed-keyboard");
/* fallback keymap until the host sends one */
server.xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if (server.xkb_ctx) {
struct xkb_rule_names names = {
.rules = NULL, .model = "pc105",
.layout = "us", .variant = NULL, .options = NULL,
};
server.xkb_keymap = xkb_keymap_new_from_names(
server.xkb_ctx, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
if (server.xkb_keymap)
wlr_keyboard_set_keymap(&server.keyboard, server.xkb_keymap);
}
wlr_seat_set_keyboard(server.seat, &server.keyboard);
server.new_output.notify = handle_new_output;
wl_signal_add(&server.backend->events.new_output, &server.new_output);
server.new_toplevel.notify = handle_new_toplevel;
wl_signal_add(&server.xdg_shell->events.new_toplevel, &server.new_toplevel);
/* headless output; new_output fires during backend start */
host.width = 1280;
host.height = 800;
wlr_headless_add_output(server.backend, host.width, host.height);
if (!wlr_backend_start(server.backend)) {
fprintf(stderr, "tabbed-wlroots: failed to start backend\n");
return 1;
}
if (wl_display_add_socket(server.wl_display, tabbed_socket) < 0) {
fprintf(stderr, "tabbed-wlroots: failed to add socket '%s': %s\n",
tabbed_socket, strerror(errno));
return 1;
}
/* Now connect to the host compositor and present inside it. */
if (!host_init()) {
fprintf(stderr, "tabbed-wlroots: could not start nested session\n");
return 1;
}
printf("Wayland socket: %s\n", tabbed_socket);
int total_apps = argc - 1;
if (total_apps == 0) {
fprintf(stderr, "usage: %s <prog> [prog...]\n", argv[0]);
return 1;
}
if (total_apps > MAX_APPS) total_apps = MAX_APPS;
for (int i = 0; i < total_apps; i++) {
struct tabbed_app *app = calloc(1, sizeof(*app));
app->cmd = strdup(argv[i + 1]);
app->pid = -1;
server.apps[i] = app;
}
server.num_apps = total_apps;
for (int i = 0; i < server.num_apps; i++)
launch_client(i);
fprintf(stderr, "tabbed-wlroots: running. "
"Ctrl+PageUp/PageDown switches tabs.\n");
/* Main event loop: service both the server and the host displays */
int server_fd = wl_event_loop_get_fd(server.loop);
int host_fd = wl_display_get_fd(host.display);
for (;;) {
wl_display_dispatch_pending(host.display);
/* Never block in a host read: use the non-blocking prepare/read
pattern so the server loop (headless frame timer) is always
serviced while waiting on the host socket. */
if (wl_display_prepare_read(host.display) == 0) {
wl_display_flush(host.display);
struct pollfd fds[2] = {
{ .fd = server_fd, .events = POLLIN },
{ .fd = host_fd, .events = POLLIN },
};
int ret;
do {
ret = poll(fds, 2, -1);
} while (ret < 0 && errno == EINTR);
if (ret > 0 && (fds[1].revents & (POLLIN | POLLERR)))
wl_display_read_events(host.display);
else
wl_display_cancel_read(host.display);
if (ret > 0 && (fds[0].revents & (POLLIN | POLLERR)))
wl_event_loop_dispatch(server.loop, 0);
} else {
wl_event_loop_dispatch(server.loop, 0);
}
wl_display_flush_clients(server.wl_display);
wl_display_dispatch_pending(host.display);
/* The headless output fires a frame signal at ~60Hz; render then
(also when a redraw was requested directly). */
if (frame_pending || needs_redraw) {
frame_pending = false;
needs_redraw = false;
render_and_present();
}
wl_display_flush(host.display);
}
for (int i = 0; i < server.num_apps; i++) {
if (server.apps[i]->pid > 0)
kill(server.apps[i]->pid, SIGTERM);
free(server.apps[i]->cmd);
free(server.apps[i]);
}
if (server.xkb_keymap) xkb_keymap_unref(server.xkb_keymap);
if (server.xkb_ctx) xkb_context_unref(server.xkb_ctx);
return 0;
}