8369 lines
331 KiB
C
8369 lines
331 KiB
C
/* kick.c -- nested kick 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; a client's fullscreen request is
|
|
* honored and mirrors onto the host window, and vice versa.
|
|
*/
|
|
|
|
#define _GNU_SOURCE
|
|
|
|
#include <errno.h>
|
|
#include <execinfo.h>
|
|
#include <fcntl.h>
|
|
#include <linux/memfd.h>
|
|
#include <poll.h>
|
|
#include <signal.h>
|
|
#include <stdarg.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 <libdecor.h>
|
|
|
|
#include "xdg-shell-client.h"
|
|
#include "wlr-data-control-client.h"
|
|
|
|
#include <EGL/egl.h>
|
|
#include <EGL/eglext.h>
|
|
#include <GLES2/gl2.h>
|
|
#include <GLES2/gl2ext.h>
|
|
|
|
#include <xkbcommon/xkbcommon.h>
|
|
|
|
/* xkbcommon 1.6 renamed/removed the Mod4 macro: >= 1.7 defines only the
|
|
legacy XKB_MOD_NAME_LOGO (kept for compat), < 1.7 defines the modern
|
|
XKB_MOD_NAME_MOD4. Prefer the modern name where it exists. */
|
|
#ifdef XKB_MOD_NAME_MOD4
|
|
#define XKB_PREFIX_MOD4_NAME XKB_MOD_NAME_MOD4
|
|
#else
|
|
#define XKB_PREFIX_MOD4_NAME XKB_MOD_NAME_LOGO
|
|
#endif
|
|
|
|
#include <ft2build.h>
|
|
#include FT_FREETYPE_H
|
|
|
|
#include <cairo.h>
|
|
#include <librsvg/rsvg.h>
|
|
#include <png.h>
|
|
|
|
#include <wlr/backend.h>
|
|
#include <wlr/backend/headless.h>
|
|
#include <wlr/backend/session.h>
|
|
#include <wlr/interfaces/wlr_keyboard.h>
|
|
#include <wlr/render/allocator.h>
|
|
#include <wlr/render/drm_format_set.h>
|
|
#include <wlr/render/egl.h>
|
|
#include <wlr/render/gles2.h>
|
|
#include <wlr/render/pass.h>
|
|
#include <wlr/render/swapchain.h>
|
|
#include <wlr/render/wlr_renderer.h>
|
|
#include <wlr/render/wlr_texture.h>
|
|
#include <wlr/types/wlr_buffer.h>
|
|
#include <wlr/types/wlr_compositor.h>
|
|
#include <wlr/types/wlr_cursor.h>
|
|
#include <wlr/types/wlr_cursor_shape_v1.h>
|
|
#include <wlr/types/wlr_data_control_v1.h>
|
|
#include <wlr/types/wlr_data_device.h>
|
|
#include <wlr/types/wlr_input_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_output_layout.h>
|
|
#include <wlr/types/wlr_pointer.h>
|
|
#include <wlr/types/wlr_pointer_constraints_v1.h>
|
|
#include <wlr/types/wlr_presentation_time.h>
|
|
#include <wlr/types/wlr_primary_selection.h>
|
|
#include <wlr/types/wlr_primary_selection_v1.h>
|
|
#include <wlr/types/wlr_relative_pointer_v1.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_viewporter.h>
|
|
#include <wlr/types/wlr_xcursor_manager.h>
|
|
#include <wlr/types/wlr_xdg_output_v1.h>
|
|
#include <wlr/types/wlr_xdg_shell.h>
|
|
#include <wlr/util/log.h>
|
|
|
|
/* XWayland support is on when the linked wlroots was built with it
|
|
(WLR_HAS_XWAYLAND in the public <wlr/config.h>) and the xcb dev headers
|
|
are available (KICK_HAVE_XCB from meson.build). Anything missing and the
|
|
X11 branches below compile away. */
|
|
#include <wlr/config.h>
|
|
#if defined(WLR_HAS_XWAYLAND) && defined(KICK_HAVE_XCB)
|
|
#define HAVE_XWAYLAND 1
|
|
#else
|
|
#define HAVE_XWAYLAND 0
|
|
#endif
|
|
|
|
#if HAVE_XWAYLAND
|
|
#include <wlr/xwayland/xwayland.h>
|
|
#endif
|
|
|
|
/* server.apps grows as needed; there is no tab limit. */
|
|
|
|
/* ===================== Server-side (wlroots) state ===================== */
|
|
|
|
struct kick_app {
|
|
char *cmd;
|
|
pid_t pid;
|
|
/* Process group the *connected* client lives in. Command-line commands
|
|
are forked all at once at startup (see launch_all_cli), so a CLI tab's
|
|
slot is claimed by whichever command's client connects -- matched by
|
|
process group at claim time, not by order; sub-windows and external
|
|
clients claim later. A close must signal the group recorded at claim
|
|
time rather than the launcher's pid. */
|
|
pid_t pgid;
|
|
/* PID of the *connected* client (from claim-time credentials). Used as
|
|
the kill target when the client belongs to a process group we do not
|
|
own (e.g. one started from the compositor's own shell/terminal), where
|
|
signaling the whole group would kill the launcher's session too. */
|
|
pid_t client_pid;
|
|
struct wlr_xdg_toplevel *toplevel;
|
|
#if HAVE_XWAYLAND
|
|
/* X11 tab: an Xwayland surface instead of an xdg toplevel (toplevel is
|
|
NULL, is_x11 set). Shared tab logic goes through the app_* view
|
|
helpers; only claim/scene wiring differs per surface kind. */
|
|
bool is_x11;
|
|
struct wlr_xwayland_surface *xsurface;
|
|
#endif
|
|
/* Container for this tab's whole window stack: the main surface and
|
|
every stacked sub-window hang off it as siblings. Disabled whenever
|
|
the tab is hidden; show_current() keeps exactly one child enabled so
|
|
only the current window is drawn or hit-tested. */
|
|
struct wlr_scene_tree *stack;
|
|
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;
|
|
#if HAVE_XWAYLAND
|
|
/* Extra X11-only listeners: surface association and the ICCCM requests
|
|
with no xdg counterpart. map/commit/destroy/request_maximize/
|
|
request_fullscreen/request_minimize are shared with the xdg path. */
|
|
struct wl_listener xs_associate;
|
|
struct wl_listener xs_dissociate;
|
|
struct wl_listener xs_request_configure;
|
|
struct wl_listener xs_request_activate;
|
|
struct wl_listener xs_request_close;
|
|
struct wl_listener xs_set_title;
|
|
struct wl_listener xs_set_class;
|
|
/* map/commit listen on the associated wlr_surface; tracked so destroy
|
|
never removes links that dissociate already removed. */
|
|
bool xs_surface_linked;
|
|
#endif
|
|
|
|
/* Resolved overlay icon (a GL texture in host pixel space). */
|
|
GLuint icon_tex;
|
|
int icon_w, icon_h;
|
|
|
|
/* Resolved overlay icon for standalone mode (a wlroots texture). */
|
|
struct wlr_texture *icon_wlr;
|
|
bool icon_resolved; /* true once a load attempt has been made */
|
|
|
|
/* Last cursor-shape-v1 shape requested by this app's client (0 = none
|
|
requested yet). Reset against the active tab on focus change so a
|
|
switched-from tab's shape never bleeds onto the next tab. */
|
|
int cursor_shape;
|
|
|
|
/* Slot awaiting its toplevel. True from creation until a client claims
|
|
the slot in handle_new_toplevel (matched by launch process group), so
|
|
a CLI tab or a fresh Alt+Shift+T terminal is live-but-black while its
|
|
command starts: it shows in the overlay with its command line as the
|
|
title, and cycling can land on it (backdrop + title). Cleared on
|
|
claim, and again when a black tab is closed. A slot without a
|
|
toplevel and without pending_claim would only exist transiently, so
|
|
closed tabs leave no holes: remove_app() drops them outright. */
|
|
bool pending_claim;
|
|
|
|
/* Where a tab's slot came from. ORIGIN_CLI slots hold the command-line
|
|
commands and are forked all at once at startup (see launch_all_cli),
|
|
so the tab order mirrors the command line. ORIGIN_ADD_TAB is an
|
|
Alt+Shift+T terminal. ORIGIN_SUB is a second toplevel from an
|
|
already-connected client and is glued right of its parent tab.
|
|
ORIGIN_EXTERNAL is a client we did not launch (e.g. started from our
|
|
own shell). */
|
|
enum { ORIGIN_CLI, ORIGIN_ADD_TAB, ORIGIN_SUB, ORIGIN_EXTERNAL } origin;
|
|
/* For ORIGIN_SUB: the tab owning the connected client this window
|
|
belongs to (the "parent" tab its slot is glued right of). */
|
|
struct kick_app *parent_app;
|
|
|
|
/* The user closed this tab (Alt+Delete). While set, cycle_tab skips the
|
|
slot and the SIGKILL escalation timer (close_timer) may still be armed;
|
|
cleared once the toplevel is destroyed or the slot is reused. */
|
|
bool closing;
|
|
struct wl_event_source *close_timer;
|
|
|
|
/* The tab's window stack: the wrapped main surface first (see
|
|
stack_wrap_main), then any stacked sub-windows (--sub-windows
|
|
stack) oldest first, newest arrivals at the tail. focus_sub is
|
|
the member that currently holds the keyboard while this tab is
|
|
active -- the rotation's cursor; it always points at a member
|
|
once the tab has one to show. */
|
|
struct wl_list sub_windows;
|
|
struct kick_sub_window *focus_sub;
|
|
|
|
/* Last overlay row title / icon app_id drawn for this tab. The row
|
|
shows whatever member the tab is currently displaying, so these
|
|
caches let the overlay log one line per actual change and
|
|
re-resolve the icon when the effective app_id moves underneath. */
|
|
char row_title[128];
|
|
char icon_app_id[128];
|
|
};
|
|
|
|
/* A stacked sub-window: a second toplevel from a client whose tab already
|
|
exists (--sub-windows stack). Its scene tree is a sibling of the owner
|
|
tab's main surface under the tab's stack container, so it shows and
|
|
hides with the tab; show_current() keeps only the current window's node
|
|
enabled, so the sub paints alone while it is on top. */
|
|
struct kick_sub_window {
|
|
/* True for the wrapper that represents a tab's own main surface in
|
|
its window stack: every rotation/visibility/title path then walks
|
|
one plain list of windows with no privileged entry. The wrapper
|
|
carries no listeners -- the tab-level map/commit/destroy handlers
|
|
stay authoritative -- and its liveness mirrors app_has_window(). */
|
|
bool is_main;
|
|
struct wlr_xdg_toplevel *toplevel;
|
|
#if HAVE_XWAYLAND
|
|
bool is_x11;
|
|
struct wlr_xwayland_surface *xsurface;
|
|
#endif
|
|
struct kick_app *owner;
|
|
struct wlr_scene_tree *tree;
|
|
bool mapped;
|
|
struct wl_listener destroy;
|
|
struct wl_listener map;
|
|
struct wl_listener unmap;
|
|
struct wl_listener commit;
|
|
#if HAVE_XWAYLAND
|
|
struct wl_listener xs_associate;
|
|
bool xs_surface_linked;
|
|
#endif
|
|
struct wl_list link; /* owner->sub_windows */
|
|
};
|
|
|
|
struct kick_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;
|
|
/* Transparent full-output sentinel used to force whole-output damage on
|
|
overlay transitions; see standalone_overlay_damage(). */
|
|
struct wlr_scene_rect *overlay_damage_rect;
|
|
/* Standalone-only cursor: a real wlr_cursor so the pointer is visible
|
|
(hardware cursor plane on DRM, scene-composited on headless). */
|
|
struct wlr_cursor *cursor;
|
|
struct wlr_xcursor_manager *xcursor_mgr;
|
|
struct wlr_output_layout *output_layout;
|
|
struct wlr_seat *seat;
|
|
struct wlr_xdg_shell *xdg_shell;
|
|
/* Kept for wlr_xwayland_create(), which needs the compositor global. */
|
|
struct wlr_compositor *compositor;
|
|
#if HAVE_XWAYLAND
|
|
/* XWayland: wlroots' XWM plus a lazy Xwayland server (spawned on the
|
|
first X11 client connect). or_windows holds the override-redirect
|
|
windows (menus, tooltips) that float above the tab trees. */
|
|
struct wlr_xwayland *xwayland;
|
|
struct wl_listener xwayland_new_surface;
|
|
struct wl_listener xwayland_ready;
|
|
struct wl_listener xwayland_destroy;
|
|
struct wl_list or_windows;
|
|
/* The tab whose surface held the keyboard before an override-redirect
|
|
window took it; restored when that window unmaps. */
|
|
struct kick_app *or_focus_owner;
|
|
#endif
|
|
/* Client cursor-shape-v1 requests (wlr_cursor_shape_manager_v1). The
|
|
manager is passive: it emits request_set_shape and we either apply the
|
|
shape to our standalone cursor or forward it to the host pointer. */
|
|
struct wlr_cursor_shape_manager_v1 *shape_mgr;
|
|
struct wl_listener shape_listener;
|
|
/* Protocol managers that only need to outlive the display: presentation
|
|
(frame pacing), xdg-output (logical output geometry) and viewporter
|
|
(wl_viewport buffer scaling). Kept as fields so the globals are not
|
|
garbage collected while clients still hold resources. */
|
|
struct wlr_presentation *presentation;
|
|
struct wlr_xdg_output_manager_v1 *xdg_output_mgr;
|
|
struct wlr_viewporter *viewporter;
|
|
/* Primary selection (zwp_primary_selection_v1). Clients request the
|
|
selection on their seat device; the request handler below is what
|
|
actually installs it on the seat, mirroring the clipboard path. */
|
|
struct wlr_primary_selection_v1_device_manager *primary_selection_mgr;
|
|
struct wl_listener request_set_primary_selection;
|
|
/* Pointer constraints + relative pointer (zwp_pointer_constraints_v1 /
|
|
zwp_relative_pointer_v1). Only the focused surface's constraint is
|
|
active; its region lives in the surface's local space, so the surface's
|
|
scene origin is cached when the constraint activates. */
|
|
struct wlr_pointer_constraints_v1 *pointer_constraints;
|
|
struct wlr_relative_pointer_manager_v1 *relative_pointer_mgr;
|
|
struct wlr_pointer_constraint_v1 *active_constraint;
|
|
struct wl_listener constraint_destroy;
|
|
struct wl_listener seat_pointer_focus;
|
|
double constraint_ox, constraint_oy;
|
|
double lock_x, lock_y;
|
|
double last_ptr_x, last_ptr_y;
|
|
/* Last cursor shape applied (standalone) or forwarded (nested), as a
|
|
wp_cursor_shape_device_v1 shape value; 0 = nothing applied yet. */
|
|
int cursor_shape;
|
|
struct wlr_keyboard keyboard;
|
|
struct wlr_swapchain *swapchain;
|
|
/* Self-pipe used to wake the idle event loop from the SIGUSR1 dump
|
|
handler (async-signal-safe write); the drain callback runs in the main
|
|
loop and the render gate consumes needs_redraw. */
|
|
struct wl_event_source *sigpipe_src;
|
|
int sig_pipe[2];
|
|
|
|
struct wl_listener new_output;
|
|
struct wl_listener new_toplevel;
|
|
struct wl_listener new_popup;
|
|
struct wl_listener frame;
|
|
struct wl_listener output_destroy;
|
|
struct wl_event_source *ctl_source;
|
|
|
|
/* The active tab, by slot. Set to the last command-line command's slot
|
|
at startup, so that tab is active -- black, with its command line as
|
|
the overlay title -- from the very first frame and the user only waits
|
|
for it. A slot that is active while still pending (no toplevel yet)
|
|
shows the black backdrop and clear seat focus until it maps. */
|
|
struct kick_app *active_app;
|
|
struct kick_app **apps;
|
|
int num_apps;
|
|
int apps_cap;
|
|
|
|
struct xkb_context *xkb_ctx;
|
|
struct xkb_keymap *xkb_keymap;
|
|
struct xkb_state *xkb_state;
|
|
/* Modifier index of the tab-management prefix (Alt or Super/Mod4,
|
|
per --modifier). */
|
|
xkb_mod_index_t xkb_prefix_mod;
|
|
|
|
/* A client's fullscreen request is honored and mirrored onto the host
|
|
window, and vice versa. */
|
|
bool clients_fullscreen;
|
|
|
|
/* Standalone (real output) mode: the output is driven directly by a
|
|
native backend (DRM/headless) instead of being blitted into a host
|
|
window. Set from KICK_MODE=standalone or a forced WLR_BACKENDS. */
|
|
bool standalone;
|
|
bool standalone_headless;
|
|
struct wlr_session *session;
|
|
struct wl_listener new_input;
|
|
struct wlr_texture *overlay_atlas_wlr; /* standalone: overlaid atlas */
|
|
|
|
/* Standalone native keyboard devices (one entry per libinput keyboard;
|
|
see standalone_kbdev below). */
|
|
struct wl_list kbdevs;
|
|
/* Standalone native pointer devices (one entry per libinput pointer;
|
|
see standalone_ptrdev below). */
|
|
struct wl_list ptrdevs;
|
|
};
|
|
|
|
/* Requested output resolution from --screen-mode WxH[@Hz]; 0/0/0 means "let
|
|
the output pick its preferred mode". Refresh is stored in mHz. */
|
|
static int mode_req_w, mode_req_h;
|
|
static int mode_req_refresh;
|
|
|
|
/* The command-line command to start focused (--start-cmd-index), 0-based as
|
|
soon as the (1-based) user value is parsed. -1 (unset) means the last
|
|
command, keeping the startup contract. */
|
|
static int start_cmd_index = -1;
|
|
|
|
/* ===================== Host-side (nested) state ===================== */
|
|
|
|
/* One cached ASCII glyph in the overlay's font atlas texture. */
|
|
struct overlay_glyph {
|
|
int ax, ay; /* atlas pixel origin */
|
|
int pw, ph; /* glyph bitmap size */
|
|
int adv; /* advance width, pixels */
|
|
int top; /* y offset of the glyph top from the row top */
|
|
};
|
|
|
|
struct kick_host {
|
|
struct wl_display *display;
|
|
struct wl_registry *registry;
|
|
struct wl_compositor *compositor;
|
|
struct wl_surface *surface;
|
|
struct xdg_wm_base *wm_base;
|
|
/* The nested window is a libdecor frame around our wl_surface: libdecor
|
|
owns the xdg_surface/xdg_toplevel plus the window decorations,
|
|
negotiating server-side decoration when the host offers it and drawing
|
|
its own titlebar (with move/resize/close) otherwise (GNOME). */
|
|
struct libdecor *libdecor;
|
|
struct libdecor_frame *frame;
|
|
struct wl_seat *seat;
|
|
struct wl_keyboard *keyboard;
|
|
struct wl_pointer *pointer;
|
|
/* Host cursor-shape-v1: forward a nested client's requested cursor shape
|
|
to the host compositor (which draws the cursor over our window). The
|
|
device binds to the host pointer and goes inert with it, so it is
|
|
recreated whenever pointer capability is re-gained. */
|
|
struct wp_cursor_shape_manager_v1 *shape_mgr;
|
|
struct wp_cursor_shape_device_v1 *shape_dev;
|
|
uint32_t ptr_serial; /* latest host wl_pointer.enter serial */
|
|
uint32_t seat_name; /* registry name of the bound wl_seat global */
|
|
bool registry_synced; /* initial registry scan completed */
|
|
bool configured;
|
|
bool fullscreen;
|
|
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;
|
|
int32_t axis_discrete[2];
|
|
/* GL program */
|
|
GLuint gl_program;
|
|
GLint gl_pos_loc, gl_uv_loc, gl_tex_loc, gl_color_loc;
|
|
GLuint import_tex;
|
|
/* Alt-held tab-title overlay */
|
|
bool overlay_visible;
|
|
GLuint overlay_atlas_tex;
|
|
GLuint overlay_white_tex;
|
|
int overlay_atlas_w, overlay_atlas_h;
|
|
struct overlay_glyph overlay_glyphs[95]; /* chars 32..126 */
|
|
int overlay_font_h; /* ascent + descent, pixels */
|
|
/* Present gating: at most one buffer aimed at the host compositor at a
|
|
time, paced by the host surface's wl_callback. */
|
|
struct wl_callback *frame_cb;
|
|
bool present_pending;
|
|
bool back_dirty;
|
|
/* Keybind repeat for the captured tab-switch binding. The host
|
|
compositor only sends one physical press, so we drive the repeat
|
|
ourselves with a timer (fixed delay/rate), like labwc does for its
|
|
own keybinds. */
|
|
struct wl_event_source *repeat_timer;
|
|
uint32_t repeat_key;
|
|
int repeat_dir;
|
|
/* What each repeat tick should do: REPEAT_CYCLE switches tabs,
|
|
REPEAT_REORDER moves the active tab within the list, REPEAT_SUBCYCLE
|
|
rotates the active tab's stacked windows. */
|
|
enum { REPEAT_CYCLE, REPEAT_REORDER, REPEAT_SUBCYCLE } repeat_kind;
|
|
};
|
|
|
|
static struct kick_server server;
|
|
static struct kick_host host;
|
|
static char kick_socket[64];
|
|
static char socket_path[256];
|
|
static volatile sig_atomic_t needs_redraw;
|
|
static bool frame_pending;
|
|
static bool shutting_down;
|
|
|
|
/* Terminal launched for the Alt+Shift+T "new tab" binding. Defaults to
|
|
alacritty (sane no-config font size, unlike foot's 8pt); overridable via
|
|
the $TERMINAL environment variable. The value is used verbatim by sh -c,
|
|
so it may carry arguments. */
|
|
static const char *term_cmd = "alacritty";
|
|
static bool term_key_held; /* Alt+Shift+T currently down (repeat latch) */
|
|
static bool delete_key_held; /* prefix+Delete currently down (repeat latch) */
|
|
|
|
/* The modifier prefix for the tab-management chords: hold = show overlay,
|
|
Up/Down = cycle, Shift+Up/Down = reorder, Delete = close. Defaults to
|
|
Alt; --modifier super selects Super (Mod4), which is a good fit for
|
|
standalone/DRM and labwc-style hosts that do not reserve it the way
|
|
GNOME does. */
|
|
enum kick_prefix { PREFIX_ALT, PREFIX_SUPER };
|
|
static enum kick_prefix tab_prefix = PREFIX_ALT;
|
|
|
|
/* --sub-windows: how a second toplevel from an already-connected client
|
|
appears. 'tabs' (default) glues a slot right of its parent tab;
|
|
'stack' renders it above the parent tab's surface instead -- it shares
|
|
the tab's visibility and takes keyboard focus while that tab is shown,
|
|
and never appears in cycling or the overlay. */
|
|
static bool stack_sub_windows;
|
|
|
|
/* ===================== Clipboard bridge ===================== */
|
|
|
|
enum bridge_backend {
|
|
BRIDGE_BACKEND_NONE = 0,
|
|
BRIDGE_BACKEND_DATA_CONTROL,
|
|
BRIDGE_BACKEND_WL_DATA_DEVICE,
|
|
};
|
|
|
|
/* Bridges the nested seat's selection with the host compositor's. Two host
|
|
backends share one inner (nested-seat) implementation:
|
|
- data-control: zwlr_data_control_v1 (labwc/sway/etc.), full duplex,
|
|
independent of keyboard focus; its v2 set_primary_selection /
|
|
primary_selection pair carries the primary (middle-click) selection.
|
|
- wl_data_device: plain stable protocol (Mutter/GNOME fallback). Writing
|
|
the host selection always works (needs a captured keyboard serial);
|
|
reading the host selection only works while our window is focused.
|
|
It has no primary selection, so the primary bridge only exists on the
|
|
data-control backend.
|
|
Data flows by passing file descriptors straight through in both directions
|
|
(host offer -> nested source, nested source -> host source), so no
|
|
intermediate copy is needed. */
|
|
struct clipboard_bridge {
|
|
enum bridge_backend backend;
|
|
|
|
/* nested (wlroots) seat */
|
|
struct wl_listener request_set_selection;
|
|
struct wl_listener set_selection;
|
|
struct wl_listener set_primary_selection;
|
|
|
|
/* the nested selection source currently mirrored to the host; serves
|
|
host paste requests until it is destroyed */
|
|
struct wlr_data_source *inner_src;
|
|
struct wl_listener inner_src_destroy;
|
|
bool inner_clear_requested; /* nested client asked to clear the selection */
|
|
bool ignore_next_inner; /* one-shot echo guard for host->nested clears */
|
|
|
|
/* the nested primary-selection source currently mirrored to the host;
|
|
serves host primary-selection requests (middle click) until destroyed */
|
|
struct wlr_primary_selection_source *inner_primary_src;
|
|
struct wl_listener inner_primary_src_destroy;
|
|
bool inner_primary_clear_requested;
|
|
bool primary_ignore_next_inner; /* one-shot guard for host->nested echoes */
|
|
|
|
/* host: standard wl_data_device (always bound; the fallback backend) */
|
|
struct wl_data_device_manager *ddm;
|
|
struct wl_data_device *dd;
|
|
struct wl_data_source *dd_source; /* our host-side source (nested copy) */
|
|
struct wl_data_offer *dd_offer; /* current host selection offer */
|
|
struct wl_data_offer *dd_echo_offer; /* offer describing our own set */
|
|
uint32_t kbd_serial; /* last host keyboard/enter serial */
|
|
|
|
/* host: wlr-data-control backend (if the host exposes it) */
|
|
struct zwlr_data_control_manager_v1 *dc_manager;
|
|
struct zwlr_data_control_device_v1 *dc_device;
|
|
struct zwlr_data_control_source_v1 *dc_source;
|
|
struct zwlr_data_control_offer_v1 *dc_offer;
|
|
struct zwlr_data_control_offer_v1 *dc_echo_offer;
|
|
/* host primary selection over the same data-control device (v2): our
|
|
published source, the current host offer, and the echo of our own set */
|
|
struct zwlr_data_control_source_v1 *dc_primary_source;
|
|
struct zwlr_data_control_offer_v1 *dc_primary_offer;
|
|
struct zwlr_data_control_offer_v1 *dc_primary_echo_offer;
|
|
bool dc_primary_supported; /* bound data-control version >= 2 */
|
|
|
|
/* MIME list currently being collected from a host offer */
|
|
char **mimes;
|
|
size_t mimes_len, mimes_cap;
|
|
bool dc_pending_echo; /* our own set via data-control: echo not yet consumed */
|
|
bool dd_pending_echo; /* our own set via wl_data_device: echo not yet consumed */
|
|
bool dc_primary_pending_echo; /* our own primary set: echo not yet consumed */
|
|
bool host_set_pending; /* nested selection not yet on the host; retry on focus */
|
|
|
|
/* Eager text prefetch: when a real host selection is mirrored, its primary
|
|
text MIME is pulled into this buffer at copy time so pastes are served
|
|
instantly instead of asking the host for each one. */
|
|
int prefetch_fd; /* pipe read end, -1 when idle */
|
|
struct wl_event_source *prefetch_src;
|
|
char *prefetch_buf;
|
|
size_t prefetch_len, prefetch_cap;
|
|
char prefetch_mime[64]; /* canonical text MIME, "" if none */
|
|
bool prefetch_ready; /* buffer complete (EOF received) */
|
|
|
|
/* Eager text prefetch for the mirrored primary selection. Kept separate
|
|
from the clipboard prefetch above: the host can change either selection
|
|
on its own, so their buffers must never collide. */
|
|
int primary_prefetch_fd;
|
|
struct wl_event_source *primary_prefetch_src;
|
|
char *primary_prefetch_buf;
|
|
size_t primary_prefetch_len, primary_prefetch_cap;
|
|
char primary_prefetch_mime[64];
|
|
bool primary_prefetch_ready;
|
|
};
|
|
|
|
static struct clipboard_bridge bridge = {
|
|
.prefetch_fd = -1,
|
|
.primary_prefetch_fd = -1,
|
|
};
|
|
|
|
static void bridge_cleanup(void);
|
|
static void bridge_init(void);
|
|
static void bridge_attach_seat(void);
|
|
|
|
/* Frame dumps: SIGUSR1 (or KICK_AUTODUMP) requests a scene-buffer save
|
|
(client content as wlroots drew it) and a host back-buffer save (what we
|
|
composited for the host) on the next frame. */
|
|
static volatile sig_atomic_t dump_scene_req, dump_host_req;
|
|
static int dump_seq;
|
|
static bool client_dumped = false;
|
|
|
|
/* Set by sigchld_handler whenever a child exits; the main loop clears it and
|
|
runs sweep_dead_startup_groups() to drop black tabs whose command exited
|
|
without ever connecting. A plain flag (set + cleared in the loop) keeps
|
|
the reaping work out of the signal handler. */
|
|
static volatile sig_atomic_t sigchld_seen = 0;
|
|
|
|
/* The most recently reaped child, recorded by sigchld_handler so the startup
|
|
sweep can tell the user WHY a tab's command died (usage error vs crash vs
|
|
signal). Best-effort by design: only one exit is kept, so a burst of child
|
|
exits can overwrite the entry before the sweep reads it -- the message then
|
|
just omits the status. sig_atomic_t holds pids on Linux (pid_t is int). */
|
|
static volatile sig_atomic_t last_child_pid = -1;
|
|
static volatile sig_atomic_t last_child_status = 0;
|
|
|
|
/* A tab is "live" when it can own the show: it has a mapped toplevel, or
|
|
it is pending (black) -- an unclaimed CLI tab at startup or a fresh
|
|
Alt+Shift+T terminal. Slots are otherwise removed outright, so a live
|
|
check is all that's needed to know whether a slot should be shown,
|
|
cycled to, or handed the active crown. */
|
|
static bool app_live(struct kick_app *app);
|
|
/* The currently focused toplevel (NULL while the active slot is black). */
|
|
static struct wlr_xdg_toplevel *active_toplevel(void);
|
|
static void set_active_app(struct kick_app *app);
|
|
static void set_active_surface(struct wlr_xdg_toplevel *toplevel);
|
|
static void render_and_present(void);
|
|
|
|
/* Request a redraw from a non-client source (kick's own UI: tab switches,
|
|
reorder, resize, overlay, boot). Marks the frame dirty and wakes the
|
|
event loop via wlr_output_schedule_frame so an idle compositor still
|
|
renders promptly; the drain of that wake presents on the next frame-ready
|
|
point instead of racing an in-flight page flip. */
|
|
static void kick_request_redraw(void) {
|
|
needs_redraw = true;
|
|
if (server.output)
|
|
wlr_output_schedule_frame(server.output);
|
|
}
|
|
static struct kick_app *app_for_toplevel(struct wlr_xdg_toplevel *toplevel);
|
|
static void host_teardown_window(void);
|
|
static int app_index(struct kick_app *app);
|
|
static struct kick_sub_window *top_mapped_sub(struct kick_app *app);
|
|
static const char *app_row_app_id(struct kick_app *app);
|
|
static struct kick_app *pick_successor(struct kick_app *app,
|
|
int prefer_right);
|
|
static void remove_app(int idx);
|
|
static int add_tab(const char *cmd);
|
|
static int cmd_in_path(const char *cmd);
|
|
static void insert_app_after(struct kick_app *app, struct kick_app *anchor);
|
|
static void launch_client(struct kick_app *app);
|
|
static void launch_all_cli(void);
|
|
|
|
#if HAVE_XWAYLAND
|
|
/* XWayland surface handlers (defined with the other toplevel handlers). */
|
|
static void handle_xwayland_new_surface(struct wl_listener *listener,
|
|
void *data);
|
|
static void handle_xwayland_ready(struct wl_listener *listener, void *data);
|
|
static void handle_xwayland_destroy(struct wl_listener *listener, void *data);
|
|
#endif
|
|
|
|
/* Cursor-shape-v1: apply a wp_cursor_shape_device_v1 shape value -- setting
|
|
the standalone cursor image or forwarding it to the host pointer. */
|
|
static void apply_cursor_shape(int shape);
|
|
static void handle_shape_set(struct wl_listener *listener, void *data);
|
|
static void host_cursor_shape_get_pointer(void);
|
|
static void host_cursor_shape_set(int shape);
|
|
|
|
/* Logging levels: quiet by default so a plain launch writes nothing to the
|
|
launching terminal except fatal errors. KICK_LOG=1 shows status lines,
|
|
KICK_LOG=2 adds the per-frame [diag] trace. SIGUSR1 and KICK_AUTODUMP
|
|
dumps always write their .ppm files regardless of level. */
|
|
enum { KICK_LOG_QUIET = 0, KICK_LOG_INFO = 1, KICK_LOG_DEBUG = 2 };
|
|
|
|
static int kick_log_level(void) {
|
|
static volatile sig_atomic_t level = -1;
|
|
if (level < 0) {
|
|
const char *v = getenv("KICK_LOG");
|
|
int l = v ? atoi(v) : 0;
|
|
if (l < KICK_LOG_QUIET) l = KICK_LOG_QUIET;
|
|
if (l > KICK_LOG_DEBUG) l = KICK_LOG_DEBUG;
|
|
level = l;
|
|
}
|
|
return level;
|
|
}
|
|
|
|
static void kick_vlog(int level, const char *fmt, va_list args) {
|
|
if (level > kick_log_level())
|
|
return;
|
|
vfprintf(stderr, fmt, args);
|
|
}
|
|
|
|
static void kick_log(int level, const char *fmt, ...) {
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
kick_vlog(level, fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
/* Route libxkbcommon's own log output (which otherwise writes
|
|
"xkbcommon: ERROR: ..." straight to stderr, bypassing KICK_LOG) through
|
|
our level system. Errors surface at info level; warnings and below are
|
|
debug-only, so a quiet launch no longer leaks parse failures to the
|
|
launching terminal. */
|
|
static void kick_xkb_log(struct xkb_context *ctx, enum xkb_log_level level,
|
|
const char *fmt, va_list args)
|
|
{
|
|
(void)ctx;
|
|
int l = (level <= XKB_LOG_LEVEL_ERROR) ? KICK_LOG_INFO : KICK_LOG_DEBUG;
|
|
if (l > kick_log_level())
|
|
return;
|
|
fputs("kick: xkbcommon: ", stderr);
|
|
vfprintf(stderr, fmt, args);
|
|
}
|
|
|
|
static void kick_log_level_init(void) {
|
|
(void)kick_log_level();
|
|
}
|
|
|
|
/* The process group to signal for an app: the group its claimed client lives
|
|
in (set at claim time), falling back to the launcher pid for slots with no
|
|
client yet. */
|
|
static pid_t app_signal_group(struct kick_app *app) {
|
|
return (app && app->pgid > 0) ? app->pgid : (app ? app->pid : -1);
|
|
}
|
|
|
|
/* Whether an app has a killable target: either a process group we own, or a
|
|
connected client pid (external clients sharing our own group). */
|
|
static bool app_killable(struct kick_app *app) {
|
|
return app && (app_signal_group(app) > 0 || app->client_pid > 0);
|
|
}
|
|
|
|
/* Signal an app's process target. A group we created (launch_client runs the
|
|
child in its own setpgid'd group) is signaled as a group so the whole client
|
|
tree dies. A client we did NOT create, or one sharing our own process group
|
|
(e.g. started from the same terminal as the compositor), is signaled by pid
|
|
alone: group-signaling it would kill the launcher's session, ourselves
|
|
included. */
|
|
static void kill_app(struct kick_app *app, int sig) {
|
|
pid_t g = app_signal_group(app);
|
|
if (g > 0 && g != getpgid(0))
|
|
kill(-g, sig);
|
|
else if (app && app->client_pid > 0)
|
|
kill(app->client_pid, sig);
|
|
}
|
|
|
|
/* Terminate every launched client: SIGTERM first for a graceful shutdown,
|
|
then SIGKILL any stragglers after a short grace period. */
|
|
static void terminate_clients(void) {
|
|
for (int i = 0; i < server.num_apps; i++)
|
|
kill_app(server.apps[i], SIGTERM);
|
|
struct timespec wait = { .tv_sec = 0, .tv_nsec = 150000000 };
|
|
nanosleep(&wait, NULL);
|
|
for (int i = 0; i < server.num_apps; i++)
|
|
kill_app(server.apps[i], SIGKILL);
|
|
}
|
|
|
|
/* Remove the session's Wayland socket file (and its companion .lock file,
|
|
both created by wl_display_add_socket), if present. The full path is
|
|
precomputed at startup so this only ever calls unlink()/snprintf(), which
|
|
are async-signal-safe and therefore safe from the signal/crash handlers
|
|
too. A missing file is fine: ENOENT is ignored, i.e. nothing to clean. */
|
|
static void remove_socket_file(void) {
|
|
unlink(socket_path);
|
|
char lock_path[sizeof(socket_path) + 6];
|
|
snprintf(lock_path, sizeof(lock_path), "%s.lock", socket_path);
|
|
unlink(lock_path);
|
|
}
|
|
|
|
static void overlay_font_fini(void);
|
|
static void standalone_overlay_damage(void);
|
|
|
|
/* Standalone native keyboard: one struct per libinput keyboard device, so
|
|
modifier state is always read from the device that emitted the event (the
|
|
legacy single-slot approach broke Shift/Caps when a host exposed several
|
|
keyboards, e.g. a consumer-control node alongside the real one). */
|
|
struct standalone_kbdev {
|
|
struct wlr_keyboard *keyboard;
|
|
struct wl_listener key;
|
|
struct wl_listener modifiers;
|
|
struct wl_listener destroy;
|
|
struct wl_list link;
|
|
};
|
|
|
|
/* Standalone native pointer: one struct per libinput pointer device. Each
|
|
device owns its listeners so a device going away (libinput suspends every
|
|
device when the session loses its VT) can detach them before wlroots
|
|
asserts the event lists are empty, and so all pointers -- not just the
|
|
last-attached one -- reach the seat. */
|
|
struct standalone_ptrdev {
|
|
struct wlr_pointer *pointer;
|
|
struct wl_listener motion;
|
|
struct wl_listener motion_absolute;
|
|
struct wl_listener button;
|
|
struct wl_listener axis;
|
|
struct wl_listener frame;
|
|
struct wl_listener destroy;
|
|
struct wl_list link;
|
|
};
|
|
|
|
/* Grow server.apps to hold at least n entries. Returns 0 on success, -1 on
|
|
allocation failure. The buffer is freed in shutdown_cleanup(). */
|
|
static int apps_ensure_cap(int n) {
|
|
if (n <= server.apps_cap) return 0;
|
|
struct kick_app **grown =
|
|
realloc(server.apps, (size_t)n * sizeof(*grown));
|
|
if (!grown) return -1;
|
|
server.apps = grown;
|
|
server.apps_cap = n;
|
|
return 0;
|
|
}
|
|
|
|
/* ===================== View helpers (xdg / X11) ===================== */
|
|
/* A tab's window is an xdg toplevel or, with XWayland, an Xwayland surface.
|
|
These helpers keep the shared tab logic (liveness, focus, overlay titles,
|
|
sizing, fullscreen) identical for both; the X11 branches compile away
|
|
without HAVE_XWAYLAND. */
|
|
|
|
static bool app_has_window(struct kick_app *app) {
|
|
if (!app) return false;
|
|
if (app->toplevel) return true;
|
|
#if HAVE_XWAYLAND
|
|
return app->xsurface != NULL;
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
/* Whether the tab's window stack is currently shown (its container node
|
|
enabled). Independent of WHICH window inside it is current. */
|
|
static bool app_visible(struct kick_app *app) {
|
|
return app && app->stack && app->stack->node.enabled;
|
|
}
|
|
|
|
static struct wlr_surface *app_surface(struct kick_app *app) {
|
|
if (!app) return NULL;
|
|
if (app->toplevel) return app->toplevel->base->surface;
|
|
#if HAVE_XWAYLAND
|
|
if (app->is_x11)
|
|
return app->xsurface ? app->xsurface->surface : NULL;
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
static const char *app_title(struct kick_app *app) {
|
|
if (!app) return NULL;
|
|
if (app->toplevel) return app->toplevel->title;
|
|
#if HAVE_XWAYLAND
|
|
if (app->is_x11)
|
|
return app->xsurface ? app->xsurface->title : NULL;
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
static const char *app_app_id(struct kick_app *app) {
|
|
if (!app) return NULL;
|
|
if (app->toplevel) return app->toplevel->app_id;
|
|
#if HAVE_XWAYLAND
|
|
if (app->is_x11)
|
|
return app->xsurface ? app->xsurface->class : NULL;
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
static void app_set_activated(struct kick_app *app, bool on) {
|
|
if (!app) return;
|
|
if (app->toplevel) {
|
|
wlr_xdg_toplevel_set_activated(app->toplevel, on);
|
|
return;
|
|
}
|
|
#if HAVE_XWAYLAND
|
|
if (app->is_x11 && app->xsurface)
|
|
wlr_xwayland_surface_activate(app->xsurface, on);
|
|
#endif
|
|
}
|
|
|
|
static void app_set_maximized(struct kick_app *app, bool on) {
|
|
if (!app) return;
|
|
if (app->toplevel) {
|
|
wlr_xdg_toplevel_set_maximized(app->toplevel, on);
|
|
return;
|
|
}
|
|
#if HAVE_XWAYLAND
|
|
if (app->is_x11 && app->xsurface)
|
|
wlr_xwayland_surface_set_maximized(app->xsurface, on, on);
|
|
#endif
|
|
}
|
|
|
|
static void app_set_fullscreen(struct kick_app *app, bool on) {
|
|
if (!app) return;
|
|
if (app->toplevel) {
|
|
wlr_xdg_toplevel_set_fullscreen(app->toplevel, on);
|
|
return;
|
|
}
|
|
#if HAVE_XWAYLAND
|
|
if (app->is_x11 && app->xsurface)
|
|
wlr_xwayland_surface_set_fullscreen(app->xsurface, on);
|
|
#endif
|
|
}
|
|
|
|
/* Keep a mapped window at the maximized output size. XDG clients are told
|
|
via set_size; X11 clients get a configure to the full output at 0,0. */
|
|
static void app_set_size(struct kick_app *app, int width, int height) {
|
|
if (!app) return;
|
|
if (app->toplevel) {
|
|
wlr_xdg_toplevel_set_size(app->toplevel, width, height);
|
|
return;
|
|
}
|
|
#if HAVE_XWAYLAND
|
|
if (app->is_x11 && app->xsurface && app->xsurface->surface)
|
|
wlr_xwayland_surface_configure(app->xsurface, 0, 0,
|
|
(uint16_t)width, (uint16_t)height);
|
|
#endif
|
|
}
|
|
|
|
/* Whether the underlying protocol object accepts state: xdg surfaces only
|
|
after base->initialized; X11 surfaces once associated with a wlr_surface. */
|
|
static bool app_initialized(struct kick_app *app) {
|
|
if (!app) return false;
|
|
if (app->toplevel) return app->toplevel->base->initialized;
|
|
#if HAVE_XWAYLAND
|
|
if (app->is_x11)
|
|
return app->xsurface && app->xsurface->surface != NULL;
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
/* Graceful close for the surface kind; XDG tabs keep their signal-only
|
|
close path, so this is only non-trivial for X11 (WM_DELETE_WINDOW). */
|
|
static void app_graceful_close(struct kick_app *app) {
|
|
#if HAVE_XWAYLAND
|
|
if (app && app->is_x11 && app->xsurface)
|
|
wlr_xwayland_surface_close(app->xsurface);
|
|
#endif
|
|
}
|
|
|
|
static struct wlr_surface *sub_surface(struct kick_sub_window *sub) { if (!sub) return NULL;
|
|
if (sub->toplevel) return sub->toplevel->base->surface;
|
|
#if HAVE_XWAYLAND
|
|
if (sub->is_x11)
|
|
return sub->xsurface ? sub->xsurface->surface : NULL;
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
static const char *sub_title(struct kick_sub_window *sub) {
|
|
if (!sub) return NULL;
|
|
if (sub->toplevel) return sub->toplevel->title;
|
|
#if HAVE_XWAYLAND
|
|
if (sub->is_x11)
|
|
return sub->xsurface ? sub->xsurface->title : NULL;
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
static const char *sub_app_id(struct kick_sub_window *sub) {
|
|
if (!sub) return NULL;
|
|
if (sub->toplevel) return sub->toplevel->app_id;
|
|
#if HAVE_XWAYLAND
|
|
if (sub->is_x11)
|
|
return sub->xsurface ? sub->xsurface->class : NULL;
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
static void sub_set_activated(struct kick_sub_window *sub, bool on) {
|
|
if (!sub) return;
|
|
if (sub->toplevel) {
|
|
wlr_xdg_toplevel_set_activated(sub->toplevel, on);
|
|
return;
|
|
}
|
|
#if HAVE_XWAYLAND
|
|
if (sub->is_x11 && sub->xsurface)
|
|
wlr_xwayland_surface_activate(sub->xsurface, on);
|
|
#endif
|
|
}
|
|
|
|
static bool app_live(struct kick_app *app) {
|
|
return app && (app_has_window(app) || app->pending_claim);
|
|
}
|
|
|
|
static struct wlr_xdg_toplevel *active_toplevel(void) {
|
|
return server.active_app ? server.active_app->toplevel : NULL;
|
|
}
|
|
|
|
/* Full shutdown of the nested session: take down the clients first, then
|
|
free our own allocations and exit. Guarded so the various close paths
|
|
(host window close, last tab closed) only run once. */
|
|
static void shutdown_cleanup(void) {
|
|
if (shutting_down) return;
|
|
shutting_down = true;
|
|
terminate_clients();
|
|
for (int i = 0; i < server.num_apps; i++) {
|
|
if (host.egl_ok && server.apps[i]->icon_tex) {
|
|
glDeleteTextures(1, &server.apps[i]->icon_tex);
|
|
server.apps[i]->icon_tex = 0;
|
|
}
|
|
if (server.apps[i]->icon_wlr) {
|
|
wlr_texture_destroy(server.apps[i]->icon_wlr);
|
|
server.apps[i]->icon_wlr = NULL;
|
|
}
|
|
free(server.apps[i]->cmd);
|
|
free(server.apps[i]);
|
|
}
|
|
free(server.apps);
|
|
server.apps = NULL;
|
|
|
|
/* Detach + free the standalone native keyboard devices. */
|
|
struct standalone_kbdev *kd, *kd_tmp;
|
|
wl_list_for_each_safe(kd, kd_tmp, &server.kbdevs, link) {
|
|
wl_list_remove(&kd->link);
|
|
wl_list_remove(&kd->key.link);
|
|
wl_list_remove(&kd->modifiers.link);
|
|
wl_list_remove(&kd->destroy.link);
|
|
free(kd);
|
|
}
|
|
wl_list_init(&server.kbdevs);
|
|
|
|
/* Detach + free the standalone native pointer devices. */
|
|
struct standalone_ptrdev *pd, *pd_tmp;
|
|
wl_list_for_each_safe(pd, pd_tmp, &server.ptrdevs, link) {
|
|
wl_list_remove(&pd->link);
|
|
wl_list_remove(&pd->motion.link);
|
|
wl_list_remove(&pd->motion_absolute.link);
|
|
wl_list_remove(&pd->button.link);
|
|
wl_list_remove(&pd->axis.link);
|
|
wl_list_remove(&pd->frame.link);
|
|
wl_list_remove(&pd->destroy.link);
|
|
free(pd);
|
|
}
|
|
wl_list_init(&server.ptrdevs);
|
|
|
|
if (server.overlay_atlas_wlr) {
|
|
wlr_texture_destroy(server.overlay_atlas_wlr);
|
|
server.overlay_atlas_wlr = NULL;
|
|
}
|
|
if (server.xkb_state) xkb_state_unref(server.xkb_state);
|
|
if (server.xkb_keymap) xkb_keymap_unref(server.xkb_keymap);
|
|
if (server.xkb_ctx) xkb_context_unref(server.xkb_ctx);
|
|
overlay_font_fini();
|
|
bridge_cleanup();
|
|
remove_socket_file();
|
|
}
|
|
|
|
/* ===================== 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, "kick: 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, "kick: 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"
|
|
"uniform vec4 u_color;\n"
|
|
"void main() {\n"
|
|
" gl_FragColor = texture2D(u_tex, v_uv) * u_color;\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, "kick: 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");
|
|
host.gl_color_loc = glGetUniformLocation(host.gl_program, "u_color");
|
|
|
|
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);
|
|
|
|
glGenTextures(1, &host.overlay_white_tex);
|
|
glBindTexture(GL_TEXTURE_2D, host.overlay_white_tex);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
const GLubyte white_px[4] = { 255, 255, 255, 255 };
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA,
|
|
GL_UNSIGNED_BYTE, white_px);
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
return true;
|
|
}
|
|
|
|
/* Blit a source rect of a texture into the window at the given destination
|
|
rect (dst_x, dst_y, dst_w, dst_h, window coords), clipped to the window.
|
|
The source is stretched uniformly to the destination (1:1 when the rects
|
|
match). The draw is tinted by `color` (RGBA), so solid rects are drawn
|
|
from a 1x1 white texture and text glyphs from the font atlas. */
|
|
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 dst_w, int dst_h,
|
|
int win_w, int win_h, const GLfloat color[4])
|
|
{
|
|
if (win_w <= 0 || win_h <= 0) return;
|
|
if (src_w <= 0 || src_h <= 0 || dst_w <= 0 || dst_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 >= dst_w || cy >= dst_h) return;
|
|
int wx0 = dst_x + cx;
|
|
int wy0 = dst_y + cy;
|
|
int cw = dst_w - cx;
|
|
int ch = dst_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;
|
|
|
|
float u0 = ((float)src_x + (float)src_w * cx / dst_w) / (float)tex_w;
|
|
float u1 = ((float)src_x + (float)src_w * (cx + cw) / dst_w) / (float)tex_w;
|
|
float v0 = ((float)src_y + (float)src_h * cy / dst_h) / (float)tex_h;
|
|
float v1 = ((float)src_y + (float)src_h * (cy + ch) / dst_h) / (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);
|
|
glUniform4fv(host.gl_color_loc, 1, color);
|
|
|
|
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);
|
|
}
|
|
|
|
/* ============ Alt-held tab-title overlay ============
|
|
While the host Alt modifier is held, a passive list of tab titles is
|
|
drawn into the top-left of the host window with the active tab
|
|
highlighted. Up/Down already cycle tabs while Alt is held, so the
|
|
highlight follows server.active_app automatically. Titles are read from
|
|
the wlr_xdg_toplevel at draw time (terminals retitle themselves); a
|
|
still-black tab shows its command line instead. No per-client listeners
|
|
are needed. */
|
|
|
|
static FT_Library overlay_ft_lib;
|
|
static FT_Face overlay_ft_face;
|
|
static bool overlay_font_ok;
|
|
|
|
/* Fixed rasterized font size; 30px is twice the original 15px constant. */
|
|
#define OVERLAY_FONT_PX 30
|
|
|
|
static const char *overlay_pick_font(void) {
|
|
static const char *const paths[] = {
|
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
"/usr/share/fonts/dejavu/DejaVuSans.ttf",
|
|
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
|
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
|
"/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf",
|
|
"/usr/share/fonts/liberation/LiberationSans-Regular.ttf",
|
|
"/usr/share/fonts/noto/NotoSans-Regular.ttf",
|
|
};
|
|
const char *env = getenv("KICK_FONT");
|
|
if (env && access(env, R_OK) == 0)
|
|
return env;
|
|
for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++)
|
|
if (access(paths[i], R_OK) == 0)
|
|
return paths[i];
|
|
return NULL;
|
|
}
|
|
|
|
/* Build the ASCII glyph atlas (chars 32..126) from the chosen system font.
|
|
Must be called with the host EGL context current. Non-fatal: on failure
|
|
the overlay still renders its boxes, just without text. */
|
|
static void overlay_font_init(void) {
|
|
static bool overlay_font_done;
|
|
if (overlay_font_done) return;
|
|
overlay_font_done = true;
|
|
|
|
int px = OVERLAY_FONT_PX;
|
|
static const char *overlay_font_path = NULL;
|
|
if (!overlay_ft_lib) {
|
|
const char *path = overlay_pick_font();
|
|
if (!path) {
|
|
kick_log(KICK_LOG_INFO, "[overlay] no font found; "
|
|
"set KICK_FONT to enable overlay text\n");
|
|
return;
|
|
}
|
|
overlay_font_path = path;
|
|
if (FT_Init_FreeType(&overlay_ft_lib) != 0) {
|
|
kick_log(KICK_LOG_INFO, "[overlay] FreeType init failed\n");
|
|
return;
|
|
}
|
|
if (FT_New_Face(overlay_ft_lib, path, 0, &overlay_ft_face) != 0) {
|
|
kick_log(KICK_LOG_INFO, "[overlay] cannot load font %s\n", path);
|
|
FT_Done_FreeType(overlay_ft_lib);
|
|
overlay_ft_lib = NULL;
|
|
return;
|
|
}
|
|
}
|
|
if (FT_Set_Pixel_Sizes(overlay_ft_face, 0, px) != 0) {
|
|
kick_log(KICK_LOG_INFO, "[overlay] cannot set pixel size %d\n", px);
|
|
return;
|
|
}
|
|
|
|
int height = (int)(overlay_ft_face->size->metrics.height / 64.0f) + 1;
|
|
int asc = (int)(overlay_ft_face->size->metrics.ascender / 64.0f);
|
|
int adv_max = 0;
|
|
for (int c = 32; c <= 126; c++) {
|
|
if (FT_Load_Char(overlay_ft_face, c, FT_LOAD_DEFAULT) == 0) {
|
|
int adv = (int)(overlay_ft_face->glyph->advance.x / 64.0f);
|
|
if (adv > adv_max) adv_max = adv;
|
|
}
|
|
}
|
|
if (adv_max <= 0) adv_max = 8;
|
|
int cell_w = adv_max + 3;
|
|
int cell_h = height + 3;
|
|
int cols = 16;
|
|
int rows = (95 + cols - 1) / cols;
|
|
int atlas_w = cols * cell_w;
|
|
int atlas_h = rows * cell_h;
|
|
|
|
unsigned char *atlas = calloc((size_t)atlas_w * atlas_h * 2, 1);
|
|
if (!atlas) {
|
|
kick_log(KICK_LOG_INFO, "[overlay] atlas alloc failed\n");
|
|
return;
|
|
}
|
|
int n_glyphs = 0;
|
|
for (int c = 32; c <= 126; c++) {
|
|
struct overlay_glyph *g = &host.overlay_glyphs[c - 32];
|
|
if (FT_Load_Char(overlay_ft_face, c, FT_LOAD_RENDER) != 0)
|
|
continue;
|
|
FT_Bitmap *bmp = &overlay_ft_face->glyph->bitmap;
|
|
/* A bitmap wider/taller than its cell would corrupt the glyphs it
|
|
spills into; skip such glyphs rather than damage the atlas. */
|
|
if ((int)bmp->width > cell_w - 2 || (int)bmp->rows > cell_h - 2)
|
|
continue;
|
|
int cell = c - 32;
|
|
int cx = (cell % cols) * cell_w;
|
|
int cy = (cell / cols) * cell_h;
|
|
g->ax = cx + 1;
|
|
g->ay = cy + 1;
|
|
g->pw = bmp->width;
|
|
g->ph = bmp->rows;
|
|
g->adv = (int)(overlay_ft_face->glyph->advance.x / 64.0f);
|
|
g->top = asc - overlay_ft_face->glyph->bitmap_top;
|
|
/* Cells are GL_LUMINANCE_ALPHA, so each 8-bit alpha is duplicated. */
|
|
for (int r = 0; r < (int)bmp->rows; r++) {
|
|
const unsigned char *src = bmp->buffer + (size_t)r * bmp->pitch;
|
|
unsigned char *dst = atlas
|
|
+ ((size_t)(g->ay + r) * atlas_w + g->ax) * 2;
|
|
for (int x = 0; x < (int)bmp->width; x++) {
|
|
dst[x * 2 + 0] = src[x];
|
|
dst[x * 2 + 1] = src[x];
|
|
}
|
|
}
|
|
n_glyphs++;
|
|
}
|
|
if (n_glyphs == 0) {
|
|
free(atlas);
|
|
return;
|
|
}
|
|
|
|
if (server.standalone) {
|
|
/* Standalone has no host GL context: hand the glyph bitmap to the
|
|
wlroots renderer as a premultiplied-white texture (r=g=b=a) so the
|
|
single quad shader can tint it via the pass alpha. */
|
|
unsigned char *rgba = malloc((size_t)atlas_w * atlas_h * 4);
|
|
if (rgba) {
|
|
for (size_t i = 0; i < (size_t)atlas_w * atlas_h; i++) {
|
|
unsigned char v = atlas[i * 2];
|
|
rgba[i * 4 + 0] = v;
|
|
rgba[i * 4 + 1] = v;
|
|
rgba[i * 4 + 2] = v;
|
|
rgba[i * 4 + 3] = v;
|
|
}
|
|
server.overlay_atlas_wlr = wlr_texture_from_pixels(
|
|
server.renderer, DRM_FORMAT_ARGB8888,
|
|
(uint32_t)atlas_w * 4, (uint32_t)atlas_w, (uint32_t)atlas_h,
|
|
rgba);
|
|
}
|
|
free(rgba);
|
|
free(atlas);
|
|
host.overlay_font_h = height;
|
|
overlay_font_ok = server.overlay_atlas_wlr != NULL;
|
|
kick_log(KICK_LOG_INFO, "[overlay] font %s glyphs=%d atlas=%dx%d\n",
|
|
overlay_font_path, n_glyphs, atlas_w, atlas_h);
|
|
return;
|
|
}
|
|
|
|
if (!host.overlay_atlas_tex)
|
|
glGenTextures(1, &host.overlay_atlas_tex);
|
|
glBindTexture(GL_TEXTURE_2D, host.overlay_atlas_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);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, atlas_w, atlas_h, 0,
|
|
GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, atlas);
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
free(atlas);
|
|
host.overlay_atlas_w = atlas_w;
|
|
host.overlay_atlas_h = atlas_h;
|
|
host.overlay_font_h = height;
|
|
overlay_font_ok = true;
|
|
kick_log(KICK_LOG_INFO, "[overlay] font %s glyphs=%d atlas=%dx%d\n",
|
|
overlay_font_path, n_glyphs, atlas_w, atlas_h);
|
|
}
|
|
|
|
static void overlay_font_fini(void) {
|
|
if (overlay_ft_face) {
|
|
FT_Done_Face(overlay_ft_face);
|
|
overlay_ft_face = NULL;
|
|
}
|
|
if (overlay_ft_lib) {
|
|
FT_Done_FreeType(overlay_ft_lib);
|
|
overlay_ft_lib = NULL;
|
|
}
|
|
overlay_font_ok = false;
|
|
}
|
|
|
|
/* ===================== Overlay app icons ===================== */
|
|
|
|
/* Drawn square of the icon slot, in host pixels. */
|
|
#define OVERLAY_ICON_SZ 28
|
|
#ifndef MIN
|
|
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
|
#endif
|
|
|
|
static char *overlay_strndup(const char *s, size_t n) {
|
|
char *r = malloc(n + 1);
|
|
if (r) {
|
|
memcpy(r, s, n);
|
|
r[n] = '\0';
|
|
}
|
|
return r;
|
|
}
|
|
|
|
static bool overlay_endswith(const char *s, const char *suffix) {
|
|
size_t ls = strlen(s), lx = strlen(suffix);
|
|
return ls >= lx && strcmp(s + ls - lx, suffix) == 0;
|
|
}
|
|
|
|
/* Read the Icon= value (case-sensitive, not the localized Icon[lang]= form)
|
|
out of a .desktop file, malloc'd, or NULL. */
|
|
static char *desktop_icon_from_file(FILE *f) {
|
|
char line[1024];
|
|
while (fgets(line, sizeof(line), f)) {
|
|
if (strncmp(line, "Icon=", 5) != 0)
|
|
continue;
|
|
if (line[5] == '[') /* Icon[locale]= */
|
|
continue;
|
|
size_t n = strlen(line);
|
|
while (n > 5 && (line[n - 1] == '\n' || line[n - 1] == '\r' ||
|
|
line[n - 1] == ' ' || line[n - 1] == '\t'))
|
|
n--;
|
|
return overlay_strndup(line + 5, n - 5);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Find `<app_id>.desktop` under an XDG data dir and return its Icon= value,
|
|
or NULL. */
|
|
static char *desktop_icon_name(const char *app_id) {
|
|
const char *data_home = getenv("XDG_DATA_HOME");
|
|
const char *data_dirs = getenv("XDG_DATA_DIRS");
|
|
if (!data_dirs || !data_dirs[0])
|
|
data_dirs = "/usr/local/share:/usr/share";
|
|
char buf[4096];
|
|
|
|
if (data_home && data_home[0]) {
|
|
snprintf(buf, sizeof(buf), "%s/applications/%s.desktop",
|
|
data_home, app_id);
|
|
FILE *f = fopen(buf, "r");
|
|
if (f) {
|
|
char *icon = desktop_icon_from_file(f);
|
|
fclose(f);
|
|
if (icon) return icon;
|
|
}
|
|
}
|
|
char *copy = strdup(data_dirs);
|
|
if (!copy) return NULL;
|
|
for (char *p = copy, *dir; p; p = dir) {
|
|
dir = strchr(p, ':');
|
|
if (dir) *dir++ = '\0';
|
|
if (p[0]) {
|
|
snprintf(buf, sizeof(buf), "%s/applications/%s.desktop", p, app_id);
|
|
FILE *f = fopen(buf, "r");
|
|
if (f) {
|
|
char *icon = desktop_icon_from_file(f);
|
|
fclose(f);
|
|
if (icon) {
|
|
free(copy);
|
|
return icon;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
free(copy);
|
|
return NULL;
|
|
}
|
|
|
|
/* Turn an Icon= value (theme name, bare filename or absolute path) into a
|
|
loadable .png/.svg path, or NULL. */
|
|
static char *overlay_find_icon_file(const char *name) {
|
|
static const char *const themes[] = { "Adwaita", "hicolor", "default" };
|
|
static const char *const sizes[] =
|
|
{ "scalable", "48x48", "32x32", "24x24" };
|
|
static const char *const exts[] = { "png", "svg" };
|
|
char roots[8][512];
|
|
int n_roots = 0;
|
|
const char *data_home = getenv("XDG_DATA_HOME");
|
|
const char *data_dirs = getenv("XDG_DATA_DIRS");
|
|
if (data_home && data_home[0] && n_roots < 8)
|
|
snprintf(roots[n_roots++], sizeof(roots[0]), "%s/icons", data_home);
|
|
if (!data_dirs || !data_dirs[0])
|
|
data_dirs = "/usr/local/share:/usr/share";
|
|
char *copy = strdup(data_dirs);
|
|
for (char *p = copy, *dir; p && n_roots < 8; p = dir) {
|
|
dir = strchr(p, ':');
|
|
if (dir) *dir++ = '\0';
|
|
if (p[0])
|
|
snprintf(roots[n_roots++], sizeof(roots[0]), "%s/icons", p);
|
|
}
|
|
free(copy);
|
|
|
|
if (strchr(name, '/') && access(name, R_OK) == 0 &&
|
|
(overlay_endswith(name, ".png") ||
|
|
overlay_endswith(name, ".svg")))
|
|
return strdup(name);
|
|
|
|
char buf[4096];
|
|
for (int tr = 0; tr < n_roots; tr++) {
|
|
for (size_t t = 0; t < sizeof(themes) / sizeof(themes[0]); t++) {
|
|
for (size_t s = 0; s < sizeof(sizes) / sizeof(sizes[0]); s++) {
|
|
for (size_t e = 0; e < sizeof(exts) / sizeof(exts[0]); e++) {
|
|
snprintf(buf, sizeof(buf), "%s/%s/%s/apps/%s.%s",
|
|
roots[tr], themes[t], sizes[s], name, exts[e]);
|
|
if (access(buf, R_OK) == 0)
|
|
return strdup(buf);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
static const char *const pixmap_roots[] = {
|
|
"/usr/share/pixmaps", "/usr/local/share/pixmaps",
|
|
};
|
|
for (size_t pr = 0; pr < sizeof(pixmap_roots) / sizeof(pixmap_roots[0]); pr++) {
|
|
for (size_t e = 0; e < sizeof(exts) / sizeof(exts[0]); e++) {
|
|
snprintf(buf, sizeof(buf), "%s/%s.%s", pixmap_roots[pr], name, exts[e]);
|
|
if (access(buf, R_OK) == 0)
|
|
return strdup(buf);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Decode a PNG into straight (non-premultiplied) RGBA8, malloc'd. */
|
|
static unsigned char *overlay_load_png(const char *path, int *w, int *h) {
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) return NULL;
|
|
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL,
|
|
NULL);
|
|
if (!png) {
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
png_infop info = png_create_info_struct(png);
|
|
if (!info) {
|
|
png_destroy_read_struct(&png, NULL, NULL);
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
if (setjmp(png_jmpbuf(png))) {
|
|
png_destroy_read_struct(&png, &info, NULL);
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
png_init_io(png, f);
|
|
png_read_info(png, info);
|
|
int bw = (int)png_get_image_width(png, info);
|
|
int bh = (int)png_get_image_height(png, info);
|
|
png_byte ct = png_get_color_type(png, info);
|
|
png_byte bd = png_get_bit_depth(png, info);
|
|
if (ct == PNG_COLOR_TYPE_PALETTE)
|
|
png_set_palette_to_rgb(png);
|
|
if (ct == PNG_COLOR_TYPE_GRAY && bd < 8)
|
|
png_set_expand_gray_1_2_4_to_8(png);
|
|
if (png_get_valid(png, info, PNG_INFO_tRNS))
|
|
png_set_tRNS_to_alpha(png);
|
|
if (bd == 16)
|
|
png_set_strip_16(png);
|
|
if (ct == PNG_COLOR_TYPE_RGB || ct == PNG_COLOR_TYPE_GRAY)
|
|
png_set_filler(png, 0xff, PNG_FILLER_AFTER);
|
|
if (ct == PNG_COLOR_TYPE_GRAY || ct == PNG_COLOR_TYPE_GRAY_ALPHA)
|
|
png_set_gray_to_rgb(png);
|
|
png_read_update_info(png, info);
|
|
|
|
size_t rowbytes = png_get_rowbytes(png, info);
|
|
size_t size = (size_t)bh * rowbytes;
|
|
unsigned char *rgba = malloc(size);
|
|
if (!rgba) {
|
|
png_destroy_read_struct(&png, &info, NULL);
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
png_bytep *rows = malloc(sizeof(png_bytep) * (size_t)bh);
|
|
if (!rows) {
|
|
free(rgba);
|
|
png_destroy_read_struct(&png, &info, NULL);
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
for (int y = 0; y < bh; y++)
|
|
rows[y] = rgba + (size_t)y * rowbytes;
|
|
png_read_image(png, rows);
|
|
free(rows);
|
|
png_destroy_read_struct(&png, &info, NULL);
|
|
fclose(f);
|
|
*w = bw;
|
|
*h = bh;
|
|
return rgba;
|
|
}
|
|
|
|
/* Decode an SVG into straight RGBA8 at its intrinsic size, malloc'd. */
|
|
static unsigned char *overlay_load_svg(const char *path, int *w, int *h) {
|
|
GError *err = NULL;
|
|
RsvgHandle *handle = rsvg_handle_new_from_file(path, &err);
|
|
if (!handle || err) {
|
|
if (err) g_error_free(err);
|
|
return NULL;
|
|
}
|
|
double iw = 0, ih = 0;
|
|
if (!rsvg_handle_get_intrinsic_size_in_pixels(handle, &iw, &ih))
|
|
iw = ih = 48.0; /* dimension-less SVG: assume an icon-sized square */
|
|
if (iw <= 0 || ih <= 0) {
|
|
g_object_unref(handle);
|
|
return NULL;
|
|
}
|
|
int bw = (int)iw, bh = (int)ih;
|
|
cairo_surface_t *surf =
|
|
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, bw, bh);
|
|
if (cairo_surface_status(surf) != CAIRO_STATUS_SUCCESS) {
|
|
cairo_surface_destroy(surf);
|
|
g_object_unref(handle);
|
|
return NULL;
|
|
}
|
|
cairo_t *cr = cairo_create(surf);
|
|
RsvgRectangle viewport = { 0, 0, iw, ih };
|
|
if (!rsvg_handle_render_document(handle, cr, &viewport, &err)) {
|
|
if (err) g_error_free(err);
|
|
cairo_destroy(cr);
|
|
cairo_surface_destroy(surf);
|
|
g_object_unref(handle);
|
|
return NULL;
|
|
}
|
|
cairo_destroy(cr);
|
|
cairo_surface_flush(surf);
|
|
|
|
/* Cairo's ARGB32 is premultiplied, little-endian byte order B,G,R,A. */
|
|
int n = bw * bh;
|
|
unsigned char *rgba = malloc(sizeof(unsigned char) * 4 * (size_t)n);
|
|
if (rgba) {
|
|
unsigned char *src = cairo_image_surface_get_data(surf);
|
|
for (int i = 0; i < n; i++) {
|
|
unsigned char a = src[i * 4 + 3];
|
|
unsigned char r = src[i * 4 + 2];
|
|
unsigned char g = src[i * 4 + 1];
|
|
unsigned char b = src[i * 4 + 0];
|
|
if (a > 0) {
|
|
r = (unsigned char)MIN((r * 255 + a / 2) / a, 255);
|
|
g = (unsigned char)MIN((g * 255 + a / 2) / a, 255);
|
|
b = (unsigned char)MIN((b * 255 + a / 2) / a, 255);
|
|
}
|
|
rgba[i * 4 + 0] = r;
|
|
rgba[i * 4 + 1] = g;
|
|
rgba[i * 4 + 2] = b;
|
|
rgba[i * 4 + 3] = a;
|
|
}
|
|
}
|
|
cairo_surface_destroy(surf);
|
|
g_object_unref(handle);
|
|
*w = bw;
|
|
*h = bh;
|
|
return rgba;
|
|
}
|
|
|
|
/* Resolve and upload `app`'s icon as a GL_RGBA texture (host EGL context
|
|
must be current). Best-effort: on any failure the app keeps no icon and
|
|
the overlay draws the placeholder instead. */
|
|
static void overlay_icon_load(struct kick_app *app) {
|
|
app->icon_resolved = true;
|
|
const char *app_id = app_row_app_id(app);
|
|
snprintf(app->icon_app_id, sizeof(app->icon_app_id), "%s",
|
|
app_id ? app_id : "");
|
|
if (!app_id)
|
|
return;
|
|
|
|
char *icon = desktop_icon_name(app_id);
|
|
char *path = overlay_find_icon_file(icon ? icon : app_id);
|
|
free(icon);
|
|
if (!path)
|
|
return;
|
|
|
|
int w = 0, h = 0;
|
|
unsigned char *rgba = NULL;
|
|
if (overlay_endswith(path, ".svg"))
|
|
rgba = overlay_load_svg(path, &w, &h);
|
|
else if (overlay_endswith(path, ".png"))
|
|
rgba = overlay_load_png(path, &w, &h);
|
|
free(path);
|
|
if (!rgba || w <= 0 || h <= 0) {
|
|
free(rgba);
|
|
return;
|
|
}
|
|
|
|
if (server.standalone) {
|
|
/* No host GL context: hand the icon to the wlroots renderer as a
|
|
premultiplied ARGB texture (B,G,R,A byte order). */
|
|
unsigned char *pre = malloc((size_t)w * h * 4);
|
|
if (pre) {
|
|
for (int i = 0; i < w * h; i++) {
|
|
unsigned char a = rgba[i * 4 + 3];
|
|
if (a == 0) {
|
|
pre[i * 4 + 0] = 0;
|
|
pre[i * 4 + 1] = 0;
|
|
pre[i * 4 + 2] = 0;
|
|
pre[i * 4 + 3] = 0;
|
|
} else {
|
|
pre[i * 4 + 0] = (unsigned char)(rgba[i * 4 + 2] * a / 255);
|
|
pre[i * 4 + 1] = (unsigned char)(rgba[i * 4 + 1] * a / 255);
|
|
pre[i * 4 + 2] = (unsigned char)(rgba[i * 4 + 0] * a / 255);
|
|
pre[i * 4 + 3] = a;
|
|
}
|
|
}
|
|
app->icon_wlr = wlr_texture_from_pixels(server.renderer,
|
|
DRM_FORMAT_ARGB8888, (uint32_t)w * 4, (uint32_t)w,
|
|
(uint32_t)h, pre);
|
|
free(pre);
|
|
}
|
|
free(rgba);
|
|
app->icon_w = w;
|
|
app->icon_h = h;
|
|
if (app->icon_wlr)
|
|
kick_log(KICK_LOG_INFO, "[overlay] icon %s %dx%d\n", app_id, w, h);
|
|
return;
|
|
}
|
|
|
|
GLuint tex;
|
|
glGenTextures(1, &tex);
|
|
glBindTexture(GL_TEXTURE_2D, 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);
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0,
|
|
GL_RGBA, GL_UNSIGNED_BYTE, rgba);
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
free(rgba);
|
|
app->icon_tex = tex;
|
|
app->icon_w = w;
|
|
app->icon_h = h;
|
|
kick_log(KICK_LOG_INFO, "[overlay] icon %s %dx%d\n", app_id, w, h);
|
|
}
|
|
|
|
static void draw_rect(int x, int y, int w, int h,
|
|
float r, float g, float b, float a)
|
|
{
|
|
if (w <= 0 || h <= 0) return;
|
|
GLfloat color[4] = { r, g, b, a };
|
|
draw_texture_rect(host.overlay_white_tex, 1, 1,
|
|
0, 0, 1, 1, x, y, w, h, host.width, host.height, color);
|
|
}
|
|
|
|
/* Draw `s` at (x, y) (window coords, y is the row top) tinted by `color`.
|
|
Non-ASCII characters are skipped with a narrow advance. */
|
|
static void draw_text(const char *s, int x, int y,
|
|
float r, float g, float b, float a)
|
|
{
|
|
if (!overlay_font_ok) return;
|
|
GLfloat color[4] = { r, g, b, a };
|
|
int pen = x;
|
|
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
|
|
int c = *p;
|
|
if (c < 32 || c > 126) {
|
|
pen += 4;
|
|
continue;
|
|
}
|
|
struct overlay_glyph *glyph = &host.overlay_glyphs[c - 32];
|
|
draw_texture_rect(host.overlay_atlas_tex, host.overlay_atlas_w,
|
|
host.overlay_atlas_h,
|
|
glyph->ax, glyph->ay, glyph->pw, glyph->ph,
|
|
pen, y + glyph->top, glyph->pw, glyph->ph,
|
|
host.width, host.height, color);
|
|
pen += glyph->adv;
|
|
}
|
|
}
|
|
|
|
static int overlay_text_width(const char *s) {
|
|
if (!overlay_font_ok) return 0;
|
|
int w = 0;
|
|
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
|
|
int c = *p;
|
|
if (c < 32 || c > 126) {
|
|
w += 4;
|
|
continue;
|
|
}
|
|
w += host.overlay_glyphs[c - 32].adv;
|
|
}
|
|
return w;
|
|
}
|
|
|
|
/* Copy `src` into `out`, truncated so that its on-screen width plus a
|
|
trailing "..." fits within `max_px` when the title does not already fit
|
|
whole. Uses the same advance rules as overlay_text_width(). A title that
|
|
fits within `max_px` is copied verbatim, so a row sized to a title's natural
|
|
width never grows a needless ellipsis (e.g. "foot" staying "foot" instead of
|
|
shrinking to "fo..."). Falls back to an empty string when not even one dot
|
|
fits. */
|
|
static void fit_text(const char *src, int max_px, char *out, int out_sz)
|
|
{
|
|
out[0] = '\0';
|
|
if (!src || out_sz <= 1) return;
|
|
|
|
if (overlay_text_width(src) <= max_px) {
|
|
size_t n = 0;
|
|
for (const unsigned char *p = (const unsigned char *)src; *p; p++) {
|
|
if (n + 1 >= (size_t)out_sz)
|
|
break;
|
|
out[n++] = (char)*p;
|
|
}
|
|
out[n] = '\0';
|
|
return;
|
|
}
|
|
|
|
const char *dots = "...";
|
|
int dot_w = 0;
|
|
for (const unsigned char *p = (const unsigned char *)dots; *p; p++) {
|
|
int c = *p;
|
|
if (c < 32 || c > 126) { dot_w += 4; continue; }
|
|
dot_w += host.overlay_glyphs[c - 32].adv;
|
|
}
|
|
if (max_px < dot_w) return;
|
|
|
|
kick_log(KICK_LOG_DEBUG, "[overlay] ellipsis: '%s' budget=%d\n",
|
|
src, max_px);
|
|
int w = 0;
|
|
size_t n = 0;
|
|
const unsigned char *p;
|
|
for (p = (const unsigned char *)src; *p; p++) {
|
|
int c = *p;
|
|
int adv = (c < 32 || c > 126) ? 4 : host.overlay_glyphs[c - 32].adv;
|
|
if (w + adv + dot_w > max_px)
|
|
break;
|
|
if (n + 1 >= (size_t)out_sz)
|
|
break;
|
|
out[n++] = (char)c;
|
|
w += adv;
|
|
}
|
|
if (*p) {
|
|
if (n + 4 < (size_t)out_sz) {
|
|
memcpy(out + n, dots, 3);
|
|
n += 3;
|
|
}
|
|
}
|
|
out[n] = '\0';
|
|
}
|
|
|
|
/* Row title for an overlay slot: the window the tab is actually showing --
|
|
on the active tab that is exactly the focused stack member (a stacked
|
|
sub-window or the wrapped main surface); a background tab previews its
|
|
topmost mapped member, what switching over would show -- or the command
|
|
line for a still-black (pending) tab. Logs one INFO line whenever a
|
|
row's resolved title changes, so a stacked window swapping the title
|
|
underneath an unchanged tab is observable in logs and tests. */
|
|
static const char *app_row_title(struct kick_app *app) {
|
|
const char *t = NULL;
|
|
struct kick_sub_window *pick =
|
|
(app == server.active_app && app->focus_sub)
|
|
? app->focus_sub : top_mapped_sub(app);
|
|
const char *st = pick ? sub_title(pick) : NULL;
|
|
if (pick && st && st[0])
|
|
t = st;
|
|
else {
|
|
const char *mt = app_title(app);
|
|
if (mt && mt[0])
|
|
t = mt;
|
|
}
|
|
if (!t && app->cmd && app->cmd[0])
|
|
t = app->cmd;
|
|
const char *cur = t ? t : "";
|
|
if (strcmp(cur, app->row_title) != 0) {
|
|
kick_log(KICK_LOG_INFO, "[title] app=%d '%s'\n",
|
|
app_index(app), cur);
|
|
snprintf(app->row_title, sizeof(app->row_title), "%s", cur);
|
|
}
|
|
return t;
|
|
}
|
|
|
|
/* Effective overlay app_id for a tab: same member pick as app_row_title,
|
|
mirroring it so icons track the shown window. */
|
|
static const char *app_row_app_id(struct kick_app *app) {
|
|
struct kick_sub_window *pick =
|
|
(app == server.active_app && app->focus_sub)
|
|
? app->focus_sub : top_mapped_sub(app);
|
|
const char *sid = pick ? sub_app_id(pick) : NULL;
|
|
if (pick && sid && sid[0])
|
|
return sid;
|
|
const char *mid = app_app_id(app);
|
|
if (mid && mid[0])
|
|
return mid;
|
|
return NULL;
|
|
}
|
|
|
|
/* Icons latch after one load attempt; re-latch when the effective app_id
|
|
changed underneath the tab (a stacked window took over, or went away). */
|
|
static bool overlay_icon_needs_load(struct kick_app *app) {
|
|
if (!app->icon_resolved)
|
|
return true;
|
|
const char *id = app_row_app_id(app);
|
|
return id && strcmp(id, app->icon_app_id) != 0;
|
|
}
|
|
|
|
/* A row in the overlay (both the host-GL and the standalone wlr paths):
|
|
skip slots that are dead or mid-close; black pending tabs are rows too. */
|
|
static bool app_overlay_row(struct kick_app *app) {
|
|
return app_live(app) && !app->closing;
|
|
}
|
|
|
|
static void draw_tab_overlay(void) {
|
|
if (!host.overlay_visible || server.num_apps < 1) return;
|
|
|
|
int margin = 8, boxpad = 6;
|
|
int icon_sz = OVERLAY_ICON_SZ, icon_gap = 8;
|
|
int row_h = host.overlay_font_h > 0 ? host.overlay_font_h + 8 : 18;
|
|
if (row_h < icon_sz + 4) row_h = icon_sz + 4;
|
|
int max_row_w = 0, n_rows = 0;
|
|
for (int i = 0; i < server.num_apps; i++) {
|
|
struct kick_app *app = server.apps[i];
|
|
if (!app_overlay_row(app)) continue;
|
|
if (overlay_icon_needs_load(app))
|
|
overlay_icon_load(app);
|
|
const char *title = app_row_title(app);
|
|
char fallback[16];
|
|
if (!title) {
|
|
snprintf(fallback, sizeof(fallback), "tab %d", i);
|
|
title = fallback;
|
|
}
|
|
int w = icon_sz + icon_gap + overlay_text_width(title);
|
|
if (w > max_row_w) max_row_w = w;
|
|
n_rows++;
|
|
}
|
|
if (n_rows == 0) return;
|
|
|
|
int avail_w = host.width - 2 * margin - 2 * boxpad;
|
|
if (max_row_w > avail_w) max_row_w = avail_w;
|
|
|
|
int box_w = max_row_w + 2 * boxpad;
|
|
int box_h = n_rows * row_h + 2 * boxpad;
|
|
if (box_w > host.width - 2 * margin) box_w = host.width - 2 * margin;
|
|
if (box_h > host.height - 2 * margin) box_h = host.height - 2 * margin;
|
|
int box_x = (host.width - box_w) / 2;
|
|
int box_y = (host.height - box_h) / 2;
|
|
|
|
int bw = 1;
|
|
draw_rect(box_x, box_y, box_w, box_h, 0.45f, 0.45f, 0.48f, 1.0f);
|
|
draw_rect(box_x + bw, box_y + bw, box_w - 2 * bw, box_h - 2 * bw,
|
|
0.07f, 0.07f, 0.09f, 1.0f);
|
|
|
|
int row = 0;
|
|
for (int i = 0; i < server.num_apps; i++) {
|
|
struct kick_app *app = server.apps[i];
|
|
if (!app_overlay_row(app)) continue;
|
|
const char *title = app_row_title(app);
|
|
char fallback[16];
|
|
if (!title) {
|
|
snprintf(fallback, sizeof(fallback), "tab %d", i);
|
|
title = fallback;
|
|
}
|
|
int ry = box_y + boxpad + row * row_h;
|
|
bool active = app == server.active_app;
|
|
char row_text[512];
|
|
int budget = max_row_w - icon_sz - icon_gap;
|
|
if (budget < 0) budget = 0;
|
|
fit_text(title, budget, row_text, sizeof(row_text));
|
|
int text_w = overlay_text_width(row_text);
|
|
if (active) {
|
|
int hw = icon_sz + icon_gap + text_w + 4;
|
|
int max_hw = box_w - 2 * boxpad + 4;
|
|
if (hw > max_hw) hw = max_hw;
|
|
draw_rect(box_x + boxpad - 2, ry - 1, hw, row_h,
|
|
0.24f, 0.36f, 0.60f, 0.9f);
|
|
}
|
|
int ix = box_x + boxpad;
|
|
int iy = ry + (row_h - icon_sz) / 2;
|
|
if (app->icon_tex) {
|
|
GLfloat tint[4] = { active ? 1.0f : 0.72f,
|
|
active ? 1.0f : 0.72f,
|
|
active ? 1.0f : 0.72f, 1.0f };
|
|
draw_texture_rect(app->icon_tex, app->icon_w, app->icon_h,
|
|
0, 0, app->icon_w, app->icon_h,
|
|
ix, iy, icon_sz, icon_sz,
|
|
host.width, host.height, tint);
|
|
} else {
|
|
/* Generic placeholder for no icon or a still-black tab. */
|
|
draw_rect(ix, iy, icon_sz, icon_sz, 0.45f, 0.45f, 0.48f, 0.85f);
|
|
draw_rect(ix + 3, iy + 3, icon_sz - 6, icon_sz - 6,
|
|
0.07f, 0.07f, 0.09f, 1.0f);
|
|
}
|
|
draw_text(row_text, ix + icon_sz + icon_gap,
|
|
ry + (row_h - host.overlay_font_h) / 2,
|
|
active ? 1.0f : 0.72f,
|
|
active ? 1.0f : 0.72f,
|
|
active ? 1.0f : 0.72f,
|
|
active ? 1.0f : 0.9f);
|
|
row++;
|
|
}
|
|
}
|
|
|
|
/* ---------- Standalone overlay rendering (wlr render pass) ----------
|
|
The standalone output paints via the wlroots GLES2 renderer, so the overlay
|
|
is drawn as a second pass on the same buffer directly with wlr textures.
|
|
Mirrors the layout computed by draw_tab_overlay() for the host-GL path. */
|
|
|
|
static struct wlr_render_color overlay_premul(float r, float g, float b, float a) {
|
|
return (struct wlr_render_color){ r * a, g * a, b * a, a };
|
|
}
|
|
|
|
static void overlay_pass_rect(struct wlr_render_pass *pass,
|
|
int x, int y, int w, int h,
|
|
float r, float g, float b, float a)
|
|
{
|
|
if (w <= 0 || h <= 0) return;
|
|
struct wlr_render_rect_options o = {
|
|
.box = { .x = x, .y = y, .width = w, .height = h },
|
|
.color = overlay_premul(r, g, b, a),
|
|
};
|
|
wlr_render_pass_add_rect(pass, &o);
|
|
}
|
|
|
|
static void overlay_pass_text(struct wlr_render_pass *pass, const char *s,
|
|
int x, int y, float alpha)
|
|
{
|
|
if (!overlay_font_ok || !server.overlay_atlas_wlr) return;
|
|
int pen = x;
|
|
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
|
|
int c = *p;
|
|
if (c < 32 || c > 126) {
|
|
pen += 4;
|
|
continue;
|
|
}
|
|
struct overlay_glyph *glyph = &host.overlay_glyphs[c - 32];
|
|
if (glyph->pw > 0 && glyph->ph > 0) {
|
|
struct wlr_render_texture_options o = {
|
|
.texture = server.overlay_atlas_wlr,
|
|
.src_box = { .x = glyph->ax, .y = glyph->ay,
|
|
.width = glyph->pw, .height = glyph->ph },
|
|
.dst_box = { .x = pen, .y = y + glyph->top,
|
|
.width = glyph->pw, .height = glyph->ph },
|
|
.alpha = &alpha,
|
|
.filter_mode = WLR_SCALE_FILTER_BILINEAR,
|
|
.blend_mode = WLR_RENDER_BLEND_MODE_PREMULTIPLIED,
|
|
};
|
|
wlr_render_pass_add_texture(pass, &o);
|
|
}
|
|
pen += glyph->adv;
|
|
}
|
|
}
|
|
|
|
static void overlay_pass_icon(struct wlr_render_pass *pass,
|
|
struct kick_app *app,
|
|
int ix, int iy, int size, float alpha)
|
|
{
|
|
if (!app || !app->icon_wlr) return;
|
|
struct wlr_render_texture_options o = {
|
|
.texture = app->icon_wlr,
|
|
.dst_box = { .x = ix, .y = iy, .width = size, .height = size },
|
|
.alpha = &alpha,
|
|
.filter_mode = WLR_SCALE_FILTER_BILINEAR,
|
|
.blend_mode = WLR_RENDER_BLEND_MODE_PREMULTIPLIED,
|
|
};
|
|
wlr_render_pass_add_texture(pass, &o);
|
|
}
|
|
|
|
static void draw_tab_overlay_pass(struct wlr_render_pass *pass) {
|
|
if (!host.overlay_visible || server.num_apps < 1) return;
|
|
|
|
int margin = 8, boxpad = 6;
|
|
int icon_sz = OVERLAY_ICON_SZ, icon_gap = 8;
|
|
int row_h = host.overlay_font_h > 0 ? host.overlay_font_h + 8 : 18;
|
|
if (row_h < icon_sz + 4) row_h = icon_sz + 4;
|
|
int max_row_w = 0, n_rows = 0;
|
|
for (int i = 0; i < server.num_apps; i++) {
|
|
struct kick_app *app = server.apps[i];
|
|
if (!app_overlay_row(app)) continue;
|
|
if (overlay_icon_needs_load(app))
|
|
overlay_icon_load(app);
|
|
const char *title = app_row_title(app);
|
|
char fallback[16];
|
|
if (!title) {
|
|
snprintf(fallback, sizeof(fallback), "tab %d", i);
|
|
title = fallback;
|
|
}
|
|
int w0 = icon_sz + icon_gap + overlay_text_width(title);
|
|
if (w0 > max_row_w) max_row_w = w0;
|
|
n_rows++;
|
|
}
|
|
if (n_rows == 0) return;
|
|
|
|
int avail_w = host.width - 2 * margin - 2 * boxpad;
|
|
if (max_row_w > avail_w) max_row_w = avail_w;
|
|
|
|
int box_w = max_row_w + 2 * boxpad;
|
|
int box_h = n_rows * row_h + 2 * boxpad;
|
|
if (box_w > host.width - 2 * margin) box_w = host.width - 2 * margin;
|
|
if (box_h > host.height - 2 * margin) box_h = host.height - 2 * margin;
|
|
int box_x = (host.width - box_w) / 2;
|
|
int box_y = (host.height - box_h) / 2;
|
|
|
|
int bw = 1;
|
|
overlay_pass_rect(pass, box_x, box_y, box_w, box_h, 0.45f, 0.45f, 0.48f, 1.0f);
|
|
overlay_pass_rect(pass, box_x + bw, box_y + bw, box_w - 2 * bw,
|
|
box_h - 2 * bw, 0.07f, 0.07f, 0.09f, 1.0f);
|
|
|
|
int row = 0;
|
|
for (int i = 0; i < server.num_apps; i++) {
|
|
struct kick_app *app = server.apps[i];
|
|
if (!app_overlay_row(app)) continue;
|
|
const char *title = app_row_title(app);
|
|
char fallback[16];
|
|
if (!title) {
|
|
snprintf(fallback, sizeof(fallback), "tab %d", i);
|
|
title = fallback;
|
|
}
|
|
int ry = box_y + boxpad + row * row_h;
|
|
bool active = app == server.active_app;
|
|
char row_text[512];
|
|
int budget = max_row_w - icon_sz - icon_gap;
|
|
if (budget < 0) budget = 0;
|
|
fit_text(title, budget, row_text, sizeof(row_text));
|
|
int text_w = overlay_text_width(row_text);
|
|
if (active) {
|
|
int hw = icon_sz + icon_gap + text_w + 4;
|
|
int max_hw = box_w - 2 * boxpad + 4;
|
|
if (hw > max_hw) hw = max_hw;
|
|
overlay_pass_rect(pass, box_x + boxpad - 2, ry - 1, hw, row_h,
|
|
0.24f, 0.36f, 0.60f, 0.9f);
|
|
}
|
|
int ix = box_x + boxpad;
|
|
int iy = ry + (row_h - icon_sz) / 2;
|
|
if (app->icon_wlr) {
|
|
overlay_pass_icon(pass, app, ix, iy, icon_sz,
|
|
active ? 1.0f : 0.72f);
|
|
} else {
|
|
overlay_pass_rect(pass, ix, iy, icon_sz, icon_sz,
|
|
0.45f, 0.45f, 0.48f, 0.85f);
|
|
overlay_pass_rect(pass, ix + 3, iy + 3, icon_sz - 6, icon_sz - 6,
|
|
0.07f, 0.07f, 0.09f, 1.0f);
|
|
}
|
|
overlay_pass_text(pass, row_text, ix + icon_sz + icon_gap,
|
|
ry + (row_h - host.overlay_font_h) / 2,
|
|
active ? 1.0f : 0.648f);
|
|
row++;
|
|
}
|
|
}
|
|
|
|
|
|
/* ===================== Server-side wlroots setup ===================== */
|
|
|
|
static void swapchain_resize(int width, int height) {
|
|
if (width < 50) width = 50;
|
|
if (height < 50) height = 50;
|
|
if (server.swapchain && server.swapchain->width == width
|
|
&& server.swapchain->height == height)
|
|
return;
|
|
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, "kick: 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;
|
|
|
|
/* The new size is applied to the output lazily at render time (see
|
|
apply_pending_output_resize); here only the scene geometry and the
|
|
clients are told, so configure-time stays free of modesets and
|
|
swapchain reallocation. */
|
|
|
|
if (server.bg_rect)
|
|
wlr_scene_rect_set_size(server.bg_rect, width, height);
|
|
|
|
/* Keep every mapped client at the current maximized size so switching
|
|
tabs is instant and no tab is left small after a host resize. While
|
|
fullscreen the fullscreen configure already sizes clients to the
|
|
output, so skip the maximize request to avoid state churn. */
|
|
for (int i = 0; i < server.num_apps; i++) {
|
|
struct kick_app *app = server.apps[i];
|
|
if (!app_has_window(app)) continue;
|
|
if (!server.clients_fullscreen)
|
|
app_set_maximized(app, true);
|
|
app_set_size(app, width, height);
|
|
}
|
|
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* A host configure may resize the canvas; apply that size to the output
|
|
lazily, at render time, instead of modesetting and reallocating the
|
|
swapchain on every configure. Folding the custom mode into the pending
|
|
state makes wlr_scene build the frame at the new resolution and commit
|
|
mode+buffer in one atomic operation, so a drag costs one GBM alloc and one
|
|
commit per resized frame (no empty-buffer modeset churn in between).
|
|
Standalone outputs never change size here, so this is a no-op for them. */
|
|
static void apply_pending_output_resize(struct wlr_output_state *state) {
|
|
if (!server.output) return;
|
|
int w = host.width, h = host.height;
|
|
if (server.output->width == w && server.output->height == h) return;
|
|
swapchain_resize(w, h);
|
|
wlr_output_state_set_custom_mode(state, w, h, 0);
|
|
}
|
|
|
|
static void handle_output_frame(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
frame_pending = true;
|
|
}
|
|
|
|
/* How many standalone outputs have been seen since the last (re)connect.
|
|
DRM destroys and re-creates the output when the session loses/regains its
|
|
VT, so this is reset to keep accepting the re-added connector. */
|
|
static int standalone_output_count;
|
|
|
|
/* DRM destroys the output when the session (VT) is lost. Clear our
|
|
references so nothing touches freed memory, detach the frame listener
|
|
(wlr_output_finish asserts it's empty), and let the connector re-added on
|
|
resume be accepted again. */
|
|
static void handle_output_destroy(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
(void)data;
|
|
wl_list_remove(&server.output_destroy.link);
|
|
wl_list_remove(&server.frame.link);
|
|
server.output = NULL;
|
|
server.scene_output = NULL; /* wlroots frees it via its output addon */
|
|
if (server.swapchain) {
|
|
wlr_swapchain_destroy(server.swapchain);
|
|
server.swapchain = NULL;
|
|
}
|
|
standalone_output_count = 0;
|
|
frame_pending = false;
|
|
needs_redraw = false;
|
|
}
|
|
|
|
static void handle_new_output(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_output *output = data;
|
|
|
|
/* Standalone (multi-monitor allowed at the wlroots layer): drive only
|
|
the first output with the kick canvas, like the headless nested
|
|
single output. Later outputs are left disabled. */
|
|
if (server.standalone && standalone_output_count++ > 0) {
|
|
kick_log(KICK_LOG_INFO,
|
|
"standalone: extra output ignored (first output only)\n");
|
|
return;
|
|
}
|
|
|
|
if (!wlr_output_init_render(output, server.allocator, server.renderer)) {
|
|
fprintf(stderr, "kick: 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);
|
|
|
|
/* Always render the pointer into our own frame buffer (software cursor)
|
|
instead of a hardware cursor plane: the headless backend's set_cursor
|
|
hook makes wlroots believe it has a plane (so the scene software pass
|
|
is skipped) while actually drawing nothing, and a hardware plane would
|
|
float the cursor outside the buffer we composite/dump. */
|
|
if (server.standalone)
|
|
wlr_output_lock_software_cursors(output, true);
|
|
|
|
/* Give the standalone cursor a place to live. Idempotent across VT
|
|
round-trips (wlr_output_layout_add reuses the existing entry), and the
|
|
cursor attaches its output cursor via the layout's add signal. The
|
|
nested (host-window) output is added too, so xdg-output's
|
|
get_xdg_output finds the output in the layout instead of asserting. */
|
|
if (server.output_layout)
|
|
wlr_output_layout_add(server.output_layout, 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);
|
|
server.output_destroy.notify = handle_output_destroy;
|
|
wl_signal_add(&output->events.destroy, &server.output_destroy);
|
|
|
|
/* Enable the output up front so buffer commits succeed even before the
|
|
host window has been configured. A native output drives at its
|
|
preferred mode; everything else keeps the fixed headless size. */
|
|
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);
|
|
struct wlr_output_mode *mode = NULL;
|
|
if (server.standalone && !server.standalone_headless) {
|
|
if (mode_req_w > 0) {
|
|
/* --screen-mode WxH[@Hz]: pick an exact-resolution mode, highest
|
|
refresh when none was requested. A request the output can't
|
|
satisfy is a startup error: print every reported mode and exit
|
|
(before any client launches or the socket is advertised). */
|
|
struct wlr_output_mode *m;
|
|
int best_refresh = 0;
|
|
wl_list_for_each(m, &output->modes, link) {
|
|
if (m->width != mode_req_w || m->height != mode_req_h)
|
|
continue;
|
|
if (mode_req_refresh > 0) {
|
|
if (m->refresh == mode_req_refresh) {
|
|
mode = m;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
if (m->refresh > best_refresh) {
|
|
best_refresh = m->refresh;
|
|
mode = m;
|
|
}
|
|
}
|
|
if (!mode) {
|
|
fprintf(stderr, "kick: no %dx%d",
|
|
mode_req_w, mode_req_h);
|
|
if (mode_req_refresh > 0)
|
|
fprintf(stderr, "@%u.%03u", mode_req_refresh / 1000,
|
|
mode_req_refresh % 1000);
|
|
if (wl_list_empty(&output->modes))
|
|
fprintf(stderr, " mode; output reports no modes\n");
|
|
else
|
|
fprintf(stderr, " mode; available:\n");
|
|
wl_list_for_each(m, &output->modes, link) {
|
|
fprintf(stderr, " %dx%d@%u.%03u%s%s\n",
|
|
m->width, m->height, m->refresh / 1000,
|
|
m->refresh % 1000,
|
|
m->preferred ? " (preferred)" : "",
|
|
(m->width == mode_req_w && m->height == mode_req_h)
|
|
? " (requested size)" : "");
|
|
}
|
|
exit(1);
|
|
}
|
|
} else {
|
|
mode = wlr_output_preferred_mode(output);
|
|
}
|
|
}
|
|
if (mode) {
|
|
w = mode->width;
|
|
h = mode->height;
|
|
wlr_output_state_set_mode(&state, mode);
|
|
} else {
|
|
wlr_output_state_set_custom_mode(&state, w, h, 0);
|
|
}
|
|
wlr_output_commit_state(output, &state);
|
|
wlr_output_state_finish(&state);
|
|
|
|
/* Overlay geometry and the swapchain are sized to the visible canvas;
|
|
in standalone there is no host window, so use the output size itself. */
|
|
if (server.standalone) {
|
|
host.width = w;
|
|
host.height = h;
|
|
}
|
|
swapchain_resize(w, h);
|
|
|
|
/* An opaque full-output background node gives the scene a deterministic
|
|
backdrop so the nested frame is never transparent. (Direct scanout is
|
|
disabled separately above; a rect alone cannot prevent it because
|
|
wlroots culls a rect that a covering client surface occludes, leaving
|
|
a single render-list entry which is exactly what triggers scanout.)
|
|
On a re-connected output (VT round-trip) the node already exists; just
|
|
resize it. */
|
|
if (server.bg_rect) {
|
|
wlr_scene_rect_set_size(server.bg_rect, w, h);
|
|
} else {
|
|
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);
|
|
}
|
|
|
|
/* Standalone-only damage sentinel for the overlay; see
|
|
standalone_overlay_damage(). A fully transparent rect, sized to the
|
|
output and left enabled (it renders nothing). It sits at the bottom of
|
|
the z-order and is transiently raised inside standalone_overlay_damage()
|
|
to re-populate its visible region and force whole-output damage. */
|
|
if (server.overlay_damage_rect) {
|
|
wlr_scene_rect_set_size(server.overlay_damage_rect, w, h);
|
|
} else if (server.standalone) {
|
|
server.overlay_damage_rect = wlr_scene_rect_create(
|
|
&server.scene->tree, w, h, (float[]){ 0.0f, 0.0f, 0.0f, 0.0f });
|
|
}
|
|
}
|
|
|
|
/* Nearest live slot, scanning right first (prefer_right) or left first.
|
|
The scan stops at the array ends -- it never wraps -- so the reflow after
|
|
a slot is removed matches the old first/last-tab behavior. NULL when
|
|
`app` is the only live slot. */
|
|
static struct kick_app *pick_successor(struct kick_app *app,
|
|
int prefer_right) {
|
|
if (!app) return NULL;
|
|
int died = app_index(app);
|
|
if (prefer_right) {
|
|
for (int i = died + 1; i < server.num_apps; i++)
|
|
if (app_live(server.apps[i]) && !server.apps[i]->closing)
|
|
return server.apps[i];
|
|
for (int i = died - 1; i >= 0; i--)
|
|
if (app_live(server.apps[i]) && !server.apps[i]->closing)
|
|
return server.apps[i];
|
|
} else {
|
|
for (int i = died - 1; i >= 0; i--)
|
|
if (app_live(server.apps[i]) && !server.apps[i]->closing)
|
|
return server.apps[i];
|
|
for (int i = died + 1; i < server.num_apps; i++)
|
|
if (app_live(server.apps[i]) && !server.apps[i]->closing)
|
|
return server.apps[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Drop slot `idx` outright: free the tab's allocations, sever any other
|
|
slot's parent_app reference to it, and compact server.apps[] so a closed
|
|
tab leaves no hole that a later client could stumble into. The caller
|
|
must already have handed the active crown elsewhere; remove_app only
|
|
defends active_app against a slot that was somehow still on it. */
|
|
static void remove_app(int idx) {
|
|
if (idx < 0 || idx >= server.num_apps) return;
|
|
struct kick_app *dead = server.apps[idx];
|
|
if (server.active_app == dead)
|
|
server.active_app = NULL;
|
|
for (int i = 0; i < server.num_apps; i++)
|
|
if (server.apps[i]->parent_app == dead)
|
|
server.apps[i]->parent_app = NULL;
|
|
if (dead->close_timer) {
|
|
wl_event_source_remove(dead->close_timer);
|
|
dead->close_timer = NULL;
|
|
}
|
|
if (host.egl_ok && dead->icon_tex) {
|
|
glDeleteTextures(1, &dead->icon_tex);
|
|
dead->icon_tex = 0;
|
|
}
|
|
if (dead->icon_wlr) {
|
|
wlr_texture_destroy(dead->icon_wlr);
|
|
dead->icon_wlr = NULL;
|
|
}
|
|
/* The window stack: the wrapped main surface plus stacked sub-windows
|
|
(--sub-windows stack), oldest first, newest arrivals at the tail.
|
|
Members with listeners get them removed here; the listener-free
|
|
main wrapper just drops out of the list. */
|
|
while (!wl_list_empty(&dead->sub_windows)) {
|
|
struct kick_sub_window *sub =
|
|
wl_container_of(dead->sub_windows.next, sub, link);
|
|
if (sub->is_main) {
|
|
/* The wrapped main surface carries no listeners; its scene
|
|
nodes died with the toplevel above. */
|
|
wl_list_remove(&sub->link);
|
|
free(sub);
|
|
continue;
|
|
}
|
|
#if HAVE_XWAYLAND
|
|
if (sub->is_x11) {
|
|
wl_list_remove(&sub->destroy.link);
|
|
wl_list_remove(&sub->xs_associate.link);
|
|
if (sub->xs_surface_linked) {
|
|
wl_list_remove(&sub->map.link);
|
|
wl_list_remove(&sub->unmap.link);
|
|
wl_list_remove(&sub->commit.link);
|
|
}
|
|
} else
|
|
#endif
|
|
{
|
|
wl_list_remove(&sub->destroy.link);
|
|
wl_list_remove(&sub->map.link);
|
|
wl_list_remove(&sub->unmap.link);
|
|
wl_list_remove(&sub->commit.link);
|
|
}
|
|
wl_list_remove(&sub->link);
|
|
free(sub);
|
|
}
|
|
free(dead->cmd);
|
|
free(dead);
|
|
memmove(&server.apps[idx], &server.apps[idx + 1],
|
|
(size_t)(server.num_apps - idx - 1) * sizeof(*server.apps));
|
|
server.num_apps--;
|
|
}
|
|
|
|
/* Shared tail of a tab's window-destroy handling (xdg and X11 alike); see
|
|
the definition below handle_toplevel_destroy. */
|
|
static void finish_app_destroy(struct kick_app *app);
|
|
|
|
static void handle_toplevel_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app, destroy);
|
|
app->toplevel = NULL;
|
|
/* The main surface's scene node dies with the xdg surface (created via
|
|
wlr_scene_xdg_surface_create); drop our pointers to both levels. */
|
|
app->tree = NULL;
|
|
app->stack = NULL;
|
|
|
|
/* The overlay's row for this tab disappears the moment the toplevel does,
|
|
shrinking the box mid-menu. The overlay is drawn into the scene-output
|
|
buffer outside the scene graph, so its damage trackers never learn about
|
|
the old pixels; without whole-output damage the vacated ring of the
|
|
larger box stays baked into the reused swapchain buffer. */
|
|
if (host.overlay_visible && server.standalone)
|
|
standalone_overlay_damage();
|
|
if (app->icon_tex) {
|
|
glDeleteTextures(1, &app->icon_tex);
|
|
app->icon_tex = 0;
|
|
}
|
|
|
|
/* wlroots emits destroy with data=NULL and asserts every toplevel
|
|
event listener list is empty afterwards; remove all listeners we
|
|
attached to satisfy that and avoid use-after-free. */
|
|
wl_list_remove(&app->destroy.link);
|
|
wl_list_remove(&app->map.link);
|
|
wl_list_remove(&app->commit.link);
|
|
wl_list_remove(&app->request_maximize.link);
|
|
wl_list_remove(&app->request_fullscreen.link);
|
|
wl_list_remove(&app->request_minimize.link);
|
|
wl_list_remove(&app->request_move.link);
|
|
wl_list_remove(&app->request_resize.link);
|
|
finish_app_destroy(app);
|
|
}
|
|
|
|
/* Shared tail of a tab's window-destroy handling (xdg and X11 alike): drop
|
|
the Alt+Delete escalation state, hand the active crown to a neighbor (or
|
|
tear the session down when the last tab goes), and remove the slot. */
|
|
static void finish_app_destroy(struct kick_app *app) {
|
|
app->closing = false;
|
|
if (app->close_timer) {
|
|
wl_event_source_remove(app->close_timer);
|
|
app->close_timer = NULL;
|
|
}
|
|
|
|
if (server.active_app == app) {
|
|
server.active_app = NULL;
|
|
/* Prefer the neighbor to the left (index died-1); if this is the
|
|
first tab, fall through to the right-side neighbor so the tab
|
|
that was second becomes the new leftmost. */
|
|
struct kick_app *succ = pick_successor(app, -1);
|
|
if (succ) {
|
|
set_active_app(succ);
|
|
} else {
|
|
/* last tab closed: close the host window (nested only) and exit */
|
|
host_teardown_window();
|
|
shutdown_cleanup();
|
|
exit(0);
|
|
}
|
|
}
|
|
/* The slot itself is removed outright: closed tabs leave no hole. */
|
|
remove_app(app_index(app));
|
|
}
|
|
|
|
/* Grace period after the Alt+Delete SIGTERM before SIGKILL escalation. */
|
|
#define KICK_CLOSE_ESCALATE_MS 150
|
|
|
|
/* Escalate an Alt+Delete close to SIGKILL if the client ignored the SIGTERM
|
|
and its toplevel is still up. Live slots are removed at toplevel destroy
|
|
(which also cancels this timer), so for a mapped tab the callback never
|
|
fires against a reaped or reused pid. For a black tab there is no
|
|
toplevel whose destroy would reap the slot, so its removal happens here,
|
|
right after the SIGKILL. */
|
|
static int close_tab_escalate(void *data) {
|
|
struct kick_app *app = data;
|
|
if (app->closing && app_killable(app))
|
|
kill_app(app, SIGKILL);
|
|
if (app->close_timer) {
|
|
wl_event_source_remove(app->close_timer);
|
|
app->close_timer = NULL;
|
|
}
|
|
if (app->closing && !app_has_window(app))
|
|
remove_app(app_index(app));
|
|
return 0;
|
|
}
|
|
|
|
/* Tab close (Alt+Delete on the active tab, or an X11 window's close
|
|
request). The tab vanishes immediately: the active crown moves to the
|
|
right neighbor (or the left neighbor if the closed tab was the rightmost)
|
|
in this same call. The client's whole process group gets SIGTERM, then
|
|
SIGKILL after a short grace if it ignores the TERM (see
|
|
close_tab_escalate); X11 windows also get WM_DELETE_WINDOW first. A
|
|
closing black tab (still pending) drops out of the overlay and cycle right
|
|
away and is removed by the escalation; a closing last tab tears the
|
|
session down here, since nothing else will. */
|
|
static void close_tab(struct kick_app *app) {
|
|
if (!app || !app_killable(app)) return;
|
|
pid_t g = app_signal_group(app);
|
|
pid_t shown = (g > 0 && g != getpgid(0)) ? g : app->client_pid;
|
|
|
|
app->closing = true;
|
|
int died = app_index(app);
|
|
|
|
/* A black tab never fires a toplevel destroy, so drop it from the
|
|
overlay/cycle immediately; only the escalation timer and the
|
|
success-removal paths touch it afterwards. */
|
|
if (!app_has_window(app))
|
|
app->pending_claim = false;
|
|
|
|
struct kick_app *succ = pick_successor(app, +1);
|
|
if (succ) {
|
|
set_active_app(succ);
|
|
} else {
|
|
/* Last live tab: tear the session down now. */
|
|
kick_log(KICK_LOG_INFO, "kick: closing tab %d (pid %d)\n",
|
|
died, (int)shown);
|
|
host_teardown_window();
|
|
shutdown_cleanup();
|
|
exit(0);
|
|
}
|
|
|
|
/* X11 windows get WM_DELETE_WINDOW first; the SIGTERM below (and the
|
|
SIGKILL escalation) remains the universal fallback for both kinds. */
|
|
app_graceful_close(app);
|
|
kill_app(app, SIGTERM);
|
|
kick_log(KICK_LOG_INFO, "kick: closing tab %d (pid %d)\n",
|
|
died, (int)shown);
|
|
app->close_timer = wl_event_loop_add_timer(server.loop, close_tab_escalate,
|
|
app);
|
|
if (app->close_timer)
|
|
wl_event_source_timer_update(app->close_timer, KICK_CLOSE_ESCALATE_MS);
|
|
}
|
|
|
|
static void handle_request_maximize(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app, request_maximize);
|
|
struct wlr_xdg_toplevel *toplevel = app->toplevel;
|
|
if (toplevel->base->initialized)
|
|
wlr_xdg_toplevel_set_maximized(toplevel, true);
|
|
}
|
|
|
|
static void handle_request_fullscreen(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app, request_fullscreen);
|
|
struct wlr_xdg_toplevel *toplevel = app->toplevel;
|
|
if (!toplevel->base->initialized) return;
|
|
/* wlroots 0.19 emits request_fullscreen with NULL data; the direction
|
|
lives in toplevel->requested.fullscreen. Honor the client's request
|
|
and mirror it onto the host window: a fullscreen client fills the
|
|
host window, and an un-fullscreen client puts it back. */
|
|
bool fs = toplevel->requested.fullscreen;
|
|
wlr_xdg_toplevel_set_fullscreen(toplevel, fs);
|
|
kick_log(KICK_LOG_INFO, "client fullscreen=%d\n", fs);
|
|
if (host.frame) {
|
|
if (fs)
|
|
libdecor_frame_set_fullscreen(host.frame, NULL);
|
|
else
|
|
libdecor_frame_unset_fullscreen(host.frame);
|
|
}
|
|
}
|
|
|
|
/* Propagate the host window's fullscreen state onto every client. Called
|
|
from host_xdg_toplevel_configure when that state actually changes. */
|
|
static void clients_set_fullscreen(bool fs) {
|
|
server.clients_fullscreen = fs;
|
|
kick_log(KICK_LOG_INFO, "clients fullscreen=%d\n", fs);
|
|
for (int i = 0; i < server.num_apps; i++) {
|
|
struct kick_app *app = server.apps[i];
|
|
if (!app_initialized(app)) continue;
|
|
app_set_fullscreen(app, fs);
|
|
if (!fs) {
|
|
app_set_maximized(app, true);
|
|
app_set_size(app, server.output->width, server.output->height);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/* Place a scene tree so the client's content sits at 0,0. Shared by tabs
|
|
and stacked sub-windows. */
|
|
static void position_tree(struct wlr_xdg_toplevel *toplevel,
|
|
struct wlr_scene_tree *tree) {
|
|
if (!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 = toplevel->base->geometry;
|
|
int y = -geom.y;
|
|
if (geom.y < 0)
|
|
y = 0;
|
|
wlr_scene_node_set_position(&tree->node, -geom.x, y);
|
|
}
|
|
|
|
static void position_toplevel(struct kick_app *app) {
|
|
if (!app || !app->tree || !app->toplevel) return;
|
|
position_tree(app->toplevel, app->tree);
|
|
}
|
|
|
|
static void handle_toplevel_commit(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_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);
|
|
if (server.clients_fullscreen)
|
|
wlr_xdg_toplevel_set_fullscreen(toplevel, true);
|
|
}
|
|
/* 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);
|
|
|
|
/* [diag] What does this client commit, and is its scene node visible? */
|
|
struct wlr_surface *s = toplevel->base->surface;
|
|
struct wlr_buffer *cbuf = s ? s->current.buffer : NULL;
|
|
struct wlr_dmabuf_attributes dma;
|
|
static struct wlr_buffer *last_logged = NULL;
|
|
if (cbuf && cbuf != last_logged) {
|
|
last_logged = cbuf;
|
|
bool is_dma = wlr_buffer_get_dmabuf(cbuf, &dma);
|
|
bool node_on = app_visible(app);
|
|
if (kick_log_level() >= KICK_LOG_DEBUG) {
|
|
kick_log(KICK_LOG_DEBUG, "[diag] commit app=%d geom=%dx%d+%d+%d "
|
|
"buf=%dx%d dma=%d%s node_enabled=%d active=%d\n",
|
|
app_index(app),
|
|
toplevel->base->geometry.width,
|
|
toplevel->base->geometry.height,
|
|
toplevel->base->geometry.x,
|
|
toplevel->base->geometry.y,
|
|
s->current.buffer_width,
|
|
s->current.buffer_height,
|
|
is_dma,
|
|
is_dma ? (dma.n_planes ? "" : " planes=0") : "",
|
|
node_on,
|
|
server.active_app == app);
|
|
if (is_dma) {
|
|
kick_log(KICK_LOG_DEBUG, "[diag] dmabuf planes=%d "
|
|
"format=0x%x mod=0x%llx\n",
|
|
dma.n_planes, dma.format,
|
|
(unsigned long long)dma.modifier);
|
|
}
|
|
}
|
|
if (!is_dma) {
|
|
/* shm buffer: flag for KICK_AUTODUMP; at debug level also count
|
|
non-black pixels and save the client's own buffer so we can tell
|
|
"client painted nothing" from "we failed to import". */
|
|
void *data;
|
|
uint32_t fmt;
|
|
size_t stride;
|
|
if (wlr_buffer_begin_data_ptr_access(
|
|
cbuf, WLR_BUFFER_DATA_PTR_ACCESS_READ,
|
|
&data, &fmt, &stride)) {
|
|
client_dumped = true;
|
|
if (kick_log_level() >= KICK_LOG_DEBUG) {
|
|
int bw = cbuf->width, bh = cbuf->height;
|
|
unsigned long nonblack = 0, lum = 0;
|
|
unsigned char *p = data;
|
|
for (int yy = 0; yy < bh; yy++) {
|
|
unsigned char *row = p + (size_t)yy * stride;
|
|
for (int xx = 0; xx < bw; xx++) {
|
|
unsigned long v = row[xx*4+0] + row[xx*4+1]
|
|
+ row[xx*4+2];
|
|
lum += v;
|
|
if (v > 12) nonblack++;
|
|
}
|
|
}
|
|
kick_log(KICK_LOG_DEBUG, "[diag] shm fmt=0x%x "
|
|
"nonblack=%lu/%lu mean=%lu\n", fmt, nonblack,
|
|
(unsigned long)bw * bh,
|
|
lum / ((unsigned long)bw * bh + 1));
|
|
char cpath[64];
|
|
snprintf(cpath, sizeof(cpath), "/tmp/client_%d.ppm",
|
|
dump_seq);
|
|
FILE *cf = fopen(cpath, "wb");
|
|
if (cf) {
|
|
fprintf(cf, "P6\n%d %d\n255\n", bw, bh);
|
|
unsigned char *cp = data;
|
|
for (int yy = 0; yy < bh; yy++) {
|
|
unsigned char *crow = cp + (size_t)yy * stride;
|
|
for (int xx = 0; xx < bw; xx++) {
|
|
fputc(crow[xx*4+0], cf);
|
|
fputc(crow[xx*4+1], cf);
|
|
fputc(crow[xx*4+2], cf);
|
|
}
|
|
}
|
|
fclose(cf);
|
|
kick_log(KICK_LOG_DEBUG, "[diag] client buffer "
|
|
"-> %s\n", cpath);
|
|
}
|
|
}
|
|
wlr_buffer_end_data_ptr_access(cbuf);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Insert `app` in the slot order immediately after `anchor`, shifting the
|
|
slots after it right; append at the far right if anchor is NULL or no
|
|
longer present. */
|
|
static void insert_app_after(struct kick_app *app, struct kick_app *anchor) {
|
|
int at = server.num_apps;
|
|
if (anchor)
|
|
for (int i = 0; i < server.num_apps; i++)
|
|
if (server.apps[i] == anchor) {
|
|
at = i + 1;
|
|
break;
|
|
}
|
|
if (at < server.num_apps)
|
|
memmove(&server.apps[at + 1], &server.apps[at],
|
|
(server.num_apps - at) * sizeof(*server.apps));
|
|
server.apps[at] = app;
|
|
server.num_apps++;
|
|
}
|
|
|
|
/* A new toplevel is a sub-window when its client was forked -- directly or
|
|
transitively -- from an already-connected tab's client: the shell running
|
|
inside a terminal tab forks gnome-calculator, so the calculator's pid chain
|
|
runs back through the shell to the terminal client. Exact pid equality (a
|
|
client opening a second toplevel) is the depth-0 case. Walk /proc/<pid>/stat
|
|
ppid links, capped, to find the owning tab; NULL if the client is not
|
|
descended from any tab. */
|
|
static struct kick_app *sub_window_owner(pid_t cpid) {
|
|
if (cpid <= 1)
|
|
return NULL;
|
|
pid_t p = cpid;
|
|
for (int depth = 0; depth < 8; depth++) {
|
|
for (int i = 0; i < server.num_apps; i++)
|
|
if (app_has_window(server.apps[i]) &&
|
|
server.apps[i]->client_pid == p)
|
|
return server.apps[i];
|
|
char path[64];
|
|
snprintf(path, sizeof(path), "/proc/%d/stat", (int)p);
|
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0)
|
|
return NULL;
|
|
char buf[4096];
|
|
ssize_t n = read(fd, buf, sizeof(buf) - 1);
|
|
close(fd);
|
|
if (n <= 0)
|
|
return NULL;
|
|
buf[n] = '\0';
|
|
char *last = strrchr(buf, ')');
|
|
if (!last)
|
|
return NULL;
|
|
char state;
|
|
pid_t ppid = -1;
|
|
if (sscanf(last + 1, " %c %d", &state, &ppid) != 2 || ppid <= 1)
|
|
return NULL;
|
|
p = ppid;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* ===================== Stacked sub-windows (--sub-windows stack) ===== */
|
|
|
|
/* Whether a stack member currently has a live, mapped surface: stacked
|
|
sub-windows track it through their map/unmap listeners; the wrapped
|
|
main surface exists exactly as long as its surface does. */
|
|
static bool stack_mapped(struct kick_sub_window *m) {
|
|
return m->is_main ? app_has_window(m->owner) : m->mapped;
|
|
}
|
|
|
|
/* The topmost mapped member of the tab's window stack -- newest stacked
|
|
sub-window if any, else the wrapped main surface, else NULL. */
|
|
static struct kick_sub_window *top_mapped_sub(struct kick_app *app) {
|
|
struct kick_sub_window *sub;
|
|
wl_list_for_each_reverse(sub, &app->sub_windows, link)
|
|
if (stack_mapped(sub))
|
|
return sub;
|
|
return NULL;
|
|
}
|
|
|
|
/* Show exactly one window of the tab: the focused stack member (the
|
|
wrapped main surface and stacked sub-windows are all plain members, so
|
|
no kind gets special treatment). Only the shown window's scene node
|
|
stays enabled -- hidden layers are neither drawn nor hit-tested, and a
|
|
member's popups (parented on its own tree) hide with it. */
|
|
static void show_current(struct kick_app *app) {
|
|
if (!app || !app->stack) return;
|
|
struct kick_sub_window *cur =
|
|
(server.active_app == app) ? app->focus_sub : NULL;
|
|
struct kick_sub_window *m;
|
|
wl_list_for_each(m, &app->sub_windows, link)
|
|
wlr_scene_node_set_enabled(&m->tree->node, m == cur);
|
|
}
|
|
|
|
/* Give the keyboard to a stacked sub-window of the active tab, demoting
|
|
whatever held it before (the main surface or an older sub-window). */
|
|
static void sub_enter(struct kick_sub_window *sub) {
|
|
struct kick_app *app = sub->owner;
|
|
if (app->focus_sub == sub) return;
|
|
if (app->focus_sub)
|
|
sub_set_activated(app->focus_sub, false);
|
|
app_set_activated(app, false);
|
|
sub_set_activated(sub, true);
|
|
struct wlr_keyboard *kbd = wlr_seat_get_keyboard(server.seat);
|
|
if (!kbd) kbd = &server.keyboard;
|
|
wlr_seat_keyboard_notify_enter(server.seat,
|
|
sub_surface(sub),
|
|
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
|
|
app->focus_sub = sub;
|
|
show_current(app);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* Give the keyboard to a window-stack member of the active tab -- a
|
|
stacked sub-window via sub_enter(), or the wrapped main surface, which
|
|
demotes whatever held the keyboard before and takes it itself. The
|
|
caller still calls show_current() to make the entered window the only
|
|
drawn one. */
|
|
static void stack_enter(struct kick_sub_window *m) {
|
|
struct kick_app *app = m->owner;
|
|
if (!m->is_main) {
|
|
sub_enter(m);
|
|
return;
|
|
}
|
|
if (app->focus_sub)
|
|
sub_set_activated(app->focus_sub, false);
|
|
/* The wrapped main is the cursor even when its surface is not live
|
|
yet (a still-black tab): focus_sub == the main member IS "the main
|
|
surface holds the keyboard". */
|
|
app->focus_sub = m;
|
|
if (!app_has_window(app))
|
|
return;
|
|
app_set_activated(app, true);
|
|
struct wlr_keyboard *kbd = wlr_seat_get_keyboard(server.seat);
|
|
if (!kbd) kbd = &server.keyboard;
|
|
wlr_seat_keyboard_notify_enter(server.seat,
|
|
app_surface(app),
|
|
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
|
|
}
|
|
|
|
/* After the focused member unmaps or dies, hand the keyboard back to the
|
|
owner's next topmost mapped member -- newest stacked sub-window, else
|
|
the wrapped main surface. No-op unless the owner is the tab being
|
|
shown. */
|
|
static void sub_refocus_owner(struct kick_app *app) {
|
|
if (server.active_app != app) return;
|
|
struct kick_sub_window *top = top_mapped_sub(app);
|
|
if (top) {
|
|
/* Historic log shape: "[sub] refocus" marks the keyboard landing
|
|
back on the main surface after the last stacked window went
|
|
away; member-to-member handoffs stay silent. */
|
|
if (top->is_main)
|
|
kick_log(KICK_LOG_INFO, "[sub] refocus app=%d\n", app_index(app));
|
|
stack_enter(top);
|
|
show_current(app);
|
|
kick_request_redraw();
|
|
return;
|
|
}
|
|
/* No live window anywhere in the stack: drop the keyboard entirely. */
|
|
app->focus_sub = NULL;
|
|
show_current(app);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* Rotate the active tab's window stack one step in `dir` (-1 = backward):
|
|
the plain list of members -- the wrapped main surface plus every mapped
|
|
stacked sub-window, oldest first -- steps to the neighbor of whatever
|
|
is showing, wrapping at both ends. The list itself is never mutated;
|
|
focus_sub alone tracks where the rotation sits, so repeated steps walk
|
|
the windows deterministically. Returns false when nothing is mapped
|
|
(nothing to rotate), leaving the key free to pass through to the
|
|
client. */
|
|
static bool cycle_stack(struct kick_app *app, int dir) {
|
|
struct kick_sub_window *members[16];
|
|
struct kick_sub_window *m;
|
|
int n = 0;
|
|
wl_list_for_each(m, &app->sub_windows, link)
|
|
if (stack_mapped(m) && n < 16) members[n++] = m;
|
|
|
|
if (n == 0)
|
|
return false;
|
|
|
|
int cur = 0;
|
|
while (cur < n && members[cur] != app->focus_sub) cur++;
|
|
/* An unfocused tab position counts as the main surface (member 0),
|
|
so backward wraps to the newest window and forward to the oldest --
|
|
exactly where cycling picks up from a freshly shown tab. */
|
|
stack_enter(members[(cur + dir + n) % n]);
|
|
|
|
show_current(app);
|
|
kick_log(KICK_LOG_INFO, "[sub] cycle app=%d\n", app_index(app));
|
|
kick_request_redraw();
|
|
return true;
|
|
}
|
|
|
|
static void handle_sub_commit(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_sub_window *sub = wl_container_of(listener, sub, commit);
|
|
if (sub->toplevel) {
|
|
struct wlr_xdg_toplevel *toplevel = sub->toplevel;
|
|
/* Same treatment as a tab's own surface: maximize and size to the
|
|
output on first commit, then keep the CSD-gutter position in sync. */
|
|
if (toplevel->base->initial_commit) {
|
|
wlr_xdg_toplevel_set_maximized(toplevel, true);
|
|
wlr_xdg_toplevel_set_size(toplevel, server.output->width,
|
|
server.output->height);
|
|
if (server.clients_fullscreen)
|
|
wlr_xdg_toplevel_set_fullscreen(toplevel, true);
|
|
}
|
|
position_tree(toplevel, sub->tree);
|
|
}
|
|
#if HAVE_XWAYLAND
|
|
else if (sub->is_x11 && sub->xsurface && sub->tree) {
|
|
/* Keep the window's content at the tab origin, cropping any
|
|
self-placement offset (mirrors x11_sync_position). */
|
|
struct wlr_xwayland_surface *xs = sub->xsurface;
|
|
int nx = (xs->x > 0 && xs->x < server.output->width) ? -xs->x : 0;
|
|
int ny = (xs->y > 0 && xs->y < server.output->height) ? -xs->y : 0;
|
|
wlr_scene_node_set_position(&sub->tree->node, nx, ny);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void handle_sub_map(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_sub_window *sub = wl_container_of(listener, sub, map);
|
|
sub->mapped = true;
|
|
#if HAVE_XWAYLAND
|
|
/* X11 sub-windows have no initial_commit; configure them to the
|
|
maximized output size on every map (idempotent). */
|
|
if (sub->is_x11 && sub->xsurface && sub->xsurface->surface) {
|
|
wlr_xwayland_surface_configure(sub->xsurface, 0, 0,
|
|
(uint16_t)server.output->width,
|
|
(uint16_t)server.output->height);
|
|
wlr_xwayland_surface_set_maximized(sub->xsurface, true, true);
|
|
if (server.clients_fullscreen)
|
|
wlr_xwayland_surface_set_fullscreen(sub->xsurface, true);
|
|
}
|
|
#endif
|
|
/* A sub-window never steals focus: it appears with its owner tab and
|
|
takes the keyboard only when that tab is already the one shown. */
|
|
if (server.active_app == sub->owner) {
|
|
kick_log(KICK_LOG_INFO, "[sub] focus app=%d\n",
|
|
app_index(sub->owner));
|
|
sub_enter(sub);
|
|
} else {
|
|
kick_log(KICK_LOG_INFO, "[sub] map app=%d\n", app_index(sub->owner));
|
|
}
|
|
}
|
|
|
|
static void handle_sub_unmap(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_sub_window *sub = wl_container_of(listener, sub, unmap);
|
|
sub->mapped = false;
|
|
if (sub->owner->focus_sub == sub)
|
|
sub_refocus_owner(sub->owner);
|
|
}
|
|
|
|
/* Common tail of a stacked sub-window's destroy handling: drop out of the
|
|
owner's list, restore the keyboard if it was held, free. */
|
|
static void sub_destroy_common(struct kick_sub_window *sub) {
|
|
struct kick_app *app = sub->owner;
|
|
bool had_focus = app->focus_sub == sub;
|
|
wl_list_remove(&sub->link);
|
|
kick_log(KICK_LOG_INFO, "[sub] unstack app=%d\n", app_index(app));
|
|
if (had_focus) {
|
|
app->focus_sub = NULL;
|
|
sub_refocus_owner(app);
|
|
}
|
|
free(sub);
|
|
}
|
|
|
|
static void handle_sub_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_sub_window *sub = wl_container_of(listener, sub, destroy);
|
|
wl_list_remove(&sub->destroy.link);
|
|
wl_list_remove(&sub->map.link);
|
|
wl_list_remove(&sub->unmap.link);
|
|
wl_list_remove(&sub->commit.link);
|
|
sub_destroy_common(sub);
|
|
}
|
|
|
|
/* Wrap the tab's own main surface as an ordinary member of its window
|
|
stack ([main, oldest..newest stacked subs]): rotation, visibility and
|
|
overlay titles then read one plain list with no privileged entry. The
|
|
wrapper carries no listeners -- the tab-level map/commit/destroy
|
|
handlers stay authoritative -- and remove_app() frees it with the rest. */
|
|
static void stack_wrap_main(struct kick_app *app) {
|
|
struct kick_sub_window *m = calloc(1, sizeof(*m));
|
|
if (!m || !app->tree) {
|
|
free(m);
|
|
return;
|
|
}
|
|
m->is_main = true;
|
|
m->owner = app;
|
|
m->tree = app->tree;
|
|
#if HAVE_XWAYLAND
|
|
if (app->is_x11) {
|
|
m->is_x11 = true;
|
|
m->xsurface = app->xsurface;
|
|
} else
|
|
#endif
|
|
m->toplevel = app->toplevel;
|
|
wl_list_insert(app->sub_windows.prev, &m->link);
|
|
}
|
|
|
|
/* Attach `toplevel` as a stacked sub-window of `owner`: sibling of the
|
|
owner's main surface under its stack container (shows/hides with the
|
|
tab; show_current() keeps only the current window visible), same
|
|
maximize/size/position treatment as a tab. Never creates a slot. */
|
|
static void attach_stacked_sub(struct kick_app *owner,
|
|
struct wlr_xdg_toplevel *toplevel) {
|
|
struct kick_sub_window *sub = calloc(1, sizeof(*sub));
|
|
if (!sub) return;
|
|
sub->toplevel = toplevel;
|
|
sub->owner = owner;
|
|
sub->tree = owner->stack
|
|
? wlr_scene_xdg_surface_create(owner->stack, toplevel->base) : NULL;
|
|
if (!sub->tree) {
|
|
free(sub);
|
|
return;
|
|
}
|
|
/* show_current() picks the visible window once it maps; start hidden. */
|
|
wlr_scene_node_set_enabled(&sub->tree->node, false);
|
|
sub->commit.notify = handle_sub_commit;
|
|
wl_signal_add(&toplevel->base->surface->events.commit, &sub->commit);
|
|
sub->map.notify = handle_sub_map;
|
|
wl_signal_add(&toplevel->base->surface->events.map, &sub->map);
|
|
sub->unmap.notify = handle_sub_unmap;
|
|
wl_signal_add(&toplevel->base->surface->events.unmap, &sub->unmap);
|
|
sub->destroy.notify = handle_sub_destroy;
|
|
wl_signal_add(&toplevel->events.destroy, &sub->destroy);
|
|
wl_list_insert(owner->sub_windows.prev, &sub->link);
|
|
kick_log(KICK_LOG_INFO, "[sub] stack app=%d\n", app_index(owner));
|
|
}
|
|
|
|
static void handle_toplevel_map(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app, map);
|
|
struct wlr_xdg_toplevel *toplevel = app->toplevel;
|
|
if (kick_log_level() >= KICK_LOG_DEBUG)
|
|
kick_log(KICK_LOG_DEBUG, "[diag] map app=%d\n", app_index(app));
|
|
|
|
/* A new tab never steals focus on its own. The exceptions: a
|
|
Alt+Shift+T terminal (add_tab), a sub-window whose parent tab is the
|
|
one currently shown, and the startup-selected last command-line tab
|
|
when it finally maps (it has been the active, black tab all along,
|
|
revealed the moment its first frame arrives). Every other map stays
|
|
hidden -- the overlay and cycling reach it, but it does not yank the
|
|
active surface. A slot's toplevel is set at claim time (before its
|
|
map), so "is it already shown?" is whether the stack container node
|
|
is enabled, not whether active_toplevel() is set. */
|
|
bool activate = false;
|
|
if (app->origin == ORIGIN_ADD_TAB)
|
|
activate = true;
|
|
else if (app->origin == ORIGIN_SUB && app->parent_app &&
|
|
app->parent_app->toplevel == active_toplevel())
|
|
activate = true;
|
|
else if (app == server.active_app && !app_visible(app))
|
|
activate = true;
|
|
|
|
/* A slot claimed while it was still black may have been drawn with a
|
|
placeholder icon; once a real client maps it, re-resolve the icon so
|
|
the overlay picks up its app_id. */
|
|
app->icon_resolved = false;
|
|
|
|
if (activate) {
|
|
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;
|
|
|
|
pid_t cpid = -1;
|
|
if (toplevel->base->client)
|
|
wl_client_get_credentials(toplevel->base->client->client,
|
|
&cpid, NULL, NULL);
|
|
|
|
struct kick_app *app = NULL;
|
|
/* A slot awaiting its toplevel -- an unclaimed command-line command or
|
|
a fresh Alt+Shift+T terminal -- belongs to exactly ONE client: the one
|
|
WE launched for it. launch_client() runs each command in its own
|
|
process group (setpgid(0,0)), so the connected client's group id
|
|
equals the launcher pid app->pid, including through a shell wrapper
|
|
(`sh -c "foot -e htop"`). Matching by group -- not exact pid -- is
|
|
what keeps a wrapped command on its own command-line slot. A client
|
|
from a group we never created (an external one) never matches, so it
|
|
can't steal a still-loading tab's position. */
|
|
pid_t pgid = cpid > 0 ? getpgid(cpid) : -1;
|
|
for (int i = 0; i < server.num_apps && !app; i++) {
|
|
struct kick_app *a = server.apps[i];
|
|
if (a->toplevel || !a->pending_claim) continue;
|
|
if (a->pid > 0 && a->pid == pgid)
|
|
app = a;
|
|
}
|
|
/* A second toplevel from a client that already owns a tab is a sub-window
|
|
of that tab: give it its own slot glued right after its parent (the
|
|
protocol parent, else the owning tab), never touching a reserved
|
|
pending slot. Command-line commands are all forked at startup (see
|
|
launch_all_cli); their clients match the pending loop above, and only
|
|
then. */
|
|
if (!app && cpid > 0) {
|
|
struct kick_app *owner = sub_window_owner(cpid);
|
|
if (owner) {
|
|
/* Stack mode: no slot at all -- the window rides on its
|
|
owner's tab, above its surface. */
|
|
if (stack_sub_windows) {
|
|
attach_stacked_sub(owner, toplevel);
|
|
return;
|
|
}
|
|
if (apps_ensure_cap(server.num_apps + 1) != 0) return;
|
|
struct kick_app *sub = calloc(1, sizeof(*sub));
|
|
if (!sub) return;
|
|
sub->pid = -1;
|
|
sub->cmd = NULL;
|
|
sub->origin = ORIGIN_SUB;
|
|
sub->parent_app = owner;
|
|
wl_list_init(&sub->sub_windows);
|
|
struct kick_app *pa = toplevel->parent
|
|
? app_for_toplevel(toplevel->parent) : NULL;
|
|
insert_app_after(sub, pa ? pa : owner);
|
|
app = sub;
|
|
}
|
|
}
|
|
/* No pending slot matched (an external client, or one from a group we
|
|
never created). Closed tabs are removed outright, so there are no
|
|
free holes to fall back into; this loop only catches a slot that is
|
|
transiently without a toplevel and not reserved, so it usually falls
|
|
through to the ORIGIN_EXTERNAL append below. */
|
|
if (!app)
|
|
for (int i = 0; i < server.num_apps; i++) {
|
|
struct kick_app *a = server.apps[i];
|
|
if (!a->toplevel && !a->pending_claim && !a->closing) {
|
|
app = a;
|
|
break;
|
|
}
|
|
}
|
|
if (!app) {
|
|
/* No slot for this toplevel (an external client with every slot
|
|
taken). Give it its own tab so it appears on top and stays
|
|
reachable via tab cycling, right after the tab that created it
|
|
(protocol parent, else the active tab); far right if unidentified. */
|
|
if (apps_ensure_cap(server.num_apps + 1) != 0) return;
|
|
struct kick_app *extra = calloc(1, sizeof(*extra));
|
|
if (!extra) return;
|
|
extra->pid = -1;
|
|
extra->cmd = NULL;
|
|
extra->origin = ORIGIN_EXTERNAL;
|
|
wl_list_init(&extra->sub_windows);
|
|
struct kick_app *pa = toplevel->parent
|
|
? app_for_toplevel(toplevel->parent) : NULL;
|
|
if (!pa)
|
|
pa = app_for_toplevel(active_toplevel());
|
|
insert_app_after(extra, pa);
|
|
app = extra;
|
|
}
|
|
|
|
app->toplevel = toplevel;
|
|
app->pending_claim = false;
|
|
app->stack = wlr_scene_tree_create(&server.scene->tree);
|
|
app->tree = app->stack
|
|
? wlr_scene_xdg_surface_create(app->stack, toplevel->base) : NULL;
|
|
/* hidden until it becomes the active tab */
|
|
if (app->stack)
|
|
wlr_scene_node_set_enabled(&app->stack->node, false);
|
|
stack_wrap_main(app);
|
|
|
|
/* [claim] ties the app slot to the *connected client's* PID, so tests
|
|
can kill the tab that is actually active. The slot was chosen above by
|
|
the process-group match against a pending command (CLI or add_tab), by
|
|
the sub-window/parent rule, or by the ORIGIN_EXTERNAL append. */
|
|
/* The client that actually occupies this slot may be the command we
|
|
forked, an add_tab() tab, or an external client. Record its process
|
|
group so a close signals the right one -- but only when that group is
|
|
one WE own. An externally started client (e.g. from the compositor's
|
|
own terminal) shares our process group; adopting it would make
|
|
close/shutdown group-signal the launcher's whole session. Such
|
|
clients are killed by pid. */
|
|
app->client_pid = cpid;
|
|
pid_t cpg = cpid > 0 ? getpgid(cpid) : -1;
|
|
app->pgid = (cpg > 0 && cpg != getpgid(0)) ? cpg : -1;
|
|
kick_log(KICK_LOG_INFO, "[claim] app=%d pid=%d\n",
|
|
app_index(app), (int)cpid);
|
|
|
|
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);
|
|
}
|
|
|
|
#if HAVE_XWAYLAND
|
|
/* ===================== XWayland (X11 tabs) ===================== */
|
|
/* An X11 window becomes a tab exactly like an xdg toplevel: claimed against
|
|
a pending command-line slot by process group, glued next to its owner as
|
|
a sub-window, or appended as an external tab. Two structural differences
|
|
drive the code below:
|
|
- Every X11 window belongs to ONE Wayland client (the Xwayland process),
|
|
so client identity comes from xsurface->pid -- the real X client's PID
|
|
from _NET_WM_PID -- never from wl_client credentials.
|
|
- The wlr_surface only exists between the associate/dissociate events,
|
|
so the surface map/commit listeners are attached there, not at claim
|
|
time (dissociate always precedes destroy; a flag guards both orders).
|
|
Override-redirect windows (menus, tooltips) are handled further below:
|
|
they float above the tabs and never become slots. */
|
|
|
|
static void handle_xs_surface_map(struct wl_listener *listener, void *data);
|
|
static void handle_xs_commit(struct wl_listener *listener, void *data);
|
|
static void or_handle_map(struct wl_listener *listener, void *data);
|
|
static void or_handle_unmap(struct wl_listener *listener, void *data);
|
|
static void or_handle_commit(struct wl_listener *listener, void *data);
|
|
|
|
static struct kick_app *app_for_xsurface(struct wlr_xwayland_surface *xs) {
|
|
if (!xs) return NULL;
|
|
for (int i = 0; i < server.num_apps; i++)
|
|
if (server.apps[i]->xsurface == xs)
|
|
return server.apps[i];
|
|
return NULL;
|
|
}
|
|
|
|
/* Keep a managed X11 window's content at the tab origin: kick configures
|
|
every tab to the full output at 0,0, so a client that placed itself
|
|
elsewhere before obeying gets its offset cropped (mirrors position_tree's
|
|
CSD-gutter handling). Offsets beyond the output are ignored rather than
|
|
blanking the whole tab. */
|
|
static void x11_sync_position(struct kick_app *app) {
|
|
struct wlr_xwayland_surface *xs = app->xsurface;
|
|
if (!xs || !app->tree || !server.output) return;
|
|
int nx = (xs->x > 0 && xs->x < server.output->width) ? -xs->x : 0;
|
|
int ny = (xs->y > 0 && xs->y < server.output->height) ? -xs->y : 0;
|
|
wlr_scene_node_set_position(&app->tree->node, nx, ny);
|
|
}
|
|
|
|
/* Maximize-and-size treatment shared by new X11 tabs and stacked X11
|
|
sub-windows (X11 has no initial_commit to hook, and reconfiguring on
|
|
every map is idempotent). */
|
|
static void x11_initial_configure(struct wlr_xwayland_surface *xs) {
|
|
if (!xs || !xs->surface || !server.output) return;
|
|
wlr_xwayland_surface_configure(xs, 0, 0,
|
|
(uint16_t)server.output->width,
|
|
(uint16_t)server.output->height);
|
|
wlr_xwayland_surface_set_maximized(xs, true, true);
|
|
if (server.clients_fullscreen)
|
|
wlr_xwayland_surface_set_fullscreen(xs, true);
|
|
}
|
|
|
|
/* --- Stacked X11 sub-windows (--sub-windows stack) --- */
|
|
|
|
static void handle_sub_x_associate(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_sub_window *sub = wl_container_of(listener, sub, xs_associate);
|
|
if (!sub->xsurface || !sub->xsurface->surface || !sub->tree) return;
|
|
wlr_scene_surface_create(sub->tree, sub->xsurface->surface);
|
|
sub->map.notify = handle_sub_map;
|
|
wl_signal_add(&sub->xsurface->surface->events.map, &sub->map);
|
|
sub->unmap.notify = handle_sub_unmap;
|
|
wl_signal_add(&sub->xsurface->surface->events.unmap, &sub->unmap);
|
|
sub->commit.notify = handle_sub_commit;
|
|
wl_signal_add(&sub->xsurface->surface->events.commit, &sub->commit);
|
|
sub->xs_surface_linked = true;
|
|
}
|
|
|
|
static void handle_sub_x_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_sub_window *sub = wl_container_of(listener, sub, destroy);
|
|
wl_list_remove(&sub->destroy.link);
|
|
wl_list_remove(&sub->xs_associate.link);
|
|
if (sub->xs_surface_linked) {
|
|
wl_list_remove(&sub->map.link);
|
|
wl_list_remove(&sub->unmap.link);
|
|
wl_list_remove(&sub->commit.link);
|
|
sub->xs_surface_linked = false;
|
|
}
|
|
sub_destroy_common(sub);
|
|
}
|
|
|
|
/* Attach an X11 transient as a stacked sub-window: sibling of the owner's
|
|
main surface under its stack container, same lifecycle as
|
|
attach_stacked_sub() but with the association-driven surface wiring of
|
|
this surface kind. */
|
|
static void attach_stacked_sub_x11(struct kick_app *owner,
|
|
struct wlr_xwayland_surface *xsurface) {
|
|
struct kick_sub_window *sub = calloc(1, sizeof(*sub));
|
|
if (!sub) return;
|
|
sub->is_x11 = true;
|
|
sub->xsurface = xsurface;
|
|
sub->owner = owner;
|
|
sub->tree = owner->stack ? wlr_scene_tree_create(owner->stack) : NULL;
|
|
if (!sub->tree) {
|
|
free(sub);
|
|
return;
|
|
}
|
|
/* show_current() picks the visible window once it maps; start hidden. */
|
|
wlr_scene_node_set_enabled(&sub->tree->node, false);
|
|
if (xsurface->surface) {
|
|
/* Defensive: association normally happens after new_surface. */
|
|
handle_sub_x_associate(&sub->xs_associate, NULL);
|
|
}
|
|
sub->xs_associate.notify = handle_sub_x_associate;
|
|
wl_signal_add(&xsurface->events.associate, &sub->xs_associate);
|
|
sub->destroy.notify = handle_sub_x_destroy;
|
|
wl_signal_add(&xsurface->events.destroy, &sub->destroy);
|
|
wl_list_insert(owner->sub_windows.prev, &sub->link);
|
|
kick_log(KICK_LOG_INFO, "[sub] stack app=%d\n", app_index(owner));
|
|
}
|
|
|
|
/* --- Managed X11 tabs --- */
|
|
|
|
static void handle_xs_associate(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app, xs_associate);
|
|
if (!app->xsurface || !app->xsurface->surface || !app->tree) return;
|
|
/* The scene node rendering the window's buffers; recreated per
|
|
association (the wlr_surface dies with it). */
|
|
wlr_scene_surface_create(app->tree, app->xsurface->surface);
|
|
app->map.notify = handle_xs_surface_map;
|
|
wl_signal_add(&app->xsurface->surface->events.map, &app->map);
|
|
app->commit.notify = handle_xs_commit;
|
|
wl_signal_add(&app->xsurface->surface->events.commit, &app->commit);
|
|
app->xs_surface_linked = true;
|
|
}
|
|
|
|
static void handle_xs_dissociate(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app, xs_dissociate);
|
|
if (!app->xs_surface_linked) return;
|
|
wl_list_remove(&app->map.link);
|
|
wl_list_remove(&app->commit.link);
|
|
app->xs_surface_linked = false;
|
|
}
|
|
|
|
static void handle_xs_commit(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app, commit);
|
|
x11_sync_position(app);
|
|
}
|
|
|
|
static void handle_xs_surface_map(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app, map);
|
|
if (kick_log_level() >= KICK_LOG_DEBUG)
|
|
kick_log(KICK_LOG_DEBUG, "[diag] map app=%d\n", app_index(app));
|
|
|
|
x11_initial_configure(app->xsurface);
|
|
|
|
/* Same no-steal activation policy as handle_toplevel_map: an Alt+Shift+T
|
|
terminal, a sub-window of the shown tab, or the startup-selected black
|
|
tab revealing itself. Everything else waits hidden in the overlay. */
|
|
bool activate = false;
|
|
if (app->origin == ORIGIN_ADD_TAB)
|
|
activate = true;
|
|
else if (app->origin == ORIGIN_SUB && app->parent_app &&
|
|
app->parent_app == server.active_app)
|
|
activate = true;
|
|
else if (app == server.active_app && !app_visible(app))
|
|
activate = true;
|
|
|
|
/* A slot claimed while black may show a placeholder icon; re-resolve
|
|
against the now-known WM_CLASS. */
|
|
app->icon_resolved = false;
|
|
|
|
if (activate && !(app == server.active_app && app_visible(app)))
|
|
set_active_app(app);
|
|
}
|
|
|
|
static void handle_xs_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app, destroy);
|
|
app->is_x11 = false;
|
|
app->xsurface = NULL;
|
|
app->tree = NULL;
|
|
app->stack = NULL;
|
|
|
|
/* Same overlay-damage and icon cleanup as handle_toplevel_destroy. */
|
|
if (host.overlay_visible && server.standalone)
|
|
standalone_overlay_damage();
|
|
if (app->icon_tex) {
|
|
glDeleteTextures(1, &app->icon_tex);
|
|
app->icon_tex = 0;
|
|
}
|
|
|
|
/* wlroots asserts every event listener list empty after destroy. */
|
|
wl_list_remove(&app->destroy.link);
|
|
wl_list_remove(&app->xs_associate.link);
|
|
wl_list_remove(&app->xs_dissociate.link);
|
|
wl_list_remove(&app->request_maximize.link);
|
|
wl_list_remove(&app->request_fullscreen.link);
|
|
wl_list_remove(&app->request_minimize.link);
|
|
wl_list_remove(&app->xs_request_configure.link);
|
|
wl_list_remove(&app->xs_request_activate.link);
|
|
wl_list_remove(&app->xs_request_close.link);
|
|
wl_list_remove(&app->xs_set_title.link);
|
|
wl_list_remove(&app->xs_set_class.link);
|
|
if (app->xs_surface_linked) {
|
|
wl_list_remove(&app->map.link);
|
|
wl_list_remove(&app->commit.link);
|
|
app->xs_surface_linked = false;
|
|
}
|
|
|
|
finish_app_destroy(app);
|
|
}
|
|
|
|
static void handle_xs_request_maximize(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app, request_maximize);
|
|
/* Every kick tab lives maximized; acknowledge the state change. */
|
|
if (app->xsurface)
|
|
wlr_xwayland_surface_set_maximized(app->xsurface, true, true);
|
|
}
|
|
|
|
static void handle_xs_request_minimize(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener; (void)data;
|
|
/* kick has no minimized state: tabs are shown or hidden. */
|
|
}
|
|
|
|
static void handle_xs_request_fullscreen(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app, request_fullscreen);
|
|
struct wlr_xwayland_surface *xs = app->xsurface;
|
|
if (!xs) return;
|
|
/* Mirror onto the host window like the xdg path. */
|
|
bool fs = xs->fullscreen;
|
|
wlr_xwayland_surface_set_fullscreen(xs, fs);
|
|
kick_log(KICK_LOG_INFO, "client fullscreen=%d\n", fs);
|
|
if (host.frame) {
|
|
if (fs)
|
|
libdecor_frame_set_fullscreen(host.frame, NULL);
|
|
else
|
|
libdecor_frame_unset_fullscreen(host.frame);
|
|
}
|
|
}
|
|
|
|
static void handle_xs_request_configure(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app,
|
|
xs_request_configure);
|
|
/* The WM answer to any geometry request: maximized at the output size. */
|
|
x11_initial_configure(app->xsurface);
|
|
}
|
|
|
|
static void handle_xs_request_activate(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app, xs_request_activate);
|
|
/* An explicit activation request (the app's own window list, a pager)
|
|
may steal focus, exactly like Alt-cycling onto the tab. */
|
|
set_active_app(app);
|
|
}
|
|
|
|
static void handle_xs_request_close(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app, xs_request_close);
|
|
close_tab(app);
|
|
}
|
|
|
|
static void handle_xs_set_title(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app, xs_set_title);
|
|
/* Picked up by the overlay row on its next draw (row_title cache). */
|
|
(void)app;
|
|
}
|
|
|
|
static void handle_xs_set_class(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_app *app = wl_container_of(listener, app, xs_set_class);
|
|
/* Re-resolve the overlay icon against the new WM_CLASS. */
|
|
app->icon_resolved = false;
|
|
}
|
|
|
|
/* --- Override-redirect windows (menus, tooltips, popups) ---
|
|
Never tabs: they float above every tab tree at their absolute X position
|
|
while mapped, and take the keyboard only when their window type says they
|
|
want it (rofi-style launchers). */
|
|
|
|
struct kick_or_window {
|
|
struct wlr_xwayland_surface *xsurface;
|
|
struct wlr_scene_tree *tree;
|
|
bool surface_linked;
|
|
bool took_focus;
|
|
struct wl_listener destroy;
|
|
struct wl_listener associate;
|
|
struct wl_listener dissociate;
|
|
struct wl_listener map;
|
|
struct wl_listener unmap;
|
|
struct wl_listener commit;
|
|
struct wl_listener set_geometry;
|
|
struct wl_list link; /* server.or_windows */
|
|
};
|
|
|
|
static void or_window_position(struct kick_or_window *orw) {
|
|
struct wlr_xwayland_surface *xs = orw->xsurface;
|
|
if (!xs || !orw->tree) return;
|
|
/* Absolute layout coordinates: menus open where the app put them. */
|
|
wlr_scene_node_set_position(&orw->tree->node, xs->x, xs->y);
|
|
}
|
|
|
|
/* Hand the keyboard back to the tab that held it before this window took
|
|
it (or clear focus when nothing did). */
|
|
static void or_window_focus_release(struct kick_or_window *orw) {
|
|
if (!orw->took_focus) return;
|
|
orw->took_focus = false;
|
|
struct kick_app *owner = server.or_focus_owner;
|
|
server.or_focus_owner = NULL;
|
|
if (owner)
|
|
set_active_app(owner);
|
|
else
|
|
wlr_seat_keyboard_notify_clear_focus(server.seat);
|
|
}
|
|
|
|
static void or_handle_associate(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_or_window *orw = wl_container_of(listener, orw, associate);
|
|
if (!orw->xsurface || !orw->xsurface->surface || !orw->tree) return;
|
|
wlr_scene_surface_create(orw->tree, orw->xsurface->surface);
|
|
orw->map.notify = or_handle_map;
|
|
wl_signal_add(&orw->xsurface->surface->events.map, &orw->map);
|
|
orw->unmap.notify = or_handle_unmap;
|
|
wl_signal_add(&orw->xsurface->surface->events.unmap, &orw->unmap);
|
|
orw->commit.notify = or_handle_commit;
|
|
wl_signal_add(&orw->xsurface->surface->events.commit, &orw->commit);
|
|
orw->surface_linked = true;
|
|
or_window_position(orw);
|
|
}
|
|
|
|
static void or_handle_dissociate(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_or_window *orw = wl_container_of(listener, orw, dissociate);
|
|
if (!orw->surface_linked) return;
|
|
wl_list_remove(&orw->map.link);
|
|
wl_list_remove(&orw->unmap.link);
|
|
wl_list_remove(&orw->commit.link);
|
|
orw->surface_linked = false;
|
|
}
|
|
|
|
static void or_handle_map(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_or_window *orw = wl_container_of(listener, orw, map);
|
|
wlr_scene_node_set_enabled(&orw->tree->node, true);
|
|
or_window_position(orw);
|
|
if (wlr_xwayland_surface_override_redirect_wants_focus(orw->xsurface)) {
|
|
if (!server.or_focus_owner && server.active_app)
|
|
server.or_focus_owner = server.active_app;
|
|
orw->took_focus = true;
|
|
struct wlr_keyboard *kbd = wlr_seat_get_keyboard(server.seat);
|
|
if (!kbd) kbd = &server.keyboard;
|
|
wlr_seat_keyboard_notify_enter(server.seat,
|
|
orw->xsurface->surface,
|
|
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
|
|
}
|
|
kick_request_redraw();
|
|
}
|
|
|
|
static void or_handle_unmap(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_or_window *orw = wl_container_of(listener, orw, unmap);
|
|
wlr_scene_node_set_enabled(&orw->tree->node, false);
|
|
or_window_focus_release(orw);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
static void or_handle_commit(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_or_window *orw = wl_container_of(listener, orw, commit);
|
|
or_window_position(orw);
|
|
}
|
|
|
|
static void or_handle_set_geometry(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_or_window *orw = wl_container_of(listener, orw, set_geometry);
|
|
or_window_position(orw);
|
|
}
|
|
|
|
static void or_handle_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_or_window *orw = wl_container_of(listener, orw, destroy);
|
|
wl_list_remove(&orw->destroy.link);
|
|
wl_list_remove(&orw->associate.link);
|
|
wl_list_remove(&orw->dissociate.link);
|
|
wl_list_remove(&orw->set_geometry.link);
|
|
if (orw->surface_linked) {
|
|
wl_list_remove(&orw->map.link);
|
|
wl_list_remove(&orw->unmap.link);
|
|
wl_list_remove(&orw->commit.link);
|
|
orw->surface_linked = false;
|
|
}
|
|
or_window_focus_release(orw);
|
|
wl_list_remove(&orw->link);
|
|
free(orw);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
static void or_window_create(struct wlr_xwayland_surface *xsurface) {
|
|
struct kick_or_window *orw = calloc(1, sizeof(*orw));
|
|
if (!orw) return;
|
|
orw->xsurface = xsurface;
|
|
orw->tree = wlr_scene_tree_create(&server.scene->tree);
|
|
if (!orw->tree) {
|
|
free(orw);
|
|
return;
|
|
}
|
|
wlr_scene_node_set_enabled(&orw->tree->node, false);
|
|
orw->associate.notify = or_handle_associate;
|
|
wl_signal_add(&xsurface->events.associate, &orw->associate);
|
|
orw->dissociate.notify = or_handle_dissociate;
|
|
wl_signal_add(&xsurface->events.dissociate, &orw->dissociate);
|
|
orw->set_geometry.notify = or_handle_set_geometry;
|
|
wl_signal_add(&xsurface->events.set_geometry, &orw->set_geometry);
|
|
orw->destroy.notify = or_handle_destroy;
|
|
wl_signal_add(&xsurface->events.destroy, &orw->destroy);
|
|
if (xsurface->surface) {
|
|
/* Defensive: association normally happens after new_surface. */
|
|
or_handle_associate(&orw->associate, NULL);
|
|
}
|
|
wl_list_insert(server.or_windows.prev, &orw->link);
|
|
kick_log(KICK_LOG_INFO, "[x11] or window class=%s\n",
|
|
xsurface->class ? xsurface->class : "?");
|
|
}
|
|
|
|
static void handle_xwayland_new_surface(struct wl_listener *listener,
|
|
void *data) {
|
|
(void)listener;
|
|
struct wlr_xwayland_surface *xsurface = data;
|
|
|
|
if (xsurface->override_redirect) {
|
|
or_window_create(xsurface);
|
|
return;
|
|
}
|
|
|
|
/* The real X client's PID (_NET_WM_PID); 0 when the client didn't set
|
|
it, which lands the window in the external-append path below. */
|
|
pid_t xpid = xsurface->pid;
|
|
pid_t pgid = xpid > 0 ? getpgid(xpid) : -1;
|
|
|
|
/* Claim a pending slot by launcher process group: an X11 command forked
|
|
by launch_client inherits its group exactly like a Wayland client. */
|
|
struct kick_app *app = NULL;
|
|
for (int i = 0; i < server.num_apps && !app; i++) {
|
|
struct kick_app *a = server.apps[i];
|
|
if (app_has_window(a) || !a->pending_claim) continue;
|
|
if (a->pid > 0 && a->pid == pgid)
|
|
app = a;
|
|
}
|
|
/* A window from a process descended from a connected tab follows the
|
|
same sub-window rules as a second xdg toplevel. */
|
|
if (!app && xpid > 0) {
|
|
struct kick_app *owner = sub_window_owner(xpid);
|
|
if (owner) {
|
|
if (stack_sub_windows) {
|
|
attach_stacked_sub_x11(owner, xsurface);
|
|
return;
|
|
}
|
|
if (apps_ensure_cap(server.num_apps + 1) != 0) return;
|
|
struct kick_app *sub = calloc(1, sizeof(*sub));
|
|
if (!sub) return;
|
|
sub->pid = -1;
|
|
sub->cmd = NULL;
|
|
sub->origin = ORIGIN_SUB;
|
|
sub->parent_app = owner;
|
|
wl_list_init(&sub->sub_windows);
|
|
struct kick_app *pa = app_for_xsurface(xsurface->parent);
|
|
insert_app_after(sub, pa ? pa : owner);
|
|
app = sub;
|
|
}
|
|
}
|
|
/* Transient hole scan, then the external append -- mirrors
|
|
handle_new_toplevel. */
|
|
if (!app)
|
|
for (int i = 0; i < server.num_apps; i++) {
|
|
struct kick_app *a = server.apps[i];
|
|
if (!app_has_window(a) && !a->pending_claim && !a->closing) {
|
|
app = a;
|
|
break;
|
|
}
|
|
}
|
|
if (!app) {
|
|
if (apps_ensure_cap(server.num_apps + 1) != 0) return;
|
|
struct kick_app *extra = calloc(1, sizeof(*extra));
|
|
if (!extra) return;
|
|
extra->pid = -1;
|
|
extra->cmd = NULL;
|
|
extra->origin = ORIGIN_EXTERNAL;
|
|
wl_list_init(&extra->sub_windows);
|
|
struct kick_app *pa = app_for_xsurface(xsurface->parent);
|
|
if (!pa)
|
|
pa = server.active_app;
|
|
insert_app_after(extra, pa);
|
|
app = extra;
|
|
}
|
|
|
|
app->is_x11 = true;
|
|
app->xsurface = xsurface;
|
|
app->pending_claim = false;
|
|
app->stack = wlr_scene_tree_create(&server.scene->tree);
|
|
app->tree = app->stack ? wlr_scene_tree_create(app->stack) : NULL;
|
|
if (!app->tree) {
|
|
if (app->stack) {
|
|
wlr_scene_node_destroy(&app->stack->node);
|
|
app->stack = NULL;
|
|
}
|
|
app->is_x11 = false;
|
|
app->xsurface = NULL;
|
|
return;
|
|
}
|
|
/* hidden until it becomes the active tab */
|
|
wlr_scene_node_set_enabled(&app->stack->node, false);
|
|
stack_wrap_main(app);
|
|
|
|
app->client_pid = xpid;
|
|
pid_t cpg = xpid > 0 ? getpgid(xpid) : -1;
|
|
app->pgid = (cpg > 0 && cpg != getpgid(0)) ? cpg : -1;
|
|
kick_log(KICK_LOG_INFO, "[claim] app=%d pid=%d\n",
|
|
app_index(app), (int)xpid);
|
|
|
|
app->destroy.notify = handle_xs_destroy;
|
|
wl_signal_add(&xsurface->events.destroy, &app->destroy);
|
|
app->xs_associate.notify = handle_xs_associate;
|
|
wl_signal_add(&xsurface->events.associate, &app->xs_associate);
|
|
app->xs_dissociate.notify = handle_xs_dissociate;
|
|
wl_signal_add(&xsurface->events.dissociate, &app->xs_dissociate);
|
|
|
|
app->request_maximize.notify = handle_xs_request_maximize;
|
|
wl_signal_add(&xsurface->events.request_maximize, &app->request_maximize);
|
|
app->request_fullscreen.notify = handle_xs_request_fullscreen;
|
|
wl_signal_add(&xsurface->events.request_fullscreen,
|
|
&app->request_fullscreen);
|
|
app->request_minimize.notify = handle_xs_request_minimize;
|
|
wl_signal_add(&xsurface->events.request_minimize, &app->request_minimize);
|
|
app->xs_request_configure.notify = handle_xs_request_configure;
|
|
wl_signal_add(&xsurface->events.request_configure,
|
|
&app->xs_request_configure);
|
|
app->xs_request_activate.notify = handle_xs_request_activate;
|
|
wl_signal_add(&xsurface->events.request_activate,
|
|
&app->xs_request_activate);
|
|
app->xs_request_close.notify = handle_xs_request_close;
|
|
wl_signal_add(&xsurface->events.request_close, &app->xs_request_close);
|
|
app->xs_set_title.notify = handle_xs_set_title;
|
|
wl_signal_add(&xsurface->events.set_title, &app->xs_set_title);
|
|
app->xs_set_class.notify = handle_xs_set_class;
|
|
wl_signal_add(&xsurface->events.set_class, &app->xs_set_class);
|
|
}
|
|
|
|
static void handle_xwayland_ready(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
kick_log(KICK_LOG_INFO, "[xwayland] ready display=%s\n",
|
|
server.xwayland && server.xwayland->display_name
|
|
? server.xwayland->display_name : "?");
|
|
}
|
|
|
|
static void handle_xwayland_destroy(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
wl_list_remove(&server.xwayland_new_surface.link);
|
|
wl_list_remove(&server.xwayland_ready.link);
|
|
wl_list_remove(&server.xwayland_destroy.link);
|
|
server.xwayland = NULL;
|
|
kick_log(KICK_LOG_INFO, "[xwayland] gone\n");
|
|
}
|
|
#endif /* HAVE_XWAYLAND */
|
|
|
|
/* ===================== Tab management ===================== */
|
|
|
|
static struct kick_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];
|
|
/* A stacked sub-window's toplevel belongs to its owner tab's client. */
|
|
for (int i = 0; i < server.num_apps; i++) {
|
|
struct kick_app *app = server.apps[i];
|
|
struct kick_sub_window *sub;
|
|
wl_list_for_each(sub, &app->sub_windows, link)
|
|
if (sub->toplevel == toplevel)
|
|
return app;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* The tab whose client (by its wl_client) owns the given toplevel surface,
|
|
or NULL. Used to attribute a cursor-shape request to the requesting tab
|
|
so shapes are cached per app and reset on tab switches. */
|
|
static struct kick_app *app_for_client(struct wl_client *client) {
|
|
if (!client) return NULL;
|
|
for (int i = 0; i < server.num_apps; i++) {
|
|
struct kick_app *app = server.apps[i];
|
|
if (app->toplevel &&
|
|
wl_resource_get_client(app->toplevel->base->surface->resource) == client)
|
|
return app;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int app_index(struct kick_app *app) {
|
|
for (int i = 0; i < server.num_apps; i++)
|
|
if (server.apps[i] == app)
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
struct kick_popup {
|
|
struct wlr_xdg_surface *xdg_surface;
|
|
struct wlr_scene_tree *scene;
|
|
struct wl_listener commit;
|
|
struct wl_listener destroy;
|
|
struct wl_listener reposition;
|
|
};
|
|
|
|
/* Enough of the host window for popups to be positioned within; open menus
|
|
may exceed the toplevel but we keep them inside the visible area. */
|
|
static void popup_usable_box(struct wlr_box *box) {
|
|
box->x = 0;
|
|
box->y = 0;
|
|
if (server.output) {
|
|
box->width = server.output->width;
|
|
box->height = server.output->height;
|
|
} else {
|
|
box->width = host.width;
|
|
box->height = host.height;
|
|
}
|
|
}
|
|
|
|
/* Popups (menus, tooltips) are regular wlr_scene_xdg_surface nodes under
|
|
their parent's tree, so wlroots' scene renderer composites them into the
|
|
output buffer like any other surface. We only need to make sure the client
|
|
gets its initial configure (unconstrain from the output box) and to place
|
|
each popup under its parent's tree. */
|
|
static void popup_handle_commit(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_popup *tp = wl_container_of(listener, tp, commit);
|
|
struct wlr_xdg_surface *xdg_surface = tp->xdg_surface;
|
|
/* The client cannot map/render the popup until it receives its initial
|
|
configure, which wlroots only sends via wlr_xdg_popup_unconstrain_from
|
|
(which also adjusts geometry within the usable area). initial_commit is
|
|
true on the first commit; later commits are ignored. */
|
|
if (xdg_surface->initial_commit && xdg_surface->popup) {
|
|
struct wlr_box box;
|
|
popup_usable_box(&box);
|
|
wlr_xdg_popup_unconstrain_from_box(xdg_surface->popup, &box);
|
|
}
|
|
}
|
|
|
|
static void popup_handle_reposition(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_popup *tp = wl_container_of(listener, tp, reposition);
|
|
if (tp->xdg_surface->popup) {
|
|
struct wlr_box box;
|
|
popup_usable_box(&box);
|
|
wlr_xdg_popup_unconstrain_from_box(tp->xdg_surface->popup, &box);
|
|
}
|
|
}
|
|
|
|
static void popup_handle_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_popup *tp = wl_container_of(listener, tp, destroy);
|
|
wl_list_remove(&tp->commit.link);
|
|
wl_list_remove(&tp->destroy.link);
|
|
wl_list_remove(&tp->reposition.link);
|
|
free(tp);
|
|
}
|
|
|
|
static void create_popup(struct wlr_xdg_popup *popup,
|
|
struct wlr_scene_tree *parent_tree) {
|
|
struct wlr_xdg_surface *xdg_surface = popup->base;
|
|
struct kick_popup *tp = calloc(1, sizeof(*tp));
|
|
if (!tp) return;
|
|
tp->xdg_surface = xdg_surface;
|
|
tp->scene = wlr_scene_xdg_surface_create(parent_tree, xdg_surface);
|
|
/* wlr_scene_xdg_surface_create destroys the node when the surface is
|
|
destroyed, so we only track positioning/reposition listeners here. */
|
|
tp->commit.notify = popup_handle_commit;
|
|
wl_signal_add(&xdg_surface->surface->events.commit, &tp->commit);
|
|
tp->destroy.notify = popup_handle_destroy;
|
|
/* Listen on the popup's own destroy: wlroots emits it (then asserts its
|
|
event lists are empty) before the popup is freed. */
|
|
wl_signal_add(&popup->events.destroy, &tp->destroy);
|
|
tp->reposition.notify = popup_handle_reposition;
|
|
wl_signal_add(&popup->events.reposition, &tp->reposition);
|
|
/* remember the scene tree so a submenu popup can find its parent's tree */
|
|
xdg_surface->data = tp;
|
|
}
|
|
|
|
/* The scene tree a toplevel's popups must hang off: each stack member's
|
|
popups ride on that member's own tree (so they show only while it is
|
|
the current window and position within it) -- the wrapped main surface
|
|
is just the first member. */
|
|
static struct wlr_scene_tree *popup_tree_for_toplevel(
|
|
struct kick_app *app, struct wlr_xdg_toplevel *toplevel) {
|
|
if (!app || !toplevel) return NULL;
|
|
struct kick_sub_window *m;
|
|
wl_list_for_each(m, &app->sub_windows, link)
|
|
if (m->toplevel && m->toplevel == toplevel)
|
|
return m->tree;
|
|
return app->tree;
|
|
}
|
|
|
|
static void handle_new_popup(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_xdg_popup *popup = data;
|
|
|
|
/* Popups position themselves relative to their immediate parent surface,
|
|
so place each popup under its parent's scene tree. */
|
|
struct wlr_scene_tree *parent_tree = NULL;
|
|
struct wlr_xdg_surface *parent =
|
|
wlr_xdg_surface_try_from_wlr_surface(popup->parent);
|
|
if (parent) {
|
|
if (parent->role == WLR_XDG_SURFACE_ROLE_TOPLEVEL) {
|
|
struct kick_app *app = app_for_toplevel(parent->toplevel);
|
|
parent_tree = popup_tree_for_toplevel(app, parent->toplevel);
|
|
} else if (parent->role == WLR_XDG_SURFACE_ROLE_POPUP) {
|
|
struct kick_popup *tp = parent->data;
|
|
if (tp) parent_tree = tp->scene;
|
|
}
|
|
}
|
|
if (!parent_tree) return;
|
|
create_popup(popup, parent_tree);
|
|
}
|
|
|
|
/* Make `app` the active slot. A mapped slot shows its surface and takes
|
|
keyboard focus; a pending (black) slot shows the backdrop and drops the
|
|
seat focus until it maps. The old active slot is hidden and deactivated.
|
|
Called with the same app as already active only to promote it black ->
|
|
mapped (the one case with something still to do), so callers that want a
|
|
true no-op must guard first. */
|
|
|
|
/* Apply a cursor-shape-v1 shape value end to end: in standalone mode set the
|
|
composited cursor image from the theme; in nested mode forward the shape to
|
|
the host pointer so the host draws the right cursor over our window. */
|
|
static void apply_cursor_shape(int shape) {
|
|
if (shape == 0 || shape == server.cursor_shape)
|
|
return;
|
|
server.cursor_shape = shape;
|
|
|
|
const char *name = wlr_cursor_shape_v1_name(
|
|
(enum wp_cursor_shape_device_v1_shape)shape);
|
|
if (server.standalone) {
|
|
if (server.cursor && server.xcursor_mgr)
|
|
wlr_cursor_set_xcursor(server.cursor, server.xcursor_mgr, name);
|
|
kick_log(KICK_LOG_DEBUG, "[cursor] shape %s\n", name);
|
|
} else {
|
|
host_cursor_shape_set(shape);
|
|
}
|
|
}
|
|
|
|
/* A nested client switched its cursor shape via cursor-shape-v1. Cache the
|
|
shape on the requesting tab (so switching tabs resets to that tab's own
|
|
last shape, or default) and apply it. */
|
|
static void handle_shape_set(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_cursor_shape_manager_v1_request_set_shape_event *ev = data;
|
|
if (ev->device_type != WLR_CURSOR_SHAPE_MANAGER_V1_DEVICE_TYPE_POINTER)
|
|
return;
|
|
struct kick_app *app = app_for_client(ev->seat_client->client);
|
|
if (app)
|
|
app->cursor_shape = ev->shape;
|
|
apply_cursor_shape(ev->shape);
|
|
}
|
|
|
|
static void set_active_app(struct kick_app *app) {
|
|
if (!app) return;
|
|
|
|
struct kick_app *old = server.active_app;
|
|
if (old && old != app) {
|
|
/* A stacked sub-window holding the keyboard loses it with the tab. */
|
|
if (old->focus_sub) {
|
|
if (old->focus_sub->toplevel)
|
|
wlr_xdg_toplevel_set_activated(old->focus_sub->toplevel,
|
|
false);
|
|
old->focus_sub = NULL;
|
|
}
|
|
if (old->toplevel) {
|
|
/* Hide the whole window stack (main surface + stacked subs);
|
|
show_current() re-picks the visible window when it returns. */
|
|
if (old->stack)
|
|
wlr_scene_node_set_enabled(&old->stack->node, false);
|
|
wlr_xdg_toplevel_set_activated(old->toplevel, false);
|
|
}
|
|
}
|
|
server.active_app = app;
|
|
|
|
if (app_has_window(app)) {
|
|
app_set_activated(app, true);
|
|
position_toplevel(app);
|
|
if (app->stack)
|
|
wlr_scene_node_set_enabled(&app->stack->node, true);
|
|
if (kick_log_level() >= KICK_LOG_DEBUG) {
|
|
struct wlr_surface *s = app_surface(app);
|
|
kick_log(KICK_LOG_DEBUG, "[diag] active app=%d surface=%dx%d "
|
|
"node on\n", app_index(app),
|
|
s ? s->current.width : 0, s ? s->current.height : 0);
|
|
}
|
|
} else if (kick_log_level() >= KICK_LOG_DEBUG) {
|
|
kick_log(KICK_LOG_DEBUG, "[diag] active slot=%d (black, awaiting map)\n",
|
|
app_index(app));
|
|
}
|
|
|
|
/* In standalone the seat keyboard is the emitting native device (set by
|
|
the standalone handlers); fall back to the virtual keyboard otherwise
|
|
so the nested/ctl path keeps using it. A black active tab has no
|
|
surface to enter, so clear the focus instead of leaving a stale one.
|
|
The tab's window stack hands the keyboard to its topmost mapped
|
|
member -- newest stacked sub-window, else the wrapped main surface. */
|
|
struct kick_sub_window *top = top_mapped_sub(app);
|
|
if (top) {
|
|
/* Only genuine sub-window entries announce themselves; entering
|
|
the wrapped main surface is every plain tab's quiet default. */
|
|
if (!top->is_main)
|
|
kick_log(KICK_LOG_INFO, "[sub] focus app=%d\n", app_index(app));
|
|
stack_enter(top);
|
|
} else {
|
|
app->focus_sub = NULL;
|
|
wlr_seat_keyboard_notify_clear_focus(server.seat);
|
|
}
|
|
|
|
/* The keyboard settle above picked the tab's current window; now make
|
|
it the only drawn one (main surface hidden while a sub holds the
|
|
stack, other subs hidden behind the current one). */
|
|
show_current(app);
|
|
|
|
/* The newly-active tab owns the cursor shape: apply its last requested
|
|
shape (or default when it never asked) so a switched-from tab's shape
|
|
never bleeds onto the next tab. */
|
|
apply_cursor_shape(app->cursor_shape > 0
|
|
? app->cursor_shape
|
|
: WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
static void set_active_surface(struct wlr_xdg_toplevel *toplevel) {
|
|
struct kick_app *app = app_for_toplevel(toplevel);
|
|
if (!app) return;
|
|
/* Promoting the already-active black tab to mapped is the one same-app
|
|
call that must still run; everything else that's a true no-op returns
|
|
here, so redundant activations emit no [diag] churn. */
|
|
if (app == server.active_app && app->toplevel && app_visible(app))
|
|
return;
|
|
set_active_app(app);
|
|
}
|
|
|
|
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] == server.active_app) {
|
|
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 (app_live(server.apps[j]) && !server.apps[j]->closing) {
|
|
set_active_app(server.apps[j]);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Move the active tab one position up (dir>0: later in the list) or down
|
|
(dir<0) within the overlay order, wrapping at the ends. The active
|
|
surface is unchanged; only its slot in server.apps[] moves, which the
|
|
overlay and cycling both iterate. Only live slots (mapped or pending
|
|
black, mirroring cycle_tab) that aren't mid-close count as positions;
|
|
with fewer than two of them there is nowhere to move. */
|
|
static void reorder_active_tab(int dir) {
|
|
int n = 0;
|
|
for (int i = 0; i < server.num_apps; i++)
|
|
if (app_live(server.apps[i]) && !server.apps[i]->closing)
|
|
n++;
|
|
if (n < 2) return;
|
|
int from = -1;
|
|
for (int i = 0; i < server.num_apps; i++)
|
|
if (server.apps[i] == server.active_app) {
|
|
from = i;
|
|
break;
|
|
}
|
|
if (from < 0) return;
|
|
|
|
int neighbor = -1;
|
|
if (dir > 0) {
|
|
for (int i = from + 1; i < server.num_apps; i++)
|
|
if (app_live(server.apps[i]) && !server.apps[i]->closing) {
|
|
neighbor = i;
|
|
break;
|
|
}
|
|
if (neighbor < 0)
|
|
for (int i = 0; i < server.num_apps; i++)
|
|
if (app_live(server.apps[i]) && !server.apps[i]->closing) {
|
|
neighbor = i;
|
|
break;
|
|
}
|
|
} else {
|
|
for (int i = from - 1; i >= 0; i--)
|
|
if (app_live(server.apps[i]) && !server.apps[i]->closing) {
|
|
neighbor = i;
|
|
break;
|
|
}
|
|
if (neighbor < 0)
|
|
for (int i = server.num_apps - 1; i >= 0; i--)
|
|
if (app_live(server.apps[i]) && !server.apps[i]->closing) {
|
|
neighbor = i;
|
|
break;
|
|
}
|
|
}
|
|
if (neighbor < 0 || neighbor == from) return;
|
|
|
|
/* Move the tab to `neighbor`'s slot and shift every slot between the two
|
|
one position, so the middle of the list moves with it. For adjacent
|
|
tabs this is just a swap, but when `neighbor` wraps around to the far
|
|
end (from is first/last of the live list) the other live tabs must all
|
|
shuffle one place rather than staying put. */
|
|
struct kick_app *moved = server.apps[from];
|
|
if (neighbor < from) {
|
|
memmove(&server.apps[neighbor + 1], &server.apps[neighbor],
|
|
(size_t)(from - neighbor) * sizeof(*server.apps));
|
|
server.apps[neighbor] = moved;
|
|
} else {
|
|
memmove(&server.apps[from], &server.apps[from + 1],
|
|
(size_t)(neighbor - from) * sizeof(*server.apps));
|
|
server.apps[neighbor] = moved;
|
|
}
|
|
|
|
kick_log(KICK_LOG_INFO, "[reorder] app=%d dir=%d\n",
|
|
neighbor, dir);
|
|
kick_request_redraw();
|
|
if (server.standalone)
|
|
standalone_overlay_damage();
|
|
}
|
|
|
|
/* Fixed keybind repeat: first repeat after 600ms, then every 40ms (25Hz). */
|
|
#define KICK_REPEAT_DELAY_MS 600
|
|
#define KICK_REPEAT_INTERVAL_MS 40
|
|
|
|
static void kbd_repeat_cancel(void) {
|
|
if (host.repeat_timer) {
|
|
wl_event_source_remove(host.repeat_timer);
|
|
host.repeat_timer = NULL;
|
|
}
|
|
host.repeat_key = 0;
|
|
}
|
|
|
|
static int kbd_repeat_cb(void *data) {
|
|
(void)data;
|
|
if (host.repeat_key == 0)
|
|
return 0;
|
|
if (host.repeat_kind == REPEAT_REORDER)
|
|
reorder_active_tab(host.repeat_dir);
|
|
else if (host.repeat_kind == REPEAT_SUBCYCLE)
|
|
cycle_stack(server.active_app, host.repeat_dir);
|
|
else
|
|
cycle_tab(host.repeat_dir);
|
|
if (host.repeat_timer)
|
|
wl_event_source_timer_update(host.repeat_timer,
|
|
KICK_REPEAT_INTERVAL_MS);
|
|
return 0;
|
|
}
|
|
|
|
static void kbd_repeat_start(int key, int dir, int kind) {
|
|
kbd_repeat_cancel();
|
|
host.repeat_key = key;
|
|
host.repeat_dir = dir;
|
|
host.repeat_kind = kind;
|
|
host.repeat_timer = wl_event_loop_add_timer(server.loop, kbd_repeat_cb, NULL);
|
|
if (host.repeat_timer)
|
|
wl_event_source_timer_update(host.repeat_timer, KICK_REPEAT_DELAY_MS);
|
|
}
|
|
|
|
/* ===================== Clipboard bridge implementation ===================== */
|
|
|
|
static void bridge_mimes_reset(void) {
|
|
for (size_t i = 0; i < bridge.mimes_len; i++)
|
|
free(bridge.mimes[i]);
|
|
bridge.mimes_len = 0;
|
|
}
|
|
|
|
static void bridge_mimes_append(const char *mime) {
|
|
if (bridge.mimes_len == bridge.mimes_cap) {
|
|
size_t ncap = bridge.mimes_cap ? bridge.mimes_cap * 2 : 8;
|
|
char **nm = realloc(bridge.mimes, ncap * sizeof(*nm));
|
|
if (!nm)
|
|
return;
|
|
bridge.mimes = nm;
|
|
bridge.mimes_cap = ncap;
|
|
}
|
|
bridge.mimes[bridge.mimes_len++] = strdup(mime);
|
|
}
|
|
|
|
static double bridge_dbg_ms(void);
|
|
static void bridge_inner_src_destroy(struct wl_listener *l, void *data);
|
|
static bool bridge_prefetch_mime_compatible(const char *mime);
|
|
static void bridge_prefetch_serve(const char *mime, int32_t fd);
|
|
static bool bridge_primary_prefetch_mime_compatible(const char *mime);
|
|
static void bridge_primary_prefetch_serve(const char *mime, int32_t fd);
|
|
|
|
/* Compositor-side nested source: when a nested client pastes, wlroots calls
|
|
our impl->send() and we pull the data from the host offer straight into the
|
|
fd wlroots gave us (zero copy, no relay). */
|
|
static void bridge_inner_source_send(struct wlr_data_source *src,
|
|
const char *mime, int32_t fd) {
|
|
(void)src;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-src] t=%.0f inner_send mime=%s inner_src=%p "
|
|
"prefetch_ready=%d dc_offer=%p dd_offer=%p\n", bridge_dbg_ms(),
|
|
mime, (void *)bridge.inner_src, bridge.prefetch_ready,
|
|
(void *)bridge.dc_offer, (void *)bridge.dd_offer);
|
|
if (bridge_prefetch_mime_compatible(mime)) {
|
|
bridge_prefetch_serve(mime, fd);
|
|
return;
|
|
}
|
|
if (bridge.backend == BRIDGE_BACKEND_DATA_CONTROL && bridge.dc_offer) {
|
|
zwlr_data_control_offer_v1_receive(bridge.dc_offer, mime, fd);
|
|
close(fd);
|
|
} else if (bridge.backend == BRIDGE_BACKEND_WL_DATA_DEVICE &&
|
|
bridge.dd_offer) {
|
|
wl_data_offer_receive(bridge.dd_offer, mime, fd);
|
|
close(fd);
|
|
} else {
|
|
close(fd);
|
|
}
|
|
}
|
|
|
|
static void bridge_inner_source_destroy(struct wlr_data_source *src) {
|
|
free(src);
|
|
}
|
|
|
|
static const struct wlr_data_source_impl bridge_inner_source_impl = {
|
|
.send = bridge_inner_source_send,
|
|
.destroy = bridge_inner_source_destroy,
|
|
};
|
|
|
|
static void bridge_set_inner_src(struct wlr_data_source *src) {
|
|
if (bridge.inner_src == src)
|
|
return;
|
|
if (bridge.inner_src) {
|
|
wl_list_remove(&bridge.inner_src_destroy.link);
|
|
bridge.inner_src = NULL;
|
|
}
|
|
bridge.inner_src = src;
|
|
if (src) {
|
|
bridge.inner_src_destroy.notify = bridge_inner_src_destroy;
|
|
wl_signal_add(&src->events.destroy, &bridge.inner_src_destroy);
|
|
}
|
|
}
|
|
|
|
static void bridge_inner_src_destroy(struct wl_listener *l, void *data) {
|
|
(void)data;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-bridge] t=%.0f inner_src destroyed\n",
|
|
bridge_dbg_ms());
|
|
wl_list_remove(&l->link);
|
|
bridge.inner_src = NULL;
|
|
}
|
|
|
|
/* Put a host selection into the nested seat. The source stays valid for the
|
|
whole lifetime of the host offer, so pastes can be served asynchronously. */
|
|
static void bridge_mirror_to_nested(void) {
|
|
struct wlr_data_source *src = calloc(1, sizeof(*src));
|
|
if (!src) {
|
|
bridge_mimes_reset();
|
|
return;
|
|
}
|
|
wlr_data_source_init(src, &bridge_inner_source_impl);
|
|
for (size_t i = 0; i < bridge.mimes_len; i++) {
|
|
char **p = wl_array_add(&src->mime_types, sizeof(char *));
|
|
if (!p) {
|
|
for (size_t j = i; j < bridge.mimes_len; j++)
|
|
free(bridge.mimes[j]);
|
|
bridge.mimes_len = 0;
|
|
break;
|
|
}
|
|
*p = bridge.mimes[i];
|
|
bridge.mimes[i] = NULL; /* ownership moves into the wl_array */
|
|
}
|
|
bridge.mimes_len = 0;
|
|
bridge_set_inner_src(src);
|
|
wlr_seat_set_selection(server.seat, src,
|
|
wl_display_next_serial(server.wl_display));
|
|
}
|
|
|
|
static void bridge_clear_nested(void) {
|
|
if (!server.seat->selection_source)
|
|
return;
|
|
bridge.ignore_next_inner = true;
|
|
wlr_seat_set_selection(server.seat, NULL,
|
|
wl_display_next_serial(server.wl_display));
|
|
}
|
|
|
|
/* ---------------- primary selection mirror (host <-> nested seat) -------- */
|
|
|
|
/* The nested primary-selection source we install when a host offer is
|
|
mirrored in. A nested client asking for the data has the request pulled
|
|
straight from the host offer over the data-control pipe. */
|
|
static void bridge_inner_primary_source_send(struct wlr_primary_selection_source *src,
|
|
const char *mime, int32_t fd) {
|
|
(void)src;
|
|
if (bridge_primary_prefetch_mime_compatible(mime)) {
|
|
bridge_primary_prefetch_serve(mime, fd);
|
|
return;
|
|
}
|
|
if (bridge.backend == BRIDGE_BACKEND_DATA_CONTROL && bridge.dc_primary_offer) {
|
|
zwlr_data_control_offer_v1_receive(bridge.dc_primary_offer, mime, fd);
|
|
close(fd);
|
|
} else {
|
|
close(fd);
|
|
}
|
|
}
|
|
|
|
static void bridge_inner_primary_source_destroy(struct wlr_primary_selection_source *src) {
|
|
free(src);
|
|
}
|
|
|
|
static const struct wlr_primary_selection_source_impl bridge_inner_primary_source_impl = {
|
|
.send = bridge_inner_primary_source_send,
|
|
.destroy = bridge_inner_primary_source_destroy,
|
|
};
|
|
|
|
static void bridge_inner_primary_src_destroy(struct wl_listener *l, void *data) {
|
|
(void)data;
|
|
wl_list_remove(&l->link);
|
|
bridge.inner_primary_src = NULL;
|
|
}
|
|
|
|
static void bridge_set_inner_primary_src(struct wlr_primary_selection_source *src) {
|
|
if (bridge.inner_primary_src == src)
|
|
return;
|
|
if (bridge.inner_primary_src) {
|
|
wl_list_remove(&bridge.inner_primary_src_destroy.link);
|
|
bridge.inner_primary_src = NULL;
|
|
}
|
|
bridge.inner_primary_src = src;
|
|
if (src) {
|
|
bridge.inner_primary_src_destroy.notify = bridge_inner_primary_src_destroy;
|
|
wl_signal_add(&src->events.destroy, &bridge.inner_primary_src_destroy);
|
|
}
|
|
}
|
|
|
|
/* Put the collected host primary offer (bridge.mimes) into the nested seat. */
|
|
static void bridge_mirror_primary_to_nested(void) {
|
|
kick_log(KICK_LOG_DEBUG, "[bridge] mirror host primary into nested "
|
|
"(mimes=%zu)\n", bridge.mimes_len);
|
|
struct wlr_primary_selection_source *src = calloc(1, sizeof(*src));
|
|
if (!src) {
|
|
bridge_mimes_reset();
|
|
return;
|
|
}
|
|
wlr_primary_selection_source_init(src, &bridge_inner_primary_source_impl);
|
|
for (size_t i = 0; i < bridge.mimes_len; i++) {
|
|
char **p = wl_array_add(&src->mime_types, sizeof(char *));
|
|
if (!p) {
|
|
for (size_t j = i; j < bridge.mimes_len; j++)
|
|
free(bridge.mimes[j]);
|
|
bridge.mimes_len = 0;
|
|
break;
|
|
}
|
|
*p = bridge.mimes[i];
|
|
bridge.mimes[i] = NULL; /* ownership moves into the wl_array */
|
|
}
|
|
bridge.mimes_len = 0;
|
|
wlr_seat_set_primary_selection(server.seat, src,
|
|
wl_display_next_serial(server.wl_display));
|
|
}
|
|
|
|
static void bridge_primary_clear_nested(void) {
|
|
if (!server.seat->primary_selection_source)
|
|
return;
|
|
kick_log(KICK_LOG_DEBUG, "[bridge] clear nested primary\n");
|
|
bridge.primary_ignore_next_inner = true;
|
|
wlr_seat_set_primary_selection(server.seat, NULL,
|
|
wl_display_next_serial(server.wl_display));
|
|
}
|
|
|
|
/* ---------------- text prefetch: serve pastes from a buffered copy -------- */
|
|
|
|
#define KICK_PREFETCH_MAX (16 * 1024 * 1024)
|
|
|
|
static void bridge_prefetch_cancel(void) {
|
|
if (bridge.prefetch_src) {
|
|
wl_event_source_remove(bridge.prefetch_src);
|
|
bridge.prefetch_src = NULL;
|
|
}
|
|
if (bridge.prefetch_fd >= 0) {
|
|
close(bridge.prefetch_fd);
|
|
bridge.prefetch_fd = -1;
|
|
}
|
|
free(bridge.prefetch_buf);
|
|
bridge.prefetch_buf = NULL;
|
|
bridge.prefetch_len = bridge.prefetch_cap = 0;
|
|
bridge.prefetch_mime[0] = '\0';
|
|
bridge.prefetch_ready = false;
|
|
}
|
|
|
|
/* Drain the prefetch pipe into the buffer. Returns true when the stream is
|
|
finished (EOF/error) and the buffer should be kept as the paste payload. */
|
|
static bool bridge_prefetch_drain(int fd) {
|
|
for (;;) {
|
|
size_t room = bridge.prefetch_cap - bridge.prefetch_len;
|
|
if (bridge.prefetch_len + 4096 > bridge.prefetch_cap &&
|
|
bridge.prefetch_cap < KICK_PREFETCH_MAX) {
|
|
size_t ncap = bridge.prefetch_cap ? bridge.prefetch_cap * 2 : 8192;
|
|
if (ncap > KICK_PREFETCH_MAX)
|
|
ncap = KICK_PREFETCH_MAX;
|
|
char *nb = realloc(bridge.prefetch_buf, ncap);
|
|
if (nb) {
|
|
bridge.prefetch_buf = nb;
|
|
bridge.prefetch_cap = ncap;
|
|
room = bridge.prefetch_cap - bridge.prefetch_len;
|
|
}
|
|
}
|
|
if (room == 0) {
|
|
/* cap reached: drain without storing to keep the writer unblocked */
|
|
char tmp;
|
|
ssize_t n = read(fd, &tmp, 1);
|
|
if (n > 0)
|
|
continue;
|
|
if (n == 0)
|
|
return true;
|
|
if (errno == EINTR)
|
|
continue;
|
|
if (errno == EAGAIN)
|
|
return false;
|
|
return true;
|
|
}
|
|
ssize_t n = read(fd, bridge.prefetch_buf + bridge.prefetch_len, room);
|
|
if (n > 0) {
|
|
bridge.prefetch_len += (size_t)n;
|
|
continue;
|
|
}
|
|
if (n == 0)
|
|
return true; /* EOF */
|
|
if (errno == EINTR)
|
|
continue;
|
|
if (errno == EAGAIN)
|
|
return false; /* more data pending */
|
|
return true; /* read error: keep whatever we got */
|
|
}
|
|
}
|
|
|
|
static int bridge_prefetch_cb(int fd, uint32_t mask, void *data) {
|
|
(void)data; (void)mask;
|
|
if (!bridge_prefetch_drain(fd))
|
|
return 0;
|
|
if (bridge.prefetch_src) {
|
|
wl_event_source_remove(bridge.prefetch_src);
|
|
bridge.prefetch_src = NULL;
|
|
}
|
|
close(fd);
|
|
bridge.prefetch_fd = -1;
|
|
bridge.prefetch_ready = true;
|
|
return 0;
|
|
}
|
|
|
|
/* Pick the best text MIME offered for prefetching. */
|
|
static const char *bridge_pick_text_mime(void) {
|
|
static const char *pref[] = {
|
|
"text/plain;charset=utf-8", "text/plain", "UTF8_STRING", NULL,
|
|
};
|
|
for (int i = 0; pref[i]; i++)
|
|
for (size_t j = 0; j < bridge.mimes_len; j++)
|
|
if (strcmp(bridge.mimes[j], pref[i]) == 0)
|
|
return bridge.mimes[j];
|
|
for (size_t j = 0; j < bridge.mimes_len; j++)
|
|
if (strncmp(bridge.mimes[j], "text/", 5) == 0)
|
|
return bridge.mimes[j];
|
|
return NULL;
|
|
}
|
|
|
|
/* Start pulling the current host offer's primary text MIME into our buffer.
|
|
Must be called while bridge.mimes holds the freshly collected list. */
|
|
static void bridge_prefetch_start(void) {
|
|
bridge_prefetch_cancel();
|
|
if (!bridge.mimes_len)
|
|
return;
|
|
const char *mime = bridge_pick_text_mime();
|
|
if (!mime)
|
|
return;
|
|
int p[2];
|
|
if (pipe2(p, O_CLOEXEC | O_NONBLOCK) != 0)
|
|
return;
|
|
bridge.prefetch_fd = p[0];
|
|
bridge.prefetch_src = wl_event_loop_add_fd(server.loop, p[0],
|
|
WL_EVENT_READABLE, bridge_prefetch_cb, NULL);
|
|
if (!bridge.prefetch_src) {
|
|
close(p[0]);
|
|
close(p[1]);
|
|
bridge.prefetch_fd = -1;
|
|
return;
|
|
}
|
|
snprintf(bridge.prefetch_mime, sizeof(bridge.prefetch_mime), "%s", mime);
|
|
if (bridge.backend == BRIDGE_BACKEND_DATA_CONTROL && bridge.dc_offer) {
|
|
zwlr_data_control_offer_v1_receive(bridge.dc_offer, mime, p[1]);
|
|
close(p[1]);
|
|
} else if (bridge.backend == BRIDGE_BACKEND_WL_DATA_DEVICE &&
|
|
bridge.dd_offer) {
|
|
wl_data_offer_receive(bridge.dd_offer, mime, p[1]);
|
|
close(p[1]);
|
|
} else {
|
|
bridge_prefetch_cancel();
|
|
}
|
|
}
|
|
|
|
/* The requested MIME can be served from the prefetched plain-text buffer. */
|
|
static bool bridge_prefetch_mime_compatible(const char *mime) {
|
|
if (!bridge.prefetch_ready || bridge.prefetch_mime[0] == '\0')
|
|
return false;
|
|
if (strcmp(mime, bridge.prefetch_mime) == 0)
|
|
return true;
|
|
bool a = strncmp(mime, "text/plain", 10) == 0 ||
|
|
strcmp(mime, "UTF8_STRING") == 0;
|
|
bool b = strncmp(bridge.prefetch_mime, "text/plain", 10) == 0 ||
|
|
strcmp(bridge.prefetch_mime, "UTF8_STRING") == 0;
|
|
return a && b;
|
|
}
|
|
|
|
static void bridge_prefetch_serve(const char *mime, int32_t fd) {
|
|
if (!bridge_prefetch_mime_compatible(mime)) {
|
|
close(fd);
|
|
return;
|
|
}
|
|
size_t off = 0;
|
|
while (off < bridge.prefetch_len) {
|
|
ssize_t n = write(fd, bridge.prefetch_buf + off,
|
|
bridge.prefetch_len - off);
|
|
if (n > 0) {
|
|
off += (size_t)n;
|
|
} else if (n < 0 && (errno == EAGAIN || errno == EINTR)) {
|
|
continue;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
close(fd);
|
|
}
|
|
|
|
/* ---------------- primary-selection text prefetch ----------------------- */
|
|
|
|
#define KICK_PRIMARY_PREFETCH_MAX KICK_PREFETCH_MAX
|
|
|
|
static void bridge_primary_prefetch_cancel(void) {
|
|
if (bridge.primary_prefetch_src) {
|
|
wl_event_source_remove(bridge.primary_prefetch_src);
|
|
bridge.primary_prefetch_src = NULL;
|
|
}
|
|
if (bridge.primary_prefetch_fd >= 0) {
|
|
close(bridge.primary_prefetch_fd);
|
|
bridge.primary_prefetch_fd = -1;
|
|
}
|
|
free(bridge.primary_prefetch_buf);
|
|
bridge.primary_prefetch_buf = NULL;
|
|
bridge.primary_prefetch_len = bridge.primary_prefetch_cap = 0;
|
|
bridge.primary_prefetch_mime[0] = '\0';
|
|
bridge.primary_prefetch_ready = false;
|
|
}
|
|
|
|
static bool bridge_primary_prefetch_drain(int fd) {
|
|
for (;;) {
|
|
size_t room = bridge.primary_prefetch_cap - bridge.primary_prefetch_len;
|
|
if (bridge.primary_prefetch_len + 4096 > bridge.primary_prefetch_cap &&
|
|
bridge.primary_prefetch_cap < KICK_PRIMARY_PREFETCH_MAX) {
|
|
size_t ncap = bridge.primary_prefetch_cap
|
|
? bridge.primary_prefetch_cap * 2 : 8192;
|
|
if (ncap > KICK_PRIMARY_PREFETCH_MAX)
|
|
ncap = KICK_PRIMARY_PREFETCH_MAX;
|
|
char *nb = realloc(bridge.primary_prefetch_buf, ncap);
|
|
if (nb) {
|
|
bridge.primary_prefetch_buf = nb;
|
|
bridge.primary_prefetch_cap = ncap;
|
|
room = bridge.primary_prefetch_cap - bridge.primary_prefetch_len;
|
|
}
|
|
}
|
|
if (room == 0) {
|
|
char tmp;
|
|
ssize_t n = read(fd, &tmp, 1);
|
|
if (n > 0)
|
|
continue;
|
|
if (n == 0)
|
|
return true;
|
|
if (errno == EINTR)
|
|
continue;
|
|
if (errno == EAGAIN)
|
|
return false;
|
|
return true;
|
|
}
|
|
ssize_t n = read(fd, bridge.primary_prefetch_buf + bridge.primary_prefetch_len,
|
|
room);
|
|
if (n > 0) {
|
|
bridge.primary_prefetch_len += (size_t)n;
|
|
continue;
|
|
}
|
|
if (n == 0)
|
|
return true;
|
|
if (errno == EINTR)
|
|
continue;
|
|
if (errno == EAGAIN)
|
|
return false;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
static int bridge_primary_prefetch_cb(int fd, uint32_t mask, void *data) {
|
|
(void)data; (void)mask;
|
|
if (!bridge_primary_prefetch_drain(fd))
|
|
return 0;
|
|
if (bridge.primary_prefetch_src) {
|
|
wl_event_source_remove(bridge.primary_prefetch_src);
|
|
bridge.primary_prefetch_src = NULL;
|
|
}
|
|
close(fd);
|
|
bridge.primary_prefetch_fd = -1;
|
|
bridge.primary_prefetch_ready = true;
|
|
return 0;
|
|
}
|
|
|
|
/* Start pulling the current host primary offer's text MIME into our buffer.
|
|
Must be called while bridge.mimes holds the freshly collected list. */
|
|
static void bridge_primary_prefetch_start(void) {
|
|
bridge_primary_prefetch_cancel();
|
|
if (bridge.backend != BRIDGE_BACKEND_DATA_CONTROL || !bridge.dc_primary_offer)
|
|
return;
|
|
if (!bridge.mimes_len)
|
|
return;
|
|
const char *mime = bridge_pick_text_mime();
|
|
if (!mime)
|
|
return;
|
|
int p[2];
|
|
if (pipe2(p, O_CLOEXEC | O_NONBLOCK) != 0)
|
|
return;
|
|
bridge.primary_prefetch_fd = p[0];
|
|
bridge.primary_prefetch_src = wl_event_loop_add_fd(server.loop, p[0],
|
|
WL_EVENT_READABLE, bridge_primary_prefetch_cb, NULL);
|
|
if (!bridge.primary_prefetch_src) {
|
|
close(p[0]);
|
|
close(p[1]);
|
|
bridge.primary_prefetch_fd = -1;
|
|
return;
|
|
}
|
|
snprintf(bridge.primary_prefetch_mime, sizeof(bridge.primary_prefetch_mime),
|
|
"%s", mime);
|
|
zwlr_data_control_offer_v1_receive(bridge.dc_primary_offer, mime, p[1]);
|
|
close(p[1]);
|
|
}
|
|
|
|
/* The requested MIME can be served from the prefetched primary-text buffer. */
|
|
static bool bridge_primary_prefetch_mime_compatible(const char *mime) {
|
|
if (!bridge.primary_prefetch_ready || bridge.primary_prefetch_mime[0] == '\0')
|
|
return false;
|
|
if (strcmp(mime, bridge.primary_prefetch_mime) == 0)
|
|
return true;
|
|
bool a = strncmp(mime, "text/plain", 10) == 0 ||
|
|
strcmp(mime, "UTF8_STRING") == 0;
|
|
bool b = strncmp(bridge.primary_prefetch_mime, "text/plain", 10) == 0 ||
|
|
strcmp(bridge.primary_prefetch_mime, "UTF8_STRING") == 0;
|
|
return a && b;
|
|
}
|
|
|
|
static void bridge_primary_prefetch_serve(const char *mime, int32_t fd) {
|
|
if (!bridge_primary_prefetch_mime_compatible(mime)) {
|
|
close(fd);
|
|
return;
|
|
}
|
|
size_t off = 0;
|
|
while (off < bridge.primary_prefetch_len) {
|
|
ssize_t n = write(fd, bridge.primary_prefetch_buf + off,
|
|
bridge.primary_prefetch_len - off);
|
|
if (n > 0) {
|
|
off += (size_t)n;
|
|
} else if (n < 0 && (errno == EAGAIN || errno == EINTR)) {
|
|
continue;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
close(fd);
|
|
}
|
|
|
|
/* ---------------- host backend: wlr-data-control ---------------- */
|
|
|
|
static void bridge_dc_source_send(void *data,
|
|
struct zwlr_data_control_source_v1 *src, const char *mime, int32_t fd) {
|
|
(void)data; (void)src;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dc_source_send mime=%s "
|
|
"fd=%d inner_src=%p\n", bridge_dbg_ms(), mime, fd,
|
|
(void *)bridge.inner_src);
|
|
if (bridge.inner_src) {
|
|
wlr_data_source_send(bridge.inner_src, mime, fd);
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dc_source_send "
|
|
"forwarded mime=%s fd=%d\n", bridge_dbg_ms(), mime, fd);
|
|
} else
|
|
close(fd);
|
|
}
|
|
|
|
static void bridge_dc_source_cancelled(void *data,
|
|
struct zwlr_data_control_source_v1 *src) {
|
|
(void)data;
|
|
zwlr_data_control_source_v1_destroy(src);
|
|
bridge.dc_source = NULL;
|
|
bridge.dc_pending_echo = false;
|
|
bridge.dd_pending_echo = false;
|
|
}
|
|
|
|
static const struct zwlr_data_control_source_v1_listener bridge_dc_source_listener = {
|
|
.send = bridge_dc_source_send,
|
|
.cancelled = bridge_dc_source_cancelled,
|
|
};
|
|
|
|
/* Our published host primary-selection source: host middle-click requests
|
|
are served by asking the originating nested client for the data. */
|
|
static void bridge_dc_primary_source_send(void *data,
|
|
struct zwlr_data_control_source_v1 *src, const char *mime, int32_t fd) {
|
|
(void)data; (void)src;
|
|
if (bridge.inner_primary_src)
|
|
wlr_primary_selection_source_send(bridge.inner_primary_src, mime, fd);
|
|
else
|
|
close(fd);
|
|
}
|
|
|
|
static void bridge_dc_primary_source_cancelled(void *data,
|
|
struct zwlr_data_control_source_v1 *src) {
|
|
(void)data;
|
|
zwlr_data_control_source_v1_destroy(src);
|
|
bridge.dc_primary_source = NULL;
|
|
bridge.dc_primary_pending_echo = false;
|
|
}
|
|
|
|
static const struct zwlr_data_control_source_v1_listener bridge_dc_primary_source_listener = {
|
|
.send = bridge_dc_primary_source_send,
|
|
.cancelled = bridge_dc_primary_source_cancelled,
|
|
};
|
|
|
|
static void bridge_dc_offer_mime(void *data,
|
|
struct zwlr_data_control_offer_v1 *offer, const char *mime) {
|
|
(void)data; (void)offer;
|
|
bridge_mimes_append(mime);
|
|
}
|
|
|
|
static const struct zwlr_data_control_offer_v1_listener bridge_dc_offer_listener = {
|
|
.offer = bridge_dc_offer_mime,
|
|
};
|
|
|
|
static void bridge_dc_device_finished(void *data,
|
|
struct zwlr_data_control_device_v1 *dev) {
|
|
(void)data; (void)dev;
|
|
bridge.backend = bridge.dd ? BRIDGE_BACKEND_WL_DATA_DEVICE
|
|
: BRIDGE_BACKEND_NONE;
|
|
if (bridge.dc_offer) {
|
|
zwlr_data_control_offer_v1_destroy(bridge.dc_offer);
|
|
bridge.dc_offer = NULL;
|
|
}
|
|
if (bridge.dc_primary_offer) {
|
|
zwlr_data_control_offer_v1_destroy(bridge.dc_primary_offer);
|
|
bridge.dc_primary_offer = NULL;
|
|
}
|
|
if (bridge.dc_primary_echo_offer) {
|
|
zwlr_data_control_offer_v1_destroy(bridge.dc_primary_echo_offer);
|
|
bridge.dc_primary_echo_offer = NULL;
|
|
}
|
|
if (bridge.dc_primary_source) {
|
|
zwlr_data_control_source_v1_destroy(bridge.dc_primary_source);
|
|
bridge.dc_primary_source = NULL;
|
|
}
|
|
bridge.dc_primary_pending_echo = false;
|
|
bridge_primary_prefetch_cancel();
|
|
if (bridge.dc_device) {
|
|
zwlr_data_control_device_v1_destroy(bridge.dc_device);
|
|
bridge.dc_device = NULL;
|
|
}
|
|
kick_log(KICK_LOG_INFO, "clipboard bridge: host data-control "
|
|
"finished; falling back to %s\n",
|
|
bridge.backend == BRIDGE_BACKEND_WL_DATA_DEVICE ?
|
|
"focus-based wl_data_device" : "none");
|
|
}
|
|
|
|
static void bridge_dc_device_data_offer(void *data,
|
|
struct zwlr_data_control_device_v1 *dev,
|
|
struct zwlr_data_control_offer_v1 *offer) {
|
|
(void)data; (void)dev;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dc data_offer %p pending_echo=%d "
|
|
"primary_pending=%d\n", bridge_dbg_ms(), (void *)offer,
|
|
bridge.dc_pending_echo, bridge.dc_primary_pending_echo);
|
|
if (bridge.dc_pending_echo) {
|
|
bridge.dc_pending_echo = false;
|
|
bridge.dc_echo_offer = offer;
|
|
} else if (bridge.dc_primary_pending_echo) {
|
|
bridge.dc_primary_pending_echo = false;
|
|
bridge.dc_primary_echo_offer = offer;
|
|
}
|
|
bridge_mimes_reset();
|
|
zwlr_data_control_offer_v1_add_listener(offer, &bridge_dc_offer_listener, NULL);
|
|
}
|
|
|
|
static void bridge_dc_device_selection(void *data,
|
|
struct zwlr_data_control_device_v1 *dev,
|
|
struct zwlr_data_control_offer_v1 *offer) {
|
|
(void)data; (void)dev;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dc selection offer=%p pending_echo=%d "
|
|
"echo_offer=%p\n", bridge_dbg_ms(), (void *)offer,
|
|
bridge.dc_pending_echo, (void *)bridge.dc_echo_offer);
|
|
bridge_prefetch_cancel();
|
|
/* Destroying our own previous source in bridge_host_set_selection makes the
|
|
compositor briefly emit selection(NULL) before the echo of the new source;
|
|
it is not a real clear, so ignore it while our echo is still pending. */
|
|
if (!offer && bridge.dc_pending_echo)
|
|
return;
|
|
if (offer && offer == bridge.dc_echo_offer) {
|
|
bridge.dc_echo_offer = NULL;
|
|
bridge_mimes_reset();
|
|
zwlr_data_control_offer_v1_destroy(offer);
|
|
bridge.dc_pending_echo = false;
|
|
return;
|
|
}
|
|
bridge.dc_pending_echo = false;
|
|
bridge.dd_pending_echo = false;
|
|
if (bridge.dc_offer)
|
|
zwlr_data_control_offer_v1_destroy(bridge.dc_offer);
|
|
bridge.dc_offer = offer;
|
|
if (!offer) {
|
|
bridge_mimes_reset();
|
|
bridge_clear_nested();
|
|
return;
|
|
}
|
|
bridge_prefetch_start();
|
|
bridge_mirror_to_nested();
|
|
}
|
|
|
|
/* Primary selection bridged over the same data-control device (v2): the
|
|
host's primary_selection event mirrors the host's middle-click selection
|
|
into the nested seat (and the echo of our own set is swallowed). */
|
|
static void bridge_dc_device_primary_selection(void *data,
|
|
struct zwlr_data_control_device_v1 *dev,
|
|
struct zwlr_data_control_offer_v1 *offer) {
|
|
(void)data; (void)dev;
|
|
if (offer && offer == bridge.dc_primary_echo_offer) {
|
|
bridge.dc_primary_echo_offer = NULL;
|
|
bridge.dc_primary_pending_echo = false;
|
|
bridge_mimes_reset();
|
|
zwlr_data_control_offer_v1_destroy(offer);
|
|
kick_log(KICK_LOG_DEBUG, "[bridge] primary echo swallowed\n");
|
|
return;
|
|
}
|
|
if (offer == NULL) {
|
|
/* Like the clipboard's selection(NULL): destroying our previous source
|
|
in bridge_host_set_primary_selection makes the compositor briefly
|
|
emit primary_selection(NULL) before the echo of the new one, which is
|
|
not a real clear -- let pending stay armed for the echo's data_offer. */
|
|
if (bridge.dc_primary_pending_echo)
|
|
return;
|
|
bridge.dc_primary_offer = NULL;
|
|
bridge_mimes_reset();
|
|
bridge_primary_prefetch_cancel();
|
|
bridge_primary_clear_nested();
|
|
return;
|
|
}
|
|
bridge.dc_primary_pending_echo = false;
|
|
if (bridge.dc_primary_offer)
|
|
zwlr_data_control_offer_v1_destroy(bridge.dc_primary_offer);
|
|
bridge.dc_primary_offer = offer;
|
|
kick_log(KICK_LOG_DEBUG, "[bridge] host primary_selection offer\n");
|
|
/* The offer's mimes were collected by the preceding data_offer/offer
|
|
events; the data_offer handler has already reset bridge.mimes. */
|
|
bridge_primary_prefetch_start();
|
|
bridge_mirror_primary_to_nested();
|
|
}
|
|
|
|
static const struct zwlr_data_control_device_v1_listener bridge_dc_listener = {
|
|
.data_offer = bridge_dc_device_data_offer,
|
|
.selection = bridge_dc_device_selection,
|
|
.primary_selection = bridge_dc_device_primary_selection,
|
|
.finished = bridge_dc_device_finished,
|
|
};
|
|
|
|
/* ---------------- host backend: wl_data_device (focus-based) ---------------- */
|
|
|
|
static void bridge_dd_source_send(void *data, struct wl_data_source *src,
|
|
const char *mime, int32_t fd) {
|
|
(void)data; (void)src;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dd_source_send mime=%s fd=%d "
|
|
"inner_src=%p\n", bridge_dbg_ms(), mime, fd,
|
|
(void *)bridge.inner_src);
|
|
if (bridge.inner_src) {
|
|
wlr_data_source_send(bridge.inner_src, mime, fd);
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dd_source_send "
|
|
"forwarded mime=%s fd=%d\n", bridge_dbg_ms(), mime, fd);
|
|
} else
|
|
close(fd);
|
|
}
|
|
|
|
static void bridge_dd_source_cancelled(void *data, struct wl_data_source *src) {
|
|
(void)data;
|
|
wl_data_source_destroy(src);
|
|
bridge.dd_source = NULL;
|
|
bridge.dc_pending_echo = false;
|
|
bridge.dd_pending_echo = false;
|
|
bridge.host_set_pending = true;
|
|
}
|
|
|
|
static const struct wl_data_source_listener bridge_dd_source_listener = {
|
|
.send = bridge_dd_source_send,
|
|
.cancelled = bridge_dd_source_cancelled,
|
|
};
|
|
|
|
static void bridge_dd_offer_mime(void *data, struct wl_data_offer *offer,
|
|
const char *mime) {
|
|
(void)data; (void)offer;
|
|
bridge_mimes_append(mime);
|
|
}
|
|
|
|
static const struct wl_data_offer_listener bridge_dd_offer_listener = {
|
|
.offer = bridge_dd_offer_mime,
|
|
/* source_actions / action left unset: drag & drop is not bridged */
|
|
};
|
|
|
|
static void bridge_dd_data_offer(void *data, struct wl_data_device *dev,
|
|
struct wl_data_offer *offer) {
|
|
(void)data; (void)dev;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dd data_offer %p pending_echo=%d\n",
|
|
bridge_dbg_ms(), (void *)offer, bridge.dd_pending_echo);
|
|
if (bridge.dd_pending_echo) {
|
|
bridge.dd_pending_echo = false;
|
|
bridge.dd_echo_offer = offer;
|
|
}
|
|
bridge_mimes_reset();
|
|
wl_data_offer_add_listener(offer, &bridge_dd_offer_listener, NULL);
|
|
}
|
|
|
|
static void bridge_dd_selection(void *data, struct wl_data_device *dev,
|
|
struct wl_data_offer *offer) {
|
|
(void)data; (void)dev;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-host] t=%.0f dd selection offer=%p pending_echo=%d "
|
|
"echo_offer=%p\n", bridge_dbg_ms(), (void *)offer,
|
|
bridge.dd_pending_echo, (void *)bridge.dd_echo_offer);
|
|
if (!offer) {
|
|
/* Focus blip: the host selection is momentarily gone (or we lost
|
|
keyboard focus). Keep the last-known nested selection and, above
|
|
all, leave the pending-echo flags intact so an in-flight dc/dd
|
|
set_selection reply is still recognized as our own echo. */
|
|
bridge_mimes_reset();
|
|
return;
|
|
}
|
|
bridge_prefetch_cancel();
|
|
if (offer == bridge.dd_echo_offer) {
|
|
bridge.dd_echo_offer = NULL;
|
|
bridge_mimes_reset();
|
|
wl_data_offer_destroy(offer);
|
|
bridge.dd_pending_echo = false;
|
|
return;
|
|
}
|
|
bridge.dc_pending_echo = false;
|
|
bridge.dd_pending_echo = false;
|
|
if (bridge.dd_offer)
|
|
wl_data_offer_destroy(bridge.dd_offer);
|
|
bridge.dd_offer = offer;
|
|
bridge_prefetch_start();
|
|
bridge_mirror_to_nested();
|
|
}
|
|
|
|
static const struct wl_data_device_listener bridge_dd_listener = {
|
|
.data_offer = bridge_dd_data_offer,
|
|
.selection = bridge_dd_selection,
|
|
/* enter/leave/motion/drop left unset: drag & drop is not bridged */
|
|
};
|
|
|
|
/* ---------------- nested seat side ---------------- */
|
|
|
|
static void bridge_dbg_src_destroyed(struct wl_listener *l, void *data) {
|
|
(void)data;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-src] t=%.0f nested selection source destroyed\n",
|
|
bridge_dbg_ms());
|
|
wl_list_remove(&l->link);
|
|
free(l);
|
|
}
|
|
|
|
static double bridge_dbg_ms(void) {
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
|
|
}
|
|
|
|
static void bridge_seat_request_set_selection(struct wl_listener *l, void *data) {
|
|
(void)l;
|
|
struct wlr_seat_request_set_selection_event *event = data;
|
|
bridge.inner_clear_requested = (event->source == NULL);
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-bridge] t=%.0f request_set_selection "
|
|
"src=%p impl=%p serial=%u\n", bridge_dbg_ms(), (void *)event->source,
|
|
(void *)(event->source ? event->source->impl : NULL),
|
|
event->serial);
|
|
if (event->source) {
|
|
struct wl_listener *dl = calloc(1, sizeof(*dl));
|
|
if (dl) {
|
|
dl->notify = bridge_dbg_src_destroyed;
|
|
wl_signal_add(&event->source->events.destroy, dl);
|
|
}
|
|
}
|
|
wlr_seat_set_selection(server.seat, event->source, event->serial);
|
|
}
|
|
|
|
static void seat_request_set_primary_selection(struct wl_listener *l,
|
|
void *data) {
|
|
(void)l;
|
|
struct wlr_seat_request_set_primary_selection_event *event = data;
|
|
bridge.inner_primary_clear_requested = (event->source == NULL);
|
|
wlr_seat_set_primary_selection(server.seat, event->source, event->serial);
|
|
}
|
|
|
|
static void bridge_host_set_selection(struct wlr_data_source *src) {
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-bridge] t=%.0f host_set_selection src=%p impl=%p "
|
|
"backend=%d dc_device=%p mimes=%zu\n", bridge_dbg_ms(),
|
|
(void *)src, (void *)(src ? src->impl : NULL), bridge.backend,
|
|
(void *)bridge.dc_device,
|
|
src ? src->mime_types.size / sizeof(char *) : 0);
|
|
bridge_set_inner_src(src);
|
|
|
|
if (bridge.backend == BRIDGE_BACKEND_DATA_CONTROL && bridge.dc_device) {
|
|
if (bridge.dc_source) {
|
|
zwlr_data_control_source_v1_destroy(bridge.dc_source);
|
|
bridge.dc_source = NULL;
|
|
}
|
|
struct zwlr_data_control_source_v1 *s =
|
|
zwlr_data_control_manager_v1_create_data_source(bridge.dc_manager);
|
|
char **p;
|
|
wl_array_for_each(p, &src->mime_types)
|
|
zwlr_data_control_source_v1_offer(s, *p);
|
|
zwlr_data_control_source_v1_add_listener(s, &bridge_dc_source_listener, NULL);
|
|
bridge.dc_source = s;
|
|
bridge.dc_pending_echo = true;
|
|
bridge.dd_pending_echo = true;
|
|
zwlr_data_control_device_v1_set_selection(bridge.dc_device, s);
|
|
bridge.host_set_pending = false;
|
|
return;
|
|
}
|
|
|
|
if (bridge.backend == BRIDGE_BACKEND_WL_DATA_DEVICE && bridge.dd) {
|
|
if (bridge.dd_source) {
|
|
wl_data_source_destroy(bridge.dd_source);
|
|
bridge.dd_source = NULL;
|
|
}
|
|
if (!bridge.kbd_serial) {
|
|
bridge.host_set_pending = true;
|
|
kick_log(KICK_LOG_INFO, "clipboard bridge: no host keyboard "
|
|
"serial yet; deferring host set_selection until focus\n");
|
|
return;
|
|
}
|
|
struct wl_data_source *s =
|
|
wl_data_device_manager_create_data_source(bridge.ddm);
|
|
char **p;
|
|
wl_array_for_each(p, &src->mime_types)
|
|
wl_data_source_offer(s, *p);
|
|
wl_data_source_add_listener(s, &bridge_dd_source_listener, NULL);
|
|
bridge.dd_source = s;
|
|
bridge.dc_pending_echo = true;
|
|
bridge.dd_pending_echo = true;
|
|
wl_data_device_set_selection(bridge.dd, s, bridge.kbd_serial);
|
|
bridge.host_set_pending = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void bridge_host_clear(void) {
|
|
if (bridge.backend == BRIDGE_BACKEND_DATA_CONTROL && bridge.dc_device)
|
|
zwlr_data_control_device_v1_set_selection(bridge.dc_device, NULL);
|
|
/* focus-based backend: nothing reliable to clear without focus/serial */
|
|
}
|
|
|
|
static void bridge_host_set_primary_selection(struct wlr_primary_selection_source *src) {
|
|
bridge_set_inner_primary_src(src);
|
|
/* A nested client is overriding whatever host primary was mirrored in, so
|
|
the mirror's prefetch is stale. */
|
|
bridge_primary_prefetch_cancel();
|
|
|
|
if (bridge.backend != BRIDGE_BACKEND_DATA_CONTROL || !bridge.dc_device
|
|
|| !bridge.dc_primary_supported)
|
|
return; /* wl_data_device / v1 data-control have no primary channel */
|
|
if (bridge.dc_primary_source) {
|
|
zwlr_data_control_source_v1_destroy(bridge.dc_primary_source);
|
|
bridge.dc_primary_source = NULL;
|
|
}
|
|
if (!src)
|
|
return;
|
|
struct zwlr_data_control_source_v1 *s =
|
|
zwlr_data_control_manager_v1_create_data_source(bridge.dc_manager);
|
|
char **p;
|
|
wl_array_for_each(p, &src->mime_types)
|
|
zwlr_data_control_source_v1_offer(s, *p);
|
|
zwlr_data_control_source_v1_add_listener(s,
|
|
&bridge_dc_primary_source_listener, NULL);
|
|
bridge.dc_primary_source = s;
|
|
bridge.dc_primary_pending_echo = true;
|
|
zwlr_data_control_device_v1_set_primary_selection(bridge.dc_device, s);
|
|
kick_log(KICK_LOG_INFO, "clipboard bridge: primary selection -> host\n");
|
|
}
|
|
|
|
static void bridge_host_primary_clear(void) {
|
|
if (bridge.backend == BRIDGE_BACKEND_DATA_CONTROL && bridge.dc_device
|
|
&& bridge.dc_primary_supported)
|
|
zwlr_data_control_device_v1_set_primary_selection(bridge.dc_device, NULL);
|
|
}
|
|
|
|
static void bridge_seat_set_primary_selection(struct wl_listener *l, void *data) {
|
|
(void)l; (void)data;
|
|
struct wlr_primary_selection_source *src = server.seat->primary_selection_source;
|
|
if (src && src->impl == &bridge_inner_primary_source_impl)
|
|
return; /* our own host->nested mirror */
|
|
if (bridge.primary_ignore_next_inner) {
|
|
bridge.primary_ignore_next_inner = false;
|
|
return;
|
|
}
|
|
if (!src) {
|
|
if (bridge.inner_primary_clear_requested) {
|
|
bridge.inner_primary_clear_requested = false;
|
|
bridge_host_primary_clear();
|
|
}
|
|
return;
|
|
}
|
|
bridge.inner_primary_clear_requested = false;
|
|
bridge_host_set_primary_selection(src);
|
|
}
|
|
|
|
static void bridge_seat_set_selection(struct wl_listener *l, void *data) {
|
|
(void)l; (void)data;
|
|
struct wlr_data_source *src = server.seat->selection_source;
|
|
kick_log(KICK_LOG_DEBUG, "[dbg-bridge] t=%.0f seat set_selection src=%p "
|
|
"impl=%p inner_mirror=%d ignore_next=%d inner_clear_requested=%d\n",
|
|
bridge_dbg_ms(), (void *)src, (void *)(src ? src->impl : NULL),
|
|
src && src->impl == &bridge_inner_source_impl,
|
|
bridge.ignore_next_inner, bridge.inner_clear_requested);
|
|
if (src && src->impl == &bridge_inner_source_impl)
|
|
return; /* our own host->nested mirror */
|
|
if (bridge.ignore_next_inner) {
|
|
bridge.ignore_next_inner = false;
|
|
return;
|
|
}
|
|
if (!src) {
|
|
if (bridge.inner_clear_requested) {
|
|
bridge.inner_clear_requested = false;
|
|
bridge_host_clear();
|
|
}
|
|
return;
|
|
}
|
|
bridge.inner_clear_requested = false;
|
|
bridge_host_set_selection(src);
|
|
}
|
|
|
|
/* ---------------- lifecycle ---------------- */
|
|
|
|
static void bridge_attach_seat(void) {
|
|
bridge.request_set_selection.notify = bridge_seat_request_set_selection;
|
|
wl_signal_add(&server.seat->events.request_set_selection,
|
|
&bridge.request_set_selection);
|
|
bridge.set_selection.notify = bridge_seat_set_selection;
|
|
wl_signal_add(&server.seat->events.set_selection, &bridge.set_selection);
|
|
bridge.set_primary_selection.notify = bridge_seat_set_primary_selection;
|
|
wl_signal_add(&server.seat->events.set_primary_selection,
|
|
&bridge.set_primary_selection);
|
|
}
|
|
|
|
static void bridge_init(void) {
|
|
if (!host.seat) {
|
|
kick_log(KICK_LOG_INFO, "clipboard bridge: no host seat; disabled\n");
|
|
return;
|
|
}
|
|
/* Re-entrant: drop any objects bound to a previous host seat so a runtime
|
|
seat re-add (registry churn, e.g. VT round-trip) re-binds cleanly. */
|
|
if (bridge.dd)
|
|
wl_data_device_release(bridge.dd);
|
|
bridge.dd = NULL;
|
|
if (bridge.dc_device)
|
|
zwlr_data_control_device_v1_destroy(bridge.dc_device);
|
|
bridge.dc_device = NULL;
|
|
if (bridge.dc_primary_source) {
|
|
zwlr_data_control_source_v1_destroy(bridge.dc_primary_source);
|
|
bridge.dc_primary_source = NULL;
|
|
}
|
|
if (bridge.dc_primary_offer) {
|
|
zwlr_data_control_offer_v1_destroy(bridge.dc_primary_offer);
|
|
bridge.dc_primary_offer = NULL;
|
|
}
|
|
if (bridge.dc_primary_echo_offer) {
|
|
zwlr_data_control_offer_v1_destroy(bridge.dc_primary_echo_offer);
|
|
bridge.dc_primary_echo_offer = NULL;
|
|
}
|
|
bridge.dc_primary_pending_echo = false;
|
|
bridge_primary_prefetch_cancel();
|
|
bridge.dd_offer = NULL;
|
|
bridge.dc_offer = NULL;
|
|
bridge.dd_echo_offer = NULL;
|
|
bridge.dc_echo_offer = NULL;
|
|
bridge.backend = BRIDGE_BACKEND_NONE;
|
|
if (bridge.ddm) {
|
|
bridge.dd = wl_data_device_manager_get_data_device(bridge.ddm, host.seat);
|
|
wl_data_device_add_listener(bridge.dd, &bridge_dd_listener, NULL);
|
|
}
|
|
if (bridge.dc_manager) {
|
|
bridge.dc_device = zwlr_data_control_manager_v1_get_data_device(
|
|
bridge.dc_manager, host.seat);
|
|
zwlr_data_control_device_v1_add_listener(bridge.dc_device,
|
|
&bridge_dc_listener, NULL);
|
|
bridge.backend = BRIDGE_BACKEND_DATA_CONTROL;
|
|
kick_log(KICK_LOG_INFO, "clipboard bridge: host data-control backend "
|
|
"(clipboard and primary selection bridged)\n");
|
|
} else if (bridge.dd) {
|
|
bridge.backend = BRIDGE_BACKEND_WL_DATA_DEVICE;
|
|
kick_log(KICK_LOG_INFO, "clipboard bridge: focus-based wl_data_device "
|
|
"backend (host lacks wlr-data-control; host->nested sync needs "
|
|
"focus; primary selection not bridged)\n");
|
|
} else {
|
|
kick_log(KICK_LOG_INFO, "clipboard bridge: no host clipboard path\n");
|
|
}
|
|
}
|
|
|
|
static void bridge_cleanup(void) {
|
|
if (bridge.inner_src) {
|
|
wl_list_remove(&bridge.inner_src_destroy.link);
|
|
bridge.inner_src = NULL;
|
|
}
|
|
if (bridge.inner_primary_src) {
|
|
wl_list_remove(&bridge.inner_primary_src_destroy.link);
|
|
bridge.inner_primary_src = NULL;
|
|
}
|
|
if (bridge.dc_source) {
|
|
zwlr_data_control_source_v1_destroy(bridge.dc_source);
|
|
bridge.dc_source = NULL;
|
|
}
|
|
if (bridge.dc_primary_source) {
|
|
zwlr_data_control_source_v1_destroy(bridge.dc_primary_source);
|
|
bridge.dc_primary_source = NULL;
|
|
}
|
|
if (bridge.dd_source) {
|
|
wl_data_source_destroy(bridge.dd_source);
|
|
bridge.dd_source = NULL;
|
|
}
|
|
if (bridge.dc_offer) {
|
|
zwlr_data_control_offer_v1_destroy(bridge.dc_offer);
|
|
bridge.dc_offer = NULL;
|
|
}
|
|
if (bridge.dc_primary_offer) {
|
|
zwlr_data_control_offer_v1_destroy(bridge.dc_primary_offer);
|
|
bridge.dc_primary_offer = NULL;
|
|
}
|
|
if (bridge.dc_primary_echo_offer) {
|
|
zwlr_data_control_offer_v1_destroy(bridge.dc_primary_echo_offer);
|
|
bridge.dc_primary_echo_offer = NULL;
|
|
}
|
|
if (bridge.dd_offer) {
|
|
wl_data_offer_destroy(bridge.dd_offer);
|
|
bridge.dd_offer = NULL;
|
|
}
|
|
if (bridge.dc_device) {
|
|
zwlr_data_control_device_v1_destroy(bridge.dc_device);
|
|
bridge.dc_device = NULL;
|
|
}
|
|
bridge_mimes_reset();
|
|
free(bridge.mimes);
|
|
bridge.mimes = NULL;
|
|
bridge.mimes_cap = 0;
|
|
bridge_prefetch_cancel();
|
|
bridge_primary_prefetch_cancel();
|
|
bridge.backend = BRIDGE_BACKEND_NONE;
|
|
}
|
|
|
|
/* ===================== Host-side listeners ===================== */
|
|
|
|
/* The host compositor draws the nested cursor over our window, so a nested
|
|
client's cursor-shape-v1 request is forwarded to the host's
|
|
wp_cursor_shape_device_v1 (bound to our host wl_pointer). The host only
|
|
honors the shape while the pointer sits over our surface and ignores
|
|
requests with a stale serial, hence host.ptr_serial. When the host lacks
|
|
cursor-shape-v1 this is a no-op and the host keeps its default cursor. */
|
|
static void host_cursor_shape_get_pointer(void) {
|
|
if (!host.shape_mgr || !host.pointer) {
|
|
host.shape_dev = NULL;
|
|
return;
|
|
}
|
|
/* get_pointer = the sole new_id request (opcode 1); the interface symbol
|
|
comes from libwlr (we must not also include the generated client header
|
|
here: both server and client headers define the shape enum). */
|
|
host.shape_dev = (struct wp_cursor_shape_device_v1 *)wl_proxy_marshal_flags(
|
|
(struct wl_proxy *)host.shape_mgr, 1 /* WP_CURSOR_SHAPE_MANAGER_V1_GET_POINTER */,
|
|
&wp_cursor_shape_device_v1_interface,
|
|
wl_proxy_get_version((struct wl_proxy *)host.shape_mgr),
|
|
0, NULL, (struct wl_proxy *)host.pointer);
|
|
}
|
|
|
|
static void host_cursor_shape_set(int shape) {
|
|
if (!host.shape_dev) {
|
|
kick_log(KICK_LOG_DEBUG, "[cursor] host shape n/a\n");
|
|
return;
|
|
}
|
|
wl_proxy_marshal_flags((struct wl_proxy *)host.shape_dev,
|
|
1 /* WP_CURSOR_SHAPE_DEVICE_V1_SET_SHAPE */, NULL,
|
|
wl_proxy_get_version((struct wl_proxy *)host.shape_dev),
|
|
0, host.ptr_serial, (uint32_t)shape);
|
|
kick_log(KICK_LOG_DEBUG, "[cursor] host shape %s\n",
|
|
wlr_cursor_shape_v1_name((enum wp_cursor_shape_device_v1_shape)shape));
|
|
}
|
|
|
|
/* wl_seat capabilities handler: see host_seat_capabilities() below (defined
|
|
after the host keyboard/pointer listeners it installs). */
|
|
static void host_seat_capabilities(void *data, struct wl_seat *seat,
|
|
uint32_t capabilities);
|
|
static void host_seat_name(void *data, struct wl_seat *seat, const char *name) {
|
|
(void)data; (void)seat; (void)name;
|
|
}
|
|
static const struct wl_seat_listener host_seat_listener = {
|
|
.capabilities = host_seat_capabilities,
|
|
.name = host_seat_name,
|
|
};
|
|
|
|
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) {
|
|
/* Bind the seat and rely on the capabilities event (which the
|
|
compositor always sends after a bind) to (re)create the keyboard
|
|
and pointer objects -- including after a VT round-trip, where the
|
|
compositor invalidates seat objects and a re-bind or capability
|
|
re-gain must be observed to keep receiving input. */
|
|
host.seat = wl_registry_bind(registry, name,
|
|
&wl_seat_interface, version < 5 ? version : 5);
|
|
host.seat_name = name;
|
|
wl_seat_add_listener(host.seat, &host_seat_listener, NULL);
|
|
if (host.registry_synced)
|
|
bridge_init(); /* runtime seat re-add: re-establish the bridge */
|
|
} else if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) == 0) {
|
|
host.shape_mgr = wl_registry_bind(registry, name,
|
|
&wp_cursor_shape_manager_v1_interface,
|
|
version < 1 ? version : 1);
|
|
kick_log(KICK_LOG_DEBUG, "[cursor] host has cursor-shape-v1\n");
|
|
} else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) {
|
|
bridge.ddm = wl_registry_bind(registry, name,
|
|
&wl_data_device_manager_interface, version < 3 ? version : 3);
|
|
} else if (strcmp(interface,
|
|
zwlr_data_control_manager_v1_interface.name) == 0) {
|
|
bridge.dc_manager = wl_registry_bind(registry, name,
|
|
&zwlr_data_control_manager_v1_interface, version < 2 ? version : 2);
|
|
bridge.dc_primary_supported =
|
|
wl_proxy_get_version((struct wl_proxy *)bridge.dc_manager) >= 2;
|
|
if (!bridge.dc_primary_supported)
|
|
kick_log(KICK_LOG_INFO, "clipboard bridge: host wlr-data-control "
|
|
"v1 only; primary selection not bridged\n");
|
|
}
|
|
}
|
|
|
|
static void host_registry_global_remove(void *data, struct wl_registry *registry,
|
|
uint32_t name)
|
|
{
|
|
(void)data; (void)registry;
|
|
/* Only the wl_seat global's removal matters to us: its children (keyboard,
|
|
pointer, clipboard data devices) are invalidated with it and must be
|
|
re-created when the compositor advertises a new seat. Other globals we
|
|
bind (compositor, xdg_wm_base, ...) are process-lifetime. */
|
|
if (host.seat_name && name == host.seat_name) {
|
|
kick_log(KICK_LOG_DEBUG, "[seat] global %u removed\n", name);
|
|
host.seat_name = 0;
|
|
host.seat = NULL;
|
|
host.keyboard = NULL;
|
|
host.pointer = NULL;
|
|
host.shape_dev = NULL; /* inert with the removed seat's pointer */
|
|
/* Seat-owned bridge objects are invalidated too; null them so the
|
|
next bridge_init() on a fresh seat re-binds cleanly. */
|
|
bridge.dd = NULL;
|
|
bridge.dc_device = NULL;
|
|
bridge.dd_offer = NULL;
|
|
bridge.dc_offer = NULL;
|
|
bridge.dd_echo_offer = NULL;
|
|
bridge.dc_echo_offer = NULL;
|
|
bridge.dc_primary_source = NULL;
|
|
bridge.dc_primary_offer = NULL;
|
|
bridge.dc_primary_echo_offer = NULL;
|
|
bridge_primary_prefetch_cancel();
|
|
bridge.backend = BRIDGE_BACKEND_NONE;
|
|
}
|
|
}
|
|
|
|
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_resize(int width, int height) {
|
|
if (width < 50) width = 50;
|
|
if (height < 50) height = 50;
|
|
if (width == host.width && height == host.height) return;
|
|
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);
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* Tear down the nested window: drop the libdecor frame (and its decoration
|
|
subsurfaces) and the content surface. Only called from the exit paths, so
|
|
no ack/commit dance is needed. */
|
|
static void host_teardown_window(void) {
|
|
if (host.frame) {
|
|
libdecor_frame_unref(host.frame);
|
|
host.frame = NULL;
|
|
}
|
|
if (host.surface) {
|
|
wl_surface_destroy(host.surface);
|
|
host.surface = NULL;
|
|
}
|
|
}
|
|
|
|
static void host_frame_configure(struct libdecor_frame *frame,
|
|
struct libdecor_configuration *config,
|
|
void *data)
|
|
{
|
|
(void)data;
|
|
int width = 0, height = 0;
|
|
if (!libdecor_configuration_get_content_size(config, frame, &width, &height)) {
|
|
/* The initial configure commonly offers no size (the compositor
|
|
lets a CSD client pick one). The old xdg_toplevel configure
|
|
handler also treated 0x0 as "keep the current size": commit the
|
|
current layout anyway so libdecor gets a real sized configure
|
|
once we present a buffer. */
|
|
width = host.width;
|
|
height = host.height;
|
|
}
|
|
enum libdecor_window_state state = 0;
|
|
bool have_state = libdecor_configuration_get_window_state(config, &state);
|
|
if (have_state) {
|
|
bool fs = (state & LIBDECOR_WINDOW_STATE_FULLSCREEN) != 0;
|
|
if (fs != host.fullscreen) {
|
|
kick_log(KICK_LOG_INFO, "host fullscreen=%d\n", fs);
|
|
clients_set_fullscreen(fs);
|
|
}
|
|
host.fullscreen = fs;
|
|
}
|
|
/* libdecor reports the *content* size; the host window wraps that content
|
|
with its decorations. The content is exactly our wlr output. */
|
|
if (width > 0 && height > 0)
|
|
host_resize(width, height);
|
|
host.configured = true;
|
|
struct libdecor_state *st = libdecor_state_new(width, height);
|
|
libdecor_frame_commit(frame, st, config);
|
|
libdecor_state_free(st);
|
|
if (host.surface)
|
|
wl_surface_commit(host.surface);
|
|
}
|
|
|
|
static void host_frame_close(struct libdecor_frame *frame, void *data) {
|
|
(void)frame; (void)data;
|
|
kick_log(KICK_LOG_INFO, "kick: window closed\n");
|
|
host_teardown_window();
|
|
shutdown_cleanup();
|
|
exit(0);
|
|
}
|
|
|
|
static void host_frame_commit(struct libdecor_frame *frame, void *data) {
|
|
/* Decoration via (synchronous) subsurfaces: libdecor asks us to commit the
|
|
content surface so the decoration appears atomically with the content. */
|
|
(void)frame; (void)data;
|
|
if (host.surface)
|
|
wl_surface_commit(host.surface);
|
|
}
|
|
|
|
static struct libdecor_frame_interface host_frame_iface = {
|
|
.configure = host_frame_configure,
|
|
.close = host_frame_close,
|
|
.commit = host_frame_commit,
|
|
};
|
|
|
|
static void host_libdecor_error(struct libdecor *ctx, enum libdecor_error error,
|
|
const char *message)
|
|
{
|
|
(void)ctx;
|
|
fprintf(stderr, "kick: libdecor error %d: %s\n", (int)error,
|
|
message ? message : "(null)");
|
|
}
|
|
|
|
static struct libdecor_interface host_libdecor_iface = {
|
|
.error = host_libdecor_error,
|
|
};
|
|
|
|
/* ---------- host keyboard ---------- */
|
|
|
|
/* (Re)build the xkb_state used to translate raw evdev keycodes into
|
|
keysyms for host hotkey matching. The state is derived from whatever
|
|
keymap is current (host-provided or fallback), so the hotkeys work on
|
|
any keyboard/layout without hardcoded keycodes. */
|
|
static void xkb_state_reload(void)
|
|
{
|
|
if (server.xkb_state) {
|
|
xkb_state_unref(server.xkb_state);
|
|
server.xkb_state = NULL;
|
|
}
|
|
if (!server.xkb_keymap)
|
|
return;
|
|
server.xkb_state = xkb_state_new(server.xkb_keymap);
|
|
server.xkb_prefix_mod = xkb_keymap_mod_get_index(server.xkb_keymap,
|
|
tab_prefix == PREFIX_SUPER ? XKB_PREFIX_MOD4_NAME
|
|
: XKB_MOD_NAME_ALT);
|
|
kick_log(KICK_LOG_DEBUG, "kick: prefix modifier index=%u "
|
|
"(%s)\n", (unsigned)server.xkb_prefix_mod,
|
|
tab_prefix == PREFIX_SUPER ? "Super/Mod4" : "Alt");
|
|
}
|
|
|
|
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 || !server.xkb_ctx) {
|
|
close(fd);
|
|
return;
|
|
}
|
|
kick_log(KICK_LOG_DEBUG, "kick: host keymap: format=%u size=%u\n",
|
|
format, size);
|
|
|
|
/* The keymap is an shm file (size includes the trailing NUL), so mmap it
|
|
like wlroots' own clients do; fall back to a plain read loop. */
|
|
char *buf = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
|
bool mapped = buf != MAP_FAILED;
|
|
size_t total = mapped ? size : 0;
|
|
if (!mapped) {
|
|
buf = malloc(size + 1);
|
|
if (!buf) {
|
|
close(fd);
|
|
return;
|
|
}
|
|
while (total < 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);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
buf[total] = '\0';
|
|
if (total < size)
|
|
kick_log(KICK_LOG_DEBUG,
|
|
"kick: host keymap: short read %zu/%u bytes\n",
|
|
total, size);
|
|
}
|
|
|
|
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;
|
|
xkb_state_reload();
|
|
kick_log(KICK_LOG_DEBUG, "kick: host keymap parsed OK\n");
|
|
} else {
|
|
kick_log(KICK_LOG_INFO, "kick: failed to parse host "
|
|
"keymap (%u bytes); keeping fallback keymap\n", size);
|
|
if (kick_log_level() >= KICK_LOG_DEBUG) {
|
|
char hex[129];
|
|
size_t n = total < 64 ? total : 64;
|
|
for (size_t i = 0; i < n; i++)
|
|
snprintf(hex + i * 2, sizeof(hex) - i * 2, "%02x",
|
|
(unsigned char)buf[i]);
|
|
kick_log(KICK_LOG_DEBUG, "kick: keymap head (%zu/%u "
|
|
"bytes): %s\n", n, size, hex);
|
|
}
|
|
}
|
|
|
|
if (mapped)
|
|
munmap(buf, size);
|
|
else
|
|
free(buf);
|
|
close(fd);
|
|
}
|
|
|
|
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)surface; (void)keys;
|
|
/* Remember the enter serial for host clipboard set_selection. Only the
|
|
focus-based wl_data_device backend can hold a stale echo guard (a
|
|
set_selection sent with a no-longer-valid serial gets no reply): drop it
|
|
on refocus there. The data-control backend echoes independently of
|
|
focus, so its in-flight guard must be preserved here, or the host's echo
|
|
of our own set_selection is mistaken for a real selection and mirrored
|
|
back into an endless set_selection/receive loop. */
|
|
bridge.kbd_serial = serial;
|
|
if (bridge.backend == BRIDGE_BACKEND_WL_DATA_DEVICE) {
|
|
bridge.dc_pending_echo = false;
|
|
bridge.dd_pending_echo = false;
|
|
}
|
|
if (bridge.host_set_pending && server.seat->selection_source &&
|
|
server.seat->selection_source->impl != &bridge_inner_source_impl) {
|
|
bridge.host_set_pending = false;
|
|
bridge_host_set_selection(server.seat->selection_source);
|
|
}
|
|
kick_log(KICK_LOG_DEBUG, "[kbd] enter surface=%p serial=%u active=%p\n",
|
|
(void *)surface, serial, (void *)active_toplevel());
|
|
struct kick_app *app = server.active_app;
|
|
if (app_has_window(app)) {
|
|
wlr_seat_keyboard_notify_enter(server.seat, app_surface(app),
|
|
server.keyboard.keycodes, server.keyboard.num_keycodes,
|
|
&server.keyboard.modifiers);
|
|
/* A stacked sub-window may hold the keyboard within the shown tab. */
|
|
if (app->focus_sub && sub_surface(app->focus_sub))
|
|
wlr_seat_keyboard_notify_enter(server.seat,
|
|
sub_surface(app->focus_sub),
|
|
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;
|
|
kick_log(KICK_LOG_DEBUG, "[kbd] leave surface=%p serial=%u\n",
|
|
(void *)surface, serial);
|
|
wlr_seat_keyboard_notify_clear_focus(server.seat);
|
|
}
|
|
|
|
/* Temporary grey-band diagnosis: dump the scene buffer (client content as
|
|
wlroots rendered it) to /tmp/scene_N.ppm. The buffer memory is top-down,
|
|
XRGB8888. */
|
|
static void dump_scene_buffer(struct wlr_buffer *buf) {
|
|
if (!buf) return;
|
|
void *data;
|
|
uint32_t format;
|
|
size_t stride;
|
|
if (!wlr_buffer_begin_data_ptr_access(buf, WLR_BUFFER_DATA_PTR_ACCESS_READ,
|
|
&data, &format, &stride)) {
|
|
kick_log(KICK_LOG_INFO, "[dump] scene buffer not CPU-readable "
|
|
"(%dx%d)\n", buf->width, buf->height);
|
|
return;
|
|
}
|
|
/* [diag] classify the buffer: count non-black pixels so we can tell
|
|
"client painted nothing" from "we failed to import". */
|
|
int w = buf->width, h = buf->height;
|
|
unsigned long nonblack = 0, lum = 0;
|
|
unsigned char *p = data;
|
|
for (int y = 0; y < h; y++) {
|
|
unsigned char *row = p + (size_t)y * stride;
|
|
for (int x = 0; x < w; x++) {
|
|
uint32_t b = row[x*4+0];
|
|
unsigned long v = b + row[x*4+1] + row[x*4+2];
|
|
lum += v;
|
|
if (v > 12) nonblack++;
|
|
}
|
|
}
|
|
kick_log(KICK_LOG_INFO, "[dump] scene %dx%d fmt=0x%x "
|
|
"nonblack=%lu/%lu mean=%lu\n", w, h, format, nonblack,
|
|
(unsigned long)w * h, lum / ((unsigned long)w * h + 1));
|
|
char path[64];
|
|
snprintf(path, sizeof(path), "/tmp/scene_%d.ppm", dump_seq);
|
|
FILE *f = fopen(path, "wb");
|
|
if (f) {
|
|
int w = buf->width, h = buf->height;
|
|
fprintf(f, "P6\n%d %d\n255\n", w, h);
|
|
unsigned char *p = data;
|
|
for (int y = 0; y < h; y++) {
|
|
unsigned char *row = p + (size_t)y * stride;
|
|
for (int x = 0; x < w; x++) {
|
|
fputc(row[x*4+0], f);
|
|
fputc(row[x*4+1], f);
|
|
fputc(row[x*4+2], f);
|
|
}
|
|
}
|
|
fclose(f);
|
|
kick_log(KICK_LOG_INFO, "[dump] scene buffer -> %s (%dx%d, "
|
|
"fmt=0x%x)\n", path, w, h, format);
|
|
}
|
|
wlr_buffer_end_data_ptr_access(buf);
|
|
dump_seq++;
|
|
}
|
|
|
|
/* Dump what we are about to present to the host (our composited back buffer).
|
|
Must be called with the host EGL context current and back buffer freshly
|
|
drawn. GL readback is bottom-up, so flip vertically. */
|
|
static void dump_host_back(void) {
|
|
int w = host.width, h = host.height;
|
|
unsigned char *px = malloc((size_t)w * h * 4);
|
|
if (!px) return;
|
|
/* The blit draws are queued on the GPU; without a finish the readback can
|
|
race the previous frame's swap and return stale/mid-clear content. */
|
|
glFinish();
|
|
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px);
|
|
unsigned long nonblack = 0, lum = 0;
|
|
for (int y = 0; y < h * w; y++) {
|
|
unsigned long v = px[y*4+0] + px[y*4+1] + px[y*4+2];
|
|
lum += v;
|
|
if (v > 12) nonblack++;
|
|
}
|
|
kick_log(KICK_LOG_INFO, "[dump] host %dx%d nonblack=%lu/%lu mean=%lu\n",
|
|
w, h, nonblack, (unsigned long)w * h,
|
|
lum / ((unsigned long)w * h + 1));
|
|
char path[64];
|
|
snprintf(path, sizeof(path), "/tmp/host_%d.ppm", dump_seq);
|
|
FILE *f = fopen(path, "wb");
|
|
if (f) {
|
|
fprintf(f, "P6\n%d %d\n255\n", w, h);
|
|
for (int y = h - 1; y >= 0; y--) {
|
|
unsigned char *row = px + (size_t)y * w * 4;
|
|
for (int x = 0; x < w; x++) {
|
|
fputc(row[x*4+0], f);
|
|
fputc(row[x*4+1], f);
|
|
fputc(row[x*4+2], f);
|
|
}
|
|
}
|
|
fclose(f);
|
|
kick_log(KICK_LOG_INFO, "[dump] host back buffer -> %s (%dx%d)\n",
|
|
path, w, h);
|
|
}
|
|
free(px);
|
|
dump_seq++;
|
|
}
|
|
|
|
/* Dump the standalone frame buffer as it will be presented (scene + overlay
|
|
composited by the wlroots renderer). With a GLES2 renderer we glReadPixels
|
|
the buffer's FBO in the wlroots EGL context; otherwise fall back to a CPU
|
|
read (e.g. the shm-backed buffers of the pixman renderer). Uses the same
|
|
"[dump] host back buffer ->" message as the nested dump so tests can parse
|
|
either mode with one helper. */
|
|
static void dump_standalone_buffer(struct wlr_buffer *buf) {
|
|
int w = buf->width, h = buf->height;
|
|
unsigned char *px = malloc((size_t)w * h * 4);
|
|
if (!px) return;
|
|
bool flip = false;
|
|
struct wlr_egl *egl = wlr_gles2_renderer_get_egl(server.renderer);
|
|
GLuint fbo = egl ? wlr_gles2_renderer_get_buffer_fbo(server.renderer, buf)
|
|
: 0;
|
|
EGLDisplay d = egl ? wlr_egl_get_display(egl) : EGL_NO_DISPLAY;
|
|
EGLContext c = egl ? wlr_egl_get_context(egl) : EGL_NO_CONTEXT;
|
|
if (d != EGL_NO_DISPLAY && c != EGL_NO_CONTEXT && fbo) {
|
|
eglMakeCurrent(d, EGL_NO_SURFACE, EGL_NO_SURFACE, c);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
|
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
eglMakeCurrent(d, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
|
flip = true;
|
|
} else {
|
|
void *data;
|
|
uint32_t format;
|
|
size_t stride;
|
|
if (!wlr_buffer_begin_data_ptr_access(buf,
|
|
WLR_BUFFER_DATA_PTR_ACCESS_READ, &data, &format, &stride)) {
|
|
free(px);
|
|
kick_log(KICK_LOG_INFO,
|
|
"[dump] standalone buffer not CPU-readable\n");
|
|
return;
|
|
}
|
|
for (int y = 0; y < h; y++)
|
|
memcpy(px + (size_t)y * w * 4, data + (size_t)y * stride,
|
|
(size_t)w * 4);
|
|
wlr_buffer_end_data_ptr_access(buf);
|
|
}
|
|
|
|
unsigned long nonblack = 0, lum = 0;
|
|
for (int y = 0; y < h * w; y++) {
|
|
unsigned long v = px[y*4+0] + px[y*4+1] + px[y*4+2];
|
|
lum += v;
|
|
if (v > 12) nonblack++;
|
|
}
|
|
kick_log(KICK_LOG_INFO, "[dump] host %dx%d nonblack=%lu/%lu mean=%lu\n",
|
|
w, h, nonblack, (unsigned long)w * h,
|
|
lum / ((unsigned long)w * h + 1));
|
|
char path[64];
|
|
snprintf(path, sizeof(path), "/tmp/host_%d.ppm", dump_seq);
|
|
FILE *f = fopen(path, "wb");
|
|
if (f) {
|
|
fprintf(f, "P6\n%d %d\n255\n", w, h);
|
|
for (int y = 0; y < h; y++) {
|
|
int sy = flip ? h - 1 - y : y;
|
|
unsigned char *row = px + (size_t)sy * w * 4;
|
|
for (int x = 0; x < w; x++) {
|
|
fputc(row[x*4+0], f);
|
|
fputc(row[x*4+1], f);
|
|
fputc(row[x*4+2], f);
|
|
}
|
|
}
|
|
fclose(f);
|
|
kick_log(KICK_LOG_INFO, "[dump] host back buffer -> %s (%dx%d)\n",
|
|
path, w, h);
|
|
if (server.cursor)
|
|
kick_log(KICK_LOG_INFO,
|
|
"[dump] cursor at (%f,%f)\n", server.cursor->x, server.cursor->y);
|
|
}
|
|
free(px);
|
|
dump_seq++;
|
|
}
|
|
|
|
/* KICK_SYNTH_INPUT=1: ignore the real host keyboard entirely and accept
|
|
input only from the stdin event feed (the test/driver channel). The host
|
|
keyboard listener stays attached (its enter/leave events still drive the
|
|
clipboard bridge), but key and modifier events from the real host are
|
|
dropped so a stray physical press can't cancel a synthesized repeat or
|
|
clobber the injected modifier state. */
|
|
static bool synth_input_only;
|
|
|
|
/* Forward declarations for the shared handlers (defined just below
|
|
host_kbd_key / host_ptr_motion). */
|
|
static bool process_key_event(uint32_t time, uint32_t key, uint32_t state,
|
|
uint32_t mods_depressed);
|
|
static void process_modifiers(uint32_t mods_depressed, uint32_t mods_latched,
|
|
uint32_t mods_locked, uint32_t group);
|
|
static void host_ptr_motion(uint32_t time, double sx, double sy);
|
|
|
|
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;
|
|
if (synth_input_only && kbd)
|
|
return;
|
|
(void)kbd;
|
|
bridge.kbd_serial = serial;
|
|
if (!process_key_event(time, key, state, host.host_mods_depressed)) {
|
|
/* forward the key to our clients, like a native device would */
|
|
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);
|
|
}
|
|
}
|
|
|
|
/* Standalone overlay visibility. The overlay is drawn into the scene output
|
|
buffer outside the scene graph, so the scene's damage trackers never learn
|
|
about its pixels; a swapchain buffer reused after the overlay was drawn
|
|
keeps the stale overlay baked in unless we force a repaint on transition.
|
|
Bumping the sentinel rect's size routes through wlr_scene_rect_set_size ->
|
|
wlr_scene_node_update() with NULL damage, which re-damages the node's whole
|
|
populated "visible" region, so both swapchain slots repaint within the next
|
|
two frames: the overlay fully appears on press and fully clears on release. */
|
|
static void standalone_overlay_damage(void) {
|
|
if (!server.output || !server.overlay_damage_rect) return;
|
|
struct wlr_scene_rect *r = server.overlay_damage_rect;
|
|
/* The sentinel's "visible" region is only kept populated while it is the
|
|
topmost sibling (the scene's damage traversal walks children in reverse
|
|
z-order, and an opaque client surface above it would empty the shared
|
|
update region before the sentinel is visited). Raise it, so the reorder
|
|
traversal populates its full-screen visible region, then bump its size
|
|
to re-damage that whole region; restore the z-order immediately so the
|
|
always-enabled transparent rect never intercepts pointer hit-testing. */
|
|
wlr_scene_node_raise_to_top(&r->node);
|
|
wlr_scene_rect_set_size(r, r->width, r->height - 1);
|
|
wlr_scene_rect_set_size(r, r->width, r->height);
|
|
wlr_scene_node_lower_to_bottom(&r->node);
|
|
}
|
|
|
|
static void update_overlay_visible(bool alt) {
|
|
if (alt == host.overlay_visible) return;
|
|
host.overlay_visible = alt;
|
|
if (server.standalone)
|
|
standalone_overlay_damage();
|
|
kick_request_redraw();
|
|
}
|
|
|
|
/* handle_new_output() forces software cursors on every standalone output, so
|
|
the pointer is always composited into our frame by the scene renderer --
|
|
which only repaints where the frame was damaged. Cursor motion damages the
|
|
*output*, not the scene, so force whole-output damage on every move or the
|
|
pointer would never be painted. */
|
|
static void standalone_cursor_motion(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
if (server.standalone)
|
|
standalone_overlay_damage();
|
|
}
|
|
|
|
static struct wl_listener standalone_cursor_motion_listener = {
|
|
.notify = standalone_cursor_motion,
|
|
};
|
|
|
|
/* Is the configured tab-management prefix (Alt, or Super/Mod4 with
|
|
--modifier super) currently pressed? Uses the xkb modifier index when a
|
|
keymap is compiled; with no keymap (xkb init failed) it falls back to the
|
|
raw wl_keyboard modifier bit: mod1/alt = 1<<3, mod4/super = 1<<6. */
|
|
static bool prefix_active(uint32_t mods_depressed) {
|
|
if (server.xkb_state)
|
|
return xkb_state_mod_index_is_active(server.xkb_state,
|
|
server.xkb_prefix_mod, XKB_STATE_MODS_EFFECTIVE);
|
|
return (mods_depressed & (tab_prefix == PREFIX_SUPER ? (1u << 6)
|
|
: (1u << 3))) != 0;
|
|
}
|
|
|
|
/* Shared key handling for both the host keyboard and native input devices:
|
|
tab-switch bindings + overlay visibility, with the caller forwarding the
|
|
key to the seat afterwards. `mods_depressed` uses the wl_keyboard modifier
|
|
layout (shift=1<<0, ctrl=1<<2, alt=1<<3). Returns true when the key was
|
|
consumed by a binding and must not be forwarded to the seat. */
|
|
static bool process_key_event(uint32_t time, uint32_t key, uint32_t state,
|
|
uint32_t mods_depressed)
|
|
{
|
|
(void)time;
|
|
if (server.xkb_state)
|
|
xkb_state_update_key(server.xkb_state, key + 8,
|
|
state == WL_KEYBOARD_KEY_STATE_PRESSED
|
|
? XKB_KEY_DOWN : XKB_KEY_UP);
|
|
xkb_keysym_t sym = server.xkb_state
|
|
? xkb_state_key_get_one_sym(server.xkb_state, key + 8)
|
|
: XKB_KEY_NoSymbol;
|
|
/* With no compiled keymap (xkb init failed) fall back to the raw
|
|
evdev keycodes / modifier bits for the hotkey matching below. */
|
|
bool alt = prefix_active(mods_depressed);
|
|
if (alt != host.overlay_visible)
|
|
update_overlay_visible(alt);
|
|
char symbuf[32];
|
|
xkb_keysym_get_name(sym, symbuf, sizeof(symbuf));
|
|
kick_log(KICK_LOG_DEBUG,
|
|
"[kbd] key=%u state=%u sym=%s ctrl=%d alt=%d repeat_key=%u\n",
|
|
key, state, sym ? symbuf : "-",
|
|
(mods_depressed & (1u << 2)) != 0, alt, host.repeat_key);
|
|
/* Ctrl+Alt+F1..F12 switch virtual terminals in standalone mode. wlroots
|
|
has no built-in handler; the compositor must notice the key and ask the
|
|
session (libseat/logind) to activate another VT. Both the press and
|
|
release are consumed so the focused client never sees the chord. */
|
|
if (server.standalone && server.session &&
|
|
sym >= XKB_KEY_XF86Switch_VT_1 &&
|
|
sym <= XKB_KEY_XF86Switch_VT_12) {
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
unsigned vt = sym - XKB_KEY_XF86Switch_VT_1 + 1;
|
|
kick_log(KICK_LOG_INFO,
|
|
"kick: vt switch -> %u\n", vt);
|
|
wlr_session_change_vt(server.session, vt);
|
|
}
|
|
return true;
|
|
}
|
|
/* Function keys (F9/F10/F11) are not grabbed: they pass through to the
|
|
focused client, which may have its own bindings (e.g. a browser's F11
|
|
fullscreen request, which handle_request_fullscreen mirrors onto the
|
|
host window). Frame dumps are available via SIGUSR1 instead. */
|
|
/* Alt+Up / Alt+Down cycle tabs. Grabbed so the focused client never
|
|
sees them. The host sends a single physical press, so repeats are
|
|
synthesized by kbd_repeat_start(); subsequent host presses of an
|
|
already-held key are ignored so a host-sent repeat never cycles twice.
|
|
Plain arrow keys (not Fn+arrow) are used because many laptop firmware
|
|
swallow Alt+Fn+PageUp (the Fn key reports as XF86WakeUp there), which
|
|
makes Alt+PageUp/PageDown unreliable. */
|
|
bool prev, next;
|
|
if (server.xkb_state) {
|
|
prev = sym == XKB_KEY_Up;
|
|
next = sym == XKB_KEY_Down;
|
|
} else {
|
|
prev = key == 103;
|
|
next = key == 108;
|
|
}
|
|
/* Alt+Shift+Up / Alt+Shift+Down move the active tab within the overlay
|
|
list (wrapping at the ends), without switching the active surface.
|
|
Checked before the plain Alt+arrow cycle below so Shift always means
|
|
"reorder, don't switch"; hold to keep moving via kbd_repeat_start.
|
|
Grabbed so the focused client never sees the chord (a terminal would
|
|
otherwise act on Shift+arrow). */
|
|
bool shift = (mods_depressed & (1u << 0)) != 0;
|
|
if (alt && shift && (prev || next)) {
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
if (host.repeat_key != key) {
|
|
int dir = next ? 1 : -1;
|
|
reorder_active_tab(dir);
|
|
kbd_repeat_start(key, dir, REPEAT_REORDER);
|
|
}
|
|
} else {
|
|
kbd_repeat_cancel();
|
|
}
|
|
return true;
|
|
}
|
|
if (alt && (prev || next)) {
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
if (host.repeat_key != key) {
|
|
int dir = next ? 1 : -1;
|
|
cycle_tab(dir);
|
|
kbd_repeat_start(key, dir, REPEAT_CYCLE);
|
|
}
|
|
} else {
|
|
kbd_repeat_cancel();
|
|
}
|
|
return true;
|
|
}
|
|
/* Alt+` rotates the active tab's stacked windows backward: from the
|
|
shown window to the one added just before it, wrapping from the
|
|
oldest through the main surface back to the newest. Consumed only
|
|
when the tab actually has stacked windows, so a client that binds
|
|
Alt+` itself still sees the key elsewhere. */
|
|
{ bool is_grave = server.xkb_state
|
|
? (sym == XKB_KEY_grave)
|
|
: key == 41;
|
|
if (alt && is_grave && server.active_app &&
|
|
!wl_list_empty(&server.active_app->sub_windows)) {
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
if (host.repeat_key != key) {
|
|
cycle_stack(server.active_app, -1);
|
|
kbd_repeat_start(key, -1, REPEAT_SUBCYCLE);
|
|
}
|
|
} else {
|
|
kbd_repeat_cancel();
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
/* prefix+Delete closes the active tab. Grabbed so the focused client never
|
|
sees the key; a latch drops host-synthesized repeats so one hold can't
|
|
close a tab per repeat. The active surface already moved to the right
|
|
neighbor inside close_active_tab(), so the tab is gone this frame even
|
|
before the client process exits. */
|
|
{ bool is_delete = server.xkb_state
|
|
? (sym == XKB_KEY_Delete)
|
|
: key == 111;
|
|
if (alt && is_delete) {
|
|
kbd_repeat_cancel();
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
if (!delete_key_held) {
|
|
delete_key_held = true;
|
|
close_tab(server.active_app);
|
|
}
|
|
} else {
|
|
delete_key_held = false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
/* Alt+Shift+T opens a new terminal tab ($TERMINAL, default alacritty).
|
|
The tab is inserted right after the current one and becomes active when
|
|
it maps. Grabbed so the focused client never sees the chord; a latch
|
|
drops host-synthesized repeats so one hold can't open a terminal per
|
|
repeat. */
|
|
{
|
|
bool is_t = server.xkb_state
|
|
? (sym == XKB_KEY_t || sym == XKB_KEY_T)
|
|
: key == 20;
|
|
if (alt && shift && is_t) {
|
|
kbd_repeat_cancel();
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
if (!term_key_held) {
|
|
term_key_held = true;
|
|
if (!cmd_in_path(term_cmd)) {
|
|
kick_log(KICK_LOG_INFO,
|
|
"kick: '%s' not found in PATH; "
|
|
"no terminal opened (set $TERMINAL)\n",
|
|
term_cmd);
|
|
} else {
|
|
add_tab(term_cmd);
|
|
}
|
|
}
|
|
} else {
|
|
term_key_held = false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
/* any other key activity stops the tab-switch repeat */
|
|
kbd_repeat_cancel();
|
|
return false;
|
|
}
|
|
|
|
/* Shared modifier handling for the host keyboard and native devices. */
|
|
static void process_modifiers(uint32_t mods_depressed, uint32_t mods_latched,
|
|
uint32_t mods_locked, uint32_t group)
|
|
{
|
|
host.host_mods_depressed = mods_depressed;
|
|
if (server.xkb_state)
|
|
xkb_state_update_mask(server.xkb_state, mods_depressed,
|
|
mods_latched, mods_locked, group, group, group);
|
|
bool alt = prefix_active(mods_depressed);
|
|
if (alt != host.overlay_visible)
|
|
update_overlay_visible(alt);
|
|
if (host.repeat_key && !alt)
|
|
kbd_repeat_cancel();
|
|
}
|
|
|
|
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)serial;
|
|
if (synth_input_only && kbd)
|
|
return;
|
|
(void)kbd;
|
|
process_modifiers(mods_depressed, mods_latched, mods_locked, group);
|
|
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,
|
|
};
|
|
|
|
/* ============ Standalone native input devices ============
|
|
In standalone mode real keyboard/pointer devices come from the backend
|
|
(libinput). Their events are routed through the same hotkey logic and are
|
|
then forwarded into the shared seat keyboard exactly like host input.
|
|
|
|
The keyboard listeners are per-device (one struct standalone_kbdev per
|
|
libinput keyboard): ending a single global slot makes modifier state come
|
|
from whichever device attached last (often an HDMI-CEC or consumer-control
|
|
node), so Shift held on the real keyboard never reached the seat. */
|
|
|
|
static void standalone_kbd_key(struct wl_listener *listener, void *data) {
|
|
struct standalone_kbdev *kd = wl_container_of(listener, kd, key);
|
|
struct wlr_keyboard_key_event *ev = data;
|
|
uint32_t mods = wlr_keyboard_get_modifiers(kd->keyboard);
|
|
if (!process_key_event(ev->time_msec, ev->keycode, ev->state, mods)) {
|
|
wlr_seat_set_keyboard(server.seat, kd->keyboard);
|
|
wlr_seat_keyboard_notify_key(server.seat, ev->time_msec, ev->keycode,
|
|
ev->state);
|
|
}
|
|
}
|
|
|
|
static void standalone_kbd_modifiers(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct standalone_kbdev *kd = wl_container_of(listener, kd, modifiers);
|
|
struct wlr_keyboard *kb = kd->keyboard;
|
|
process_modifiers(kb->modifiers.depressed, kb->modifiers.latched,
|
|
kb->modifiers.locked, kb->modifiers.group);
|
|
wlr_seat_set_keyboard(server.seat, kb);
|
|
wlr_seat_keyboard_notify_modifiers(server.seat, &kb->modifiers);
|
|
}
|
|
|
|
/* The device is going away (libinput suspends all devices when the session
|
|
loses its VT). Drop our listeners so wlr_keyboard_finish's assert that
|
|
every event list is empty holds, then free the struct. wlroots emits the
|
|
input device's destroy signal from within wlr_keyboard_finish(), so this
|
|
runs before the assert. */
|
|
static void standalone_kbd_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct standalone_kbdev *kd = wl_container_of(listener, kd, destroy);
|
|
wl_list_remove(&kd->destroy.link);
|
|
wl_list_remove(&kd->key.link);
|
|
wl_list_remove(&kd->modifiers.link);
|
|
wl_list_remove(&kd->link);
|
|
free(kd);
|
|
}
|
|
|
|
/* Forward the wlr_cursor's position to the seat as the effect/click point.
|
|
wlroots is the single source of truth for where the pointer is: it clamps
|
|
cursor->x/y against the output layout (wlr_cursor_warp_closest), so the
|
|
visible arrow and the coordinates forwarded here stay in the same space,
|
|
even when a requested --screen-mode mode differs from the geometry the
|
|
output layout derives from the committed state. Never hand-clamp a second
|
|
copy. */
|
|
static void standalone_forward_pointer(uint32_t time) {
|
|
struct wlr_output *output = server.output;
|
|
if (!server.cursor || !output) return;
|
|
double x = server.cursor->x;
|
|
double y = server.cursor->y;
|
|
wlr_output_layout_output_coords(server.output_layout, output, &x, &y);
|
|
host_ptr_motion(time, x, y);
|
|
}
|
|
|
|
/* Shared standalone pointer-motion handling, used by both the libinput device
|
|
listeners and the stdin ctl feed (which has no device, hence NULL). The
|
|
wlr_cursor never moves on its own in wlroots 0.19 -- the device wiring only
|
|
re-emits motion on cursor->events.motion -- so every relative delta must be
|
|
turned into wlr_cursor_move() or the visible pointer stays pinned while the
|
|
seat (and therefore client hover) keeps moving. With a NULL device the move
|
|
falls back to the output layout's closest-point mapping. */
|
|
/* ---------- pointer constraints + relative pointer ---------- */
|
|
|
|
/* Forward a relative pointer delta to clients (zwp_relative_pointer_v1).
|
|
The manager scopes delivery to clients that bound it on the seat. */
|
|
static void send_relative_motion(uint32_t time_msec, double dx, double dy) {
|
|
if (!server.relative_pointer_mgr || !server.seat) return;
|
|
kick_log(KICK_LOG_INFO, "[relative] motion dx=%d dy=%d\n",
|
|
(int)dx, (int)dy);
|
|
wlr_relative_pointer_manager_v1_send_relative_motion(
|
|
server.relative_pointer_mgr, server.seat,
|
|
(uint64_t)time_msec * 1000, dx, dy, dx, dy);
|
|
}
|
|
|
|
/* Find the scene origin (in scene/layout coordinates) of a surface's buffer
|
|
node, so a constraint region in surface-local space can be mapped to the
|
|
output. Surfaces live under per-app scene trees, so the walk is
|
|
recursive. */
|
|
static bool scene_surface_origin_in(struct wlr_scene_tree *tree,
|
|
struct wlr_surface *surface,
|
|
double *ox, double *oy) {
|
|
struct wlr_scene_node *node;
|
|
wl_list_for_each(node, &tree->children, link) {
|
|
if (node->type == WLR_SCENE_NODE_BUFFER) {
|
|
struct wlr_scene_buffer *sb = wlr_scene_buffer_from_node(node);
|
|
struct wlr_scene_surface *ss = wlr_scene_surface_try_from_buffer(sb);
|
|
if (ss && ss->surface == surface) {
|
|
int lx = 0, ly = 0;
|
|
if (!wlr_scene_node_coords(node, &lx, &ly)) return false;
|
|
*ox = (double)lx;
|
|
*oy = (double)ly;
|
|
return true;
|
|
}
|
|
} else if (node->type == WLR_SCENE_NODE_TREE) {
|
|
if (scene_surface_origin_in(wlr_scene_tree_from_node(node),
|
|
surface, ox, oy))
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool scene_surface_origin(struct wlr_surface *surface,
|
|
double *ox, double *oy) {
|
|
return scene_surface_origin_in(&server.scene->tree, surface, ox, oy);
|
|
}
|
|
|
|
static void constraint_deactivate(void) {
|
|
if (!server.active_constraint) return;
|
|
wlr_pointer_constraint_v1_send_deactivated(server.active_constraint);
|
|
wl_list_remove(&server.constraint_destroy.link);
|
|
server.active_constraint = NULL;
|
|
}
|
|
|
|
static void handle_constraint_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct kick_server *s = wl_container_of(listener, s, constraint_destroy);
|
|
s->active_constraint = NULL;
|
|
}
|
|
|
|
static void constraint_activate(struct wlr_surface *surface) {
|
|
if (!server.pointer_constraints || !surface) return;
|
|
struct wlr_pointer_constraint_v1 *c = wlr_pointer_constraints_v1_constraint_for_surface(
|
|
server.pointer_constraints, surface, server.seat);
|
|
if (!c) return;
|
|
server.active_constraint = c;
|
|
if (!scene_surface_origin(surface, &server.constraint_ox, &server.constraint_oy)) {
|
|
server.constraint_ox = 0.0;
|
|
server.constraint_oy = 0.0;
|
|
}
|
|
server.lock_x = server.last_ptr_x;
|
|
server.lock_y = server.last_ptr_y;
|
|
server.constraint_destroy.notify = handle_constraint_destroy;
|
|
wl_signal_add(&c->events.destroy, &server.constraint_destroy);
|
|
wlr_pointer_constraint_v1_send_activated(c);
|
|
if (c->type == WLR_POINTER_CONSTRAINT_V1_LOCKED)
|
|
kick_log(KICK_LOG_INFO, "[constraint] locked on surface\n");
|
|
else
|
|
kick_log(KICK_LOG_INFO, "[constraint] confined on surface\n");
|
|
}
|
|
|
|
/* Focus change is the only place the pointer's surface can change, so it is
|
|
the natural point to swap the active constraint. */
|
|
static void handle_seat_pointer_focus(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_seat_pointer_focus_change_event *ev = data;
|
|
constraint_deactivate();
|
|
if (ev->new_surface)
|
|
constraint_activate(ev->new_surface);
|
|
}
|
|
|
|
/* Clamp an absolute pointer position to whatever the active constraint
|
|
allows, in place. LOCKED pins to the lock position; CONFINED keeps the
|
|
pointer inside the constraint region (empty region = whole surface). */
|
|
static void constraint_filter_abs(double *x, double *y) {
|
|
struct wlr_pointer_constraint_v1 *c = server.active_constraint;
|
|
if (!c) return;
|
|
struct wlr_surface *surface = c->surface;
|
|
if (c->type == WLR_POINTER_CONSTRAINT_V1_LOCKED) {
|
|
*x = server.lock_x;
|
|
*y = server.lock_y;
|
|
return;
|
|
}
|
|
int w = surface->current.width;
|
|
int h = surface->current.height;
|
|
if (w <= 0 || h <= 0) return;
|
|
int x1 = 0, y1 = 0, x2 = w, y2 = h;
|
|
if (pixman_region32_not_empty(&c->region)) {
|
|
pixman_box32_t *ext = pixman_region32_extents(&c->region);
|
|
x1 = ext->x1;
|
|
y1 = ext->y1;
|
|
x2 = ext->x2;
|
|
y2 = ext->y2;
|
|
if (x2 > w) x2 = w;
|
|
if (y2 > h) y2 = h;
|
|
}
|
|
if (x1 >= x2) x1 = x2 - 1;
|
|
if (y1 >= y2) y1 = y2 - 1;
|
|
double lx = *x - server.constraint_ox;
|
|
double ly = *y - server.constraint_oy;
|
|
if (lx < x1) lx = x1;
|
|
else if (lx >= x2) lx = x2 - 1;
|
|
if (ly < y1) ly = y1;
|
|
else if (ly >= y2) ly = y2 - 1;
|
|
*x = server.constraint_ox + lx;
|
|
*y = server.constraint_oy + ly;
|
|
}
|
|
|
|
static void native_ptr_motion(uint32_t time, struct wlr_input_device *dev,
|
|
double dx, double dy) {
|
|
if (!server.cursor) return;
|
|
send_relative_motion(time, dx, dy);
|
|
wlr_cursor_move(server.cursor, dev, dx, dy);
|
|
/* Keep the visible arrow inside the active constraint's region (or
|
|
pinned, when locked). wlr_cursor is the source of truth for the
|
|
position, so clamp it rather than forwarding a second copy. */
|
|
if (server.active_constraint) {
|
|
double x = server.cursor->x, y = server.cursor->y;
|
|
constraint_filter_abs(&x, &y);
|
|
/* Re-warp through wlroots so the output cursor (the rendered arrow)
|
|
follows: writing cursor->x/y directly would leave it behind. */
|
|
if (x != server.cursor->x || y != server.cursor->y)
|
|
wlr_cursor_warp(server.cursor, NULL, x, y);
|
|
}
|
|
standalone_forward_pointer(time);
|
|
}
|
|
|
|
static void native_ptr_abs(uint32_t time, struct wlr_input_device *dev,
|
|
double x, double y) {
|
|
if (!server.cursor) return;
|
|
wlr_cursor_warp_absolute(server.cursor, dev, x, y);
|
|
standalone_forward_pointer(time);
|
|
}
|
|
|
|
static void standalone_ptr_motion_event(struct wl_listener *listener, void *data) {
|
|
struct standalone_ptrdev *pd = wl_container_of(listener, pd, motion);
|
|
struct wlr_pointer_motion_event *ev = data;
|
|
native_ptr_motion(ev->time_msec,
|
|
(struct wlr_input_device *)&pd->pointer->base,
|
|
ev->delta_x, ev->delta_y);
|
|
}
|
|
|
|
static void standalone_ptr_abs_event(struct wl_listener *listener, void *data) {
|
|
struct standalone_ptrdev *pd = wl_container_of(listener, pd, motion_absolute);
|
|
struct wlr_pointer_motion_absolute_event *ev = data;
|
|
native_ptr_abs(ev->time_msec,
|
|
(struct wlr_input_device *)&pd->pointer->base,
|
|
ev->x, ev->y);
|
|
}
|
|
|
|
static void standalone_ptr_button_event(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_pointer_button_event *ev = data;
|
|
if (!server.seat) return;
|
|
wlr_seat_pointer_notify_button(server.seat, ev->time_msec, ev->button,
|
|
ev->state);
|
|
wlr_seat_pointer_notify_frame(server.seat);
|
|
}
|
|
|
|
static void standalone_ptr_axis_event(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_pointer_axis_event *ev = data;
|
|
if (!server.seat) return;
|
|
wlr_seat_pointer_notify_axis(server.seat, ev->time_msec, ev->orientation,
|
|
ev->delta, ev->delta_discrete, ev->source,
|
|
WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL);
|
|
wlr_seat_pointer_notify_frame(server.seat);
|
|
}
|
|
|
|
static void standalone_ptr_frame_event(struct wl_listener *listener, void *data) {
|
|
(void)listener; (void)data;
|
|
}
|
|
|
|
/* The device is going away (libinput suspends all devices when the session
|
|
loses its VT). Drop our listeners so wlr_pointer_finish's assert that
|
|
every event list is empty holds, then free the struct. */
|
|
static void standalone_ptr_destroy(struct wl_listener *listener, void *data) {
|
|
(void)data;
|
|
struct standalone_ptrdev *pd = wl_container_of(listener, pd, destroy);
|
|
wl_list_remove(&pd->destroy.link);
|
|
wl_list_remove(&pd->motion.link);
|
|
wl_list_remove(&pd->motion_absolute.link);
|
|
wl_list_remove(&pd->button.link);
|
|
wl_list_remove(&pd->axis.link);
|
|
wl_list_remove(&pd->frame.link);
|
|
wl_list_remove(&pd->link);
|
|
free(pd);
|
|
}
|
|
|
|
static void handle_new_input(struct wl_listener *listener, void *data) {
|
|
(void)listener;
|
|
struct wlr_input_device *device = data;
|
|
|
|
switch (device->type) {
|
|
case WLR_INPUT_DEVICE_KEYBOARD: {
|
|
struct wlr_keyboard *kb = wlr_keyboard_from_input_device(device);
|
|
/* Without a keymap the device never emits modifier events; give it
|
|
ours so both the hotkeys and the seat work. */
|
|
if (server.xkb_keymap)
|
|
wlr_keyboard_set_keymap(kb, server.xkb_keymap);
|
|
struct standalone_kbdev *kd = calloc(1, sizeof(*kd));
|
|
if (!kd) break;
|
|
kd->keyboard = kb;
|
|
kd->key.notify = standalone_kbd_key;
|
|
wl_signal_add(&kb->events.key, &kd->key);
|
|
kd->modifiers.notify = standalone_kbd_modifiers;
|
|
wl_signal_add(&kb->events.modifiers, &kd->modifiers);
|
|
kd->destroy.notify = standalone_kbd_destroy;
|
|
wl_signal_add(&kb->base.events.destroy, &kd->destroy);
|
|
wl_list_insert(server.kbdevs.prev, &kd->link);
|
|
kick_log(KICK_LOG_INFO, "standalone: keyboard '%s'\n", device->name);
|
|
break;
|
|
}
|
|
case WLR_INPUT_DEVICE_POINTER: {
|
|
struct wlr_pointer *ptr = wlr_pointer_from_input_device(device);
|
|
if (server.cursor)
|
|
wlr_cursor_attach_input_device(server.cursor, device);
|
|
struct standalone_ptrdev *pd = calloc(1, sizeof(*pd));
|
|
if (!pd) break;
|
|
pd->pointer = ptr;
|
|
pd->motion.notify = standalone_ptr_motion_event;
|
|
wl_signal_add(&ptr->events.motion, &pd->motion);
|
|
pd->motion_absolute.notify = standalone_ptr_abs_event;
|
|
wl_signal_add(&ptr->events.motion_absolute, &pd->motion_absolute);
|
|
pd->button.notify = standalone_ptr_button_event;
|
|
wl_signal_add(&ptr->events.button, &pd->button);
|
|
pd->axis.notify = standalone_ptr_axis_event;
|
|
wl_signal_add(&ptr->events.axis, &pd->axis);
|
|
pd->frame.notify = standalone_ptr_frame_event;
|
|
wl_signal_add(&ptr->events.frame, &pd->frame);
|
|
pd->destroy.notify = standalone_ptr_destroy;
|
|
wl_signal_add(&ptr->base.events.destroy, &pd->destroy);
|
|
wl_list_insert(server.ptrdevs.prev, &pd->link);
|
|
kick_log(KICK_LOG_INFO, "standalone: pointer '%s'\n", device->name);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ============ stdin event feed (test/driver channel) ============
|
|
Reads raw keyboard events from stdin and dispatches them into the
|
|
same host_kbd_key / host_kbd_modifiers handlers a real host would
|
|
deliver, so the key handler itself stays under test. Two records:
|
|
|
|
1 | u32 key | u32 state (WL_KEYBOARD_KEY_STATE_*)
|
|
2 | u32 mods | u32 latched | u32 locked | u32 group
|
|
*/
|
|
#define CTL_CMD_KEY 1
|
|
#define CTL_CMD_MODS 2
|
|
#define CTL_CMD_WARP 3
|
|
#define CTL_CMD_PTR_MOTION 4
|
|
|
|
#define CTL_MAX_MSG 20 /* 1 + 4 + 4 (or 1 + 16) after padding */
|
|
#define CTL_BUF_SIZE 512
|
|
|
|
static uint32_t ctl_serial;
|
|
static uint32_t ctl_time;
|
|
|
|
static void ctl_dispatch(const uint8_t *msg, size_t len) {
|
|
uint32_t u32[4];
|
|
if (len < 5) return;
|
|
switch (msg[0]) {
|
|
case CTL_CMD_KEY:
|
|
if (len < 9) return;
|
|
memcpy(u32, msg + 1, 8);
|
|
ctl_time += 1;
|
|
host_kbd_key(NULL, NULL, ++ctl_serial, ctl_time, u32[0], u32[1]);
|
|
break;
|
|
case CTL_CMD_MODS:
|
|
if (len < 17) return;
|
|
memcpy(u32, msg + 1, 16);
|
|
ctl_time += 1;
|
|
host_kbd_modifiers(NULL, NULL, ++ctl_serial, u32[0], u32[1],
|
|
u32[2], u32[3]);
|
|
break;
|
|
case CTL_CMD_WARP:
|
|
if (len < 9) return;
|
|
memcpy(u32, msg + 1, 8);
|
|
ctl_time += 1;
|
|
/* Absolute standalone-output-pixel position, so tests can place the
|
|
visible cursor (wlr_cursor_warp with a NULL device). Warp doesn't
|
|
emit a motion event, so damage the scene here like motion would, and
|
|
forward the new position to the seat so the effect point stays where
|
|
the arrow lands. Nested mode has no wlr_cursor: feed the position
|
|
straight into the seat instead, so the ctl feed stays usable in both
|
|
modes (e.g. to give a client a pointer enter so it can request a
|
|
cursor shape). */
|
|
if (server.standalone) {
|
|
double wx = (double)u32[0], wy = (double)u32[1];
|
|
constraint_filter_abs(&wx, &wy);
|
|
wlr_cursor_warp(server.cursor, NULL, wx, wy);
|
|
standalone_forward_pointer(ctl_time);
|
|
standalone_overlay_damage();
|
|
} else {
|
|
host_ptr_motion(ctl_time, (double)u32[0], (double)u32[1]);
|
|
}
|
|
break;
|
|
case CTL_CMD_PTR_MOTION:
|
|
if (len < 9) return;
|
|
memcpy(u32, msg + 1, 8);
|
|
ctl_time += 1;
|
|
/* Relative delta through the same path a libinput pointer device
|
|
takes (native_ptr_motion -> wlr_cursor_move). With no device there
|
|
is no cursor->events.motion, so damage the scene explicitly. */
|
|
native_ptr_motion(ctl_time, NULL, (double)u32[0], (double)u32[1]);
|
|
if (server.standalone)
|
|
standalone_overlay_damage();
|
|
break;
|
|
}
|
|
}
|
|
|
|
static int ctl_cb(int fd, uint32_t mask, void *data) {
|
|
static uint8_t buf[CTL_BUF_SIZE];
|
|
static size_t have = 0;
|
|
(void)data;
|
|
|
|
/* stdin closed: stop watching but keep the compositor running */
|
|
if (mask & (WL_EVENT_HANGUP | WL_EVENT_ERROR)) {
|
|
wl_event_source_remove(server.ctl_source);
|
|
server.ctl_source = NULL;
|
|
return 0;
|
|
}
|
|
|
|
ssize_t n = read(fd, buf + have, sizeof(buf) - have);
|
|
if (n <= 0) {
|
|
if (n == 0) {
|
|
wl_event_source_remove(server.ctl_source);
|
|
server.ctl_source = NULL;
|
|
}
|
|
return 0;
|
|
}
|
|
have += (size_t)n;
|
|
|
|
uint8_t msg[CTL_MAX_MSG];
|
|
while (have >= 5) {
|
|
size_t need = (buf[0] == CTL_CMD_MODS) ? 17 : 9;
|
|
if (have < need) break;
|
|
memcpy(msg, buf, need);
|
|
ctl_dispatch(msg, need);
|
|
memmove(buf, buf + need, have - need);
|
|
have -= need;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ---------- 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;
|
|
server.last_ptr_x = sx;
|
|
server.last_ptr_y = sy;
|
|
constraint_filter_abs(&sx, &sy);
|
|
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);
|
|
/* Pointer over no nested surface (black tab): drop any client shape. */
|
|
apply_cursor_shape(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT);
|
|
}
|
|
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)surface;
|
|
host.ptr_serial = serial;
|
|
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;
|
|
double nx = wl_fixed_to_double(sx), ny = wl_fixed_to_double(sy);
|
|
/* Nested mode has no wlr_cursor: derive the relative delta from the
|
|
consecutive absolute host positions. (Standalone deltas are sent from
|
|
native_ptr_motion instead.) */
|
|
send_relative_motion(time, nx - server.last_ptr_x, ny - server.last_ptr_y);
|
|
host_ptr_motion(time, nx, ny);
|
|
}
|
|
|
|
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;
|
|
int32_t discrete = axis < 2 ? host.axis_discrete[axis] : 0;
|
|
host.axis_discrete[axis] = 0;
|
|
wlr_seat_pointer_notify_axis(server.seat, time, axis,
|
|
wl_fixed_to_double(value), discrete,
|
|
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;
|
|
if (axis < 2)
|
|
host.axis_discrete[axis] += 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,
|
|
};
|
|
|
|
/* When the host compositor (labwc on a VT round-trip) removes a seat
|
|
capability, every object created from that seat -- our wl_keyboard /
|
|
wl_pointer -- is invalidated, yet the client keeps running silently with
|
|
no input ever arriving again. Re-create the objects as capabilities are
|
|
re-gained; the compositor re-sends keymap/modifiers/enter on the fresh
|
|
wl_keyboard, and host_kbd_enter() re-focuses our nested seat. */
|
|
static void host_seat_capabilities(void *data, struct wl_seat *seat,
|
|
uint32_t capabilities)
|
|
{
|
|
(void)data; (void)seat;
|
|
kick_log(KICK_LOG_DEBUG, "[seat] capabilities=0x%x\n", capabilities);
|
|
const bool have_kbd = (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) != 0;
|
|
const bool have_ptr = (capabilities & WL_SEAT_CAPABILITY_POINTER) != 0;
|
|
if (have_kbd && !host.keyboard) {
|
|
host.keyboard = wl_seat_get_keyboard(host.seat);
|
|
if (host.keyboard) {
|
|
kick_log(KICK_LOG_DEBUG, "[seat] re-gained keyboard\n");
|
|
wl_keyboard_add_listener(host.keyboard, &host_kbd_listener, NULL);
|
|
}
|
|
} else if (!have_kbd && host.keyboard) {
|
|
wl_keyboard_release(host.keyboard);
|
|
host.keyboard = NULL;
|
|
kick_log(KICK_LOG_DEBUG, "[seat] keyboard capability lost\n");
|
|
}
|
|
if (have_ptr && !host.pointer) {
|
|
host.pointer = wl_seat_get_pointer(host.seat);
|
|
if (host.pointer) {
|
|
kick_log(KICK_LOG_DEBUG, "[seat] re-gained pointer\n");
|
|
wl_pointer_add_listener(host.pointer, &host_ptr_listener, NULL);
|
|
}
|
|
} else if (!have_ptr && host.pointer) {
|
|
host.shape_dev = NULL; /* the shape device goes inert too */
|
|
wl_pointer_release(host.pointer);
|
|
host.pointer = NULL;
|
|
kick_log(KICK_LOG_DEBUG, "[seat] pointer capability lost\n");
|
|
}
|
|
/* The cursor-shape device rides on the host pointer: (re)create it once
|
|
the pointer is (re)available. */
|
|
if (host.pointer)
|
|
host_cursor_shape_get_pointer();
|
|
}
|
|
|
|
/* ===================== 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, "kick: 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);
|
|
host.registry_synced = true;
|
|
|
|
if (!host.compositor || !host.wm_base) {
|
|
fprintf(stderr, "kick: host lacks compositor/xdg_wm_base\n");
|
|
return false;
|
|
}
|
|
|
|
host.surface = wl_compositor_create_surface(host.compositor);
|
|
host.libdecor = libdecor_new(host.display, &host_libdecor_iface);
|
|
if (!host.libdecor) {
|
|
fprintf(stderr, "kick: libdecor failed to initialise\n");
|
|
return false;
|
|
}
|
|
host.frame = libdecor_decorate(host.libdecor, host.surface,
|
|
&host_frame_iface, NULL);
|
|
if (!host.frame) {
|
|
fprintf(stderr, "kick: libdecor_decorate failed\n");
|
|
return false;
|
|
}
|
|
libdecor_frame_set_title(host.frame, "kick");
|
|
libdecor_frame_set_app_id(host.frame, "kick");
|
|
/* Ask libdecor for a frame around our content. It negotiates xdg
|
|
decorations with the host: a server-side title bar when the host offers
|
|
one (labwc, sway), its own client-side title bar (drawing in a
|
|
subsurface above our content) otherwise -- GNOME/Mutter forces CSD, so
|
|
the window becomes draggable and resizable through that bar. */
|
|
libdecor_frame_map(host.frame);
|
|
/* libdecor pongs pings on its own wm_base binding; ours is separate, so
|
|
keep ponging the ones addressed to it or the host declares us hung. */
|
|
xdg_wm_base_add_listener(host.wm_base, &host_xdg_wm_listener, NULL);
|
|
wl_surface_commit(host.surface);
|
|
wl_display_roundtrip(host.display);
|
|
libdecor_dispatch(host.libdecor, 0);
|
|
|
|
/* Host keyboard/pointer are created by the seat capabilities handler
|
|
during the registry round-trip above; nothing more to fetch here. */
|
|
bridge_init();
|
|
|
|
/* Try EGL on the host wayland platform */
|
|
pfn_eglGetPlatformDisplayEXT = (PFNEGLGETPLATFORMDISPLAYEXTPROC)
|
|
eglGetProcAddress("eglGetPlatformDisplayEXT");
|
|
if (!pfn_eglGetPlatformDisplayEXT) {
|
|
fprintf(stderr, "kick: 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, "kick: EGL display failed; "
|
|
"falling back to software rendering\n");
|
|
host.egl_ok = false;
|
|
return true;
|
|
}
|
|
if (!eglInitialize(host.egl_display, NULL, NULL)) {
|
|
fprintf(stderr, "kick: 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, "kick: 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, "kick: 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, "kick: 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, "kick: 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, "kick: GL setup failed; software fallback\n");
|
|
host.egl_ok = false;
|
|
return true;
|
|
}
|
|
overlay_font_init();
|
|
|
|
host.egl_ok = true;
|
|
kick_log(KICK_LOG_INFO, "kick: 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) {
|
|
kick_log(KICK_LOG_DEBUG, "[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)) {
|
|
kick_log(KICK_LOG_DEBUG, "[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:
|
|
kick_log(KICK_LOG_DEBUG, "[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;
|
|
}
|
|
|
|
/* Pace presents to the host compositor: this fires after the host has shown
|
|
the buffer we queued, so we know we may queue the next one. Any frame drawn
|
|
while a present is still pending is coalesced into the back buffer and
|
|
presented here, keeping exactly one outstanding buffer (no burst, no
|
|
tearing on busy repainters like terminals). */
|
|
static const struct wl_callback_listener host_frame_listener;
|
|
/* Request a host frame callback and present the drawing in the back buffer.
|
|
The callback must be requested *before* the swap: frame requests are part
|
|
of the state committed by eglSwapBuffers, so requesting it after would wait
|
|
on a commit that never comes. */
|
|
static void present_to_host(void) {
|
|
host.frame_cb = wl_surface_frame(host.surface);
|
|
wl_callback_add_listener(host.frame_cb, &host_frame_listener, NULL);
|
|
eglSwapBuffers(host.egl_display, host.egl_surface);
|
|
host.present_pending = true;
|
|
}
|
|
|
|
static void host_frame_done(void *data, struct wl_callback *callback,
|
|
uint32_t callback_data)
|
|
{
|
|
(void)data; (void)callback; (void)callback_data;
|
|
if (host.frame_cb) {
|
|
wl_callback_destroy(host.frame_cb);
|
|
host.frame_cb = NULL;
|
|
}
|
|
host.present_pending = false;
|
|
|
|
if (host.back_dirty) {
|
|
host.back_dirty = false;
|
|
present_to_host();
|
|
}
|
|
}
|
|
|
|
static const struct wl_callback_listener host_frame_listener = {
|
|
.done = host_frame_done,
|
|
};
|
|
|
|
/* The scene is rendered by the wlroots GLES2 renderer, which owns its own
|
|
EGL display/context, while we sample that buffer from a *different* EGL
|
|
context (host.egl_display). Without an explicit barrier the GPU may still
|
|
be executing the renderer's draw commands when we import+read the dmabuf,
|
|
which shows as flicker/tearing on content that repaints continuously.
|
|
Make the renderer's context current on this thread and glFinish() so the
|
|
buffer is guaranteed fully rendered before we blit it. */
|
|
static void sync_renderer_context(void) {
|
|
struct wlr_egl *src = wlr_gles2_renderer_get_egl(server.renderer);
|
|
if (!src) return;
|
|
EGLDisplay d = wlr_egl_get_display(src);
|
|
EGLContext c = wlr_egl_get_context(src);
|
|
if (d == EGL_NO_DISPLAY || c == EGL_NO_CONTEXT) return;
|
|
if (d == host.egl_display && c == host.egl_context) return;
|
|
eglMakeCurrent(d, EGL_NO_SURFACE, EGL_NO_SURFACE, c);
|
|
glFinish();
|
|
eglMakeCurrent(host.egl_display, host.egl_surface, host.egl_surface,
|
|
host.egl_context);
|
|
}
|
|
|
|
static void blit_scene_buffer(struct wlr_buffer *buf) {
|
|
int w = host.width, h = host.height;
|
|
glViewport(0, 0, w, h);
|
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
|
|
bool imported = buf && import_scene_buffer(buf, host.import_tex);
|
|
if (imported) {
|
|
static const GLfloat white[4] = { 1, 1, 1, 1 };
|
|
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,
|
|
buf->width, buf->height, w, h, white);
|
|
draw_tab_overlay();
|
|
glDisable(GL_BLEND);
|
|
}
|
|
if (dump_host_req) {
|
|
dump_host_req = false;
|
|
dump_host_back();
|
|
}
|
|
|
|
if (!imported) {
|
|
/* Never present a frame that is only the clear colour: keep the last
|
|
good frame on the host. A later frame with a good import presents
|
|
normally. */
|
|
return;
|
|
}
|
|
|
|
if (host.present_pending) {
|
|
/* Host hasn't shown the previous buffer yet: redraw into the back
|
|
buffer and coalesce the swap until the frame callback arrives. */
|
|
host.back_dirty = true;
|
|
} else {
|
|
present_to_host();
|
|
}
|
|
}
|
|
|
|
/* Standalone (native-output) frame: composite the scene into our swapchain,
|
|
then draw the Alt overlay as a second pass on the same buffer from wlr
|
|
textures. There is no host EGL surface to blit to and no host frame-call
|
|
pacing; the native output's own commit drives presentation. */
|
|
static void render_standalone(void) {
|
|
if (!server.scene_output || !server.swapchain) return;
|
|
|
|
struct wlr_output_state state;
|
|
wlr_output_state_init(&state);
|
|
apply_pending_output_resize(&state);
|
|
struct wlr_scene_output_state_options options = {
|
|
.swapchain = server.swapchain,
|
|
};
|
|
bool rendered = wlr_scene_output_build_state(server.scene_output, &state,
|
|
&options);
|
|
if (!rendered || !state.buffer) {
|
|
kick_log(KICK_LOG_DEBUG, "[render] build_state failed\n");
|
|
wlr_output_state_finish(&state);
|
|
goto frame_done;
|
|
}
|
|
|
|
if (dump_scene_req) {
|
|
dump_scene_req = false;
|
|
dump_scene_buffer(state.buffer);
|
|
}
|
|
|
|
if (host.overlay_visible) {
|
|
struct wlr_render_pass *pass = wlr_renderer_begin_buffer_pass(
|
|
server.renderer, state.buffer, NULL);
|
|
if (pass) {
|
|
draw_tab_overlay_pass(pass);
|
|
wlr_render_pass_submit(pass);
|
|
}
|
|
}
|
|
|
|
if (dump_host_req) {
|
|
dump_host_req = false;
|
|
dump_standalone_buffer(state.buffer);
|
|
}
|
|
|
|
if (!wlr_output_commit_state(server.output, &state))
|
|
kick_log(KICK_LOG_DEBUG, "[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);
|
|
}
|
|
|
|
static void render_and_present(void) {
|
|
if (server.standalone) {
|
|
render_standalone();
|
|
return;
|
|
}
|
|
if (!host.egl_ok || !host.configured) return;
|
|
if (!server.scene_output || !server.swapchain) return;
|
|
|
|
struct wlr_output_state state;
|
|
wlr_output_state_init(&state);
|
|
apply_pending_output_resize(&state);
|
|
struct wlr_scene_output_state_options options = {
|
|
.swapchain = server.swapchain,
|
|
};
|
|
bool rendered = wlr_scene_output_build_state(server.scene_output, &state,
|
|
&options);
|
|
if (!rendered || !state.buffer) {
|
|
kick_log(KICK_LOG_DEBUG, "[render] build_state failed\n");
|
|
wlr_output_state_finish(&state);
|
|
goto frame_done;
|
|
}
|
|
|
|
/* [diag] What did wlroots composite? A swapchain-sized buffer means we
|
|
composited the scene; anything else implies direct scanout of a client
|
|
buffer (which our blit may or may not import). */
|
|
if (kick_log_level() >= KICK_LOG_DEBUG) {
|
|
struct wlr_output *out = server.output;
|
|
kick_log(KICK_LOG_DEBUG, "[diag] frame buffer %dx%d (output %dx%d)\n",
|
|
state.buffer->width, state.buffer->height,
|
|
out ? out->width : 0, out ? out->height : 0);
|
|
}
|
|
|
|
/* Barrier between the wlr renderer's context and our host blit. */
|
|
sync_renderer_context();
|
|
|
|
if (dump_scene_req) {
|
|
dump_scene_req = false;
|
|
dump_scene_buffer(state.buffer);
|
|
}
|
|
|
|
static int autodump = -1;
|
|
if (autodump < 0)
|
|
autodump = getenv("KICK_AUTODUMP") ? 1 : 0;
|
|
if (autodump > 0 && client_dumped && --autodump == 0) {
|
|
dump_scene_req = true;
|
|
dump_host_req = true;
|
|
/* The flags are consumed by the *next* render pass; under the
|
|
idle-stop gate that pass needs an explicit redraw request. */
|
|
kick_request_redraw();
|
|
}
|
|
|
|
blit_scene_buffer(state.buffer);
|
|
|
|
if (!wlr_output_commit_state(server.output, &state))
|
|
kick_log(KICK_LOG_DEBUG, "[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 ===================== */
|
|
|
|
/* Does the first whitespace-delimited word of `cmd` resolve to an executable
|
|
on PATH? Mirrors how `sh -c <cmd>` resolves the first word, so this is a
|
|
cheap pre-launch "which" check. Returns 1 if found (or uncheckable, e.g.
|
|
a word too long or no PATH), 0 if the word is definitely missing. */
|
|
static int cmd_in_path(const char *cmd) {
|
|
while (*cmd == ' ' || *cmd == '\t')
|
|
cmd++;
|
|
const char *p = cmd;
|
|
while (*p && *p != ' ' && *p != '\t' && *p != '\n')
|
|
p++;
|
|
if (p == cmd)
|
|
return 0;
|
|
char word[256];
|
|
size_t n = (size_t)(p - cmd);
|
|
if (n >= sizeof(word))
|
|
return 1; /* give up on absurdly long commands */
|
|
memcpy(word, cmd, n);
|
|
word[n] = '\0';
|
|
|
|
if (strchr(word, '/'))
|
|
return access(word, X_OK) == 0;
|
|
|
|
const char *path_env = getenv("PATH");
|
|
if (!path_env) path_env = "/usr/local/bin:/usr/bin:/bin";
|
|
char buf[512];
|
|
for (const char *d = path_env; d && *d; ) {
|
|
const char *colon = strchr(d, ':');
|
|
size_t len = colon ? (size_t)(colon - d) : strlen(d);
|
|
if (len == 0) {
|
|
len = 1;
|
|
buf[0] = '.';
|
|
} else {
|
|
if (len + 1 + n >= sizeof(buf)) {
|
|
if (colon) d = colon + 1; else d = NULL;
|
|
continue;
|
|
}
|
|
memcpy(buf, d, len);
|
|
buf[len] = '/';
|
|
}
|
|
memcpy(buf + len + 1, word, n);
|
|
buf[len + 1 + n] = '\0';
|
|
if (access(buf, X_OK) == 0)
|
|
return 1;
|
|
if (colon) d = colon + 1; else d = NULL;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#if HAVE_XWAYLAND
|
|
/* Kick runs its own plain Xwayland (see the wlr_xwayland_create call site):
|
|
no -auth, so any local client connects without a magic cookie and nothing
|
|
is written to the session Xauthority. This is the normal wlroots default
|
|
and avoids the legacy X11 access-control dance entirely. */
|
|
#endif
|
|
|
|
static void launch_client(struct kick_app *app) {
|
|
pid_t pid = fork();
|
|
if (pid == 0) {
|
|
/* Own process group so we can kill the whole client tree on exit. */
|
|
setpgid(0, 0);
|
|
const char *xdg_runtime = getenv("XDG_RUNTIME_DIR");
|
|
if (!xdg_runtime) xdg_runtime = "/tmp";
|
|
setenv("WAYLAND_DISPLAY", kick_socket, 1);
|
|
setenv("XDG_RUNTIME_DIR", xdg_runtime, 1);
|
|
setenv("GDK_BACKEND", "wayland", 1);
|
|
setenv("QT_QPA_PLATFORM", "wayland", 1);
|
|
#if HAVE_XWAYLAND
|
|
/* X-only commands (xmessage, xeyes, legacy toolkits) connect through
|
|
our Xwayland. GDK/Qt stay on Wayland via the forced backends
|
|
above; SDL and friends prefer Wayland too, so DISPLAY only picks
|
|
up clients with no Wayland support. */
|
|
if (server.xwayland && server.xwayland->display_name) {
|
|
setenv("DISPLAY", server.xwayland->display_name, 1);
|
|
} else
|
|
unsetenv("DISPLAY");
|
|
#else
|
|
unsetenv("DISPLAY");
|
|
#endif
|
|
/* Unless debugging, don't let client chatter spill onto the launching
|
|
terminal; the client inherits our stdout/stderr. */
|
|
if (kick_log_level() < KICK_LOG_DEBUG) {
|
|
int nullfd = open("/dev/null", O_WRONLY);
|
|
if (nullfd >= 0) {
|
|
dup2(nullfd, STDOUT_FILENO);
|
|
dup2(nullfd, STDERR_FILENO);
|
|
if (nullfd > STDERR_FILENO) close(nullfd);
|
|
}
|
|
}
|
|
execlp("/bin/sh", "sh", "-c", app->cmd, NULL);
|
|
_exit(127);
|
|
}
|
|
if (pid > 0) {
|
|
app->pid = pid;
|
|
kick_log(KICK_LOG_INFO, "[launch] app=%d pid=%d cmd=%s\n",
|
|
app_index(app), pid, app->cmd);
|
|
}
|
|
}
|
|
|
|
/* Fork every command-line command at startup, in command-line order. Each
|
|
slot keeps its position because handle_new_toplevel claims it by process
|
|
group (launch_client runs each command in its own setpgid'd group, whose
|
|
id equals app->pid), so the tab order mirrors the command line no matter
|
|
which client connects first. A command that never connects leaves its
|
|
tab black; a command whose process group exits without ever connecting is
|
|
swept away later (see sweep_dead_startup_groups). */
|
|
static void launch_all_cli(void) {
|
|
for (int i = 0; i < server.num_apps; i++) {
|
|
struct kick_app *a = server.apps[i];
|
|
if (a->origin != ORIGIN_CLI) continue;
|
|
launch_client(a);
|
|
}
|
|
}
|
|
|
|
/* Drop black tabs whose command's whole process group has exited (e.g.
|
|
`kick /bin/false` -- sh runs false, the group empties, and the
|
|
tab would otherwise stay black forever). Runs from the main loop after a
|
|
SIGCHLD, never from the handler: it frees slots. Group liveness, not the
|
|
launcher pid's, decides: `sh -c "cmd &"` returns while cmd is still
|
|
starting, but cmd holds the group open, so kill(-groupid,0) succeeds and
|
|
the tab survives. */
|
|
/* Human words for a raw waitpid status, for the sweep's diagnostics. */
|
|
static void describe_child_status(int status, char *buf, size_t cap) {
|
|
if (WIFEXITED(status))
|
|
snprintf(buf, cap, "exit code %d", WEXITSTATUS(status));
|
|
else if (WIFSIGNALED(status))
|
|
snprintf(buf, cap, "killed by signal %d", WTERMSIG(status));
|
|
else
|
|
snprintf(buf, cap, "status 0x%x", status);
|
|
}
|
|
|
|
static void sweep_dead_startup_groups(void) {
|
|
for (int i = 0; i < server.num_apps; i++) {
|
|
struct kick_app *a = server.apps[i];
|
|
if (a->toplevel || !a->pending_claim || a->closing || a->pid <= 0)
|
|
continue;
|
|
errno = 0;
|
|
if (kill(-a->pid, 0) == 0 || errno == EPERM)
|
|
continue; /* the group still has a member */
|
|
if (errno != ESRCH)
|
|
continue;
|
|
/* Say why on stderr regardless of log level: at default levels the
|
|
client's own output is /dev/null'd and kick_log INFO is hidden, so
|
|
a command that dies before opening its window would otherwise end
|
|
the session (last tab) or shrink it with no visible explanation. */
|
|
char why[64];
|
|
why[0] = '\0';
|
|
if ((pid_t)last_child_pid == a->pid)
|
|
describe_child_status((int)last_child_status, why, sizeof(why));
|
|
const char *cmd = a->cmd ? a->cmd : "?";
|
|
if (why[0])
|
|
fprintf(stderr, "kick: tab %d (%s): command exited without "
|
|
"opening a window (%s)\n", i, cmd, why);
|
|
else
|
|
fprintf(stderr, "kick: tab %d (%s): command exited without "
|
|
"opening a window\n", i, cmd);
|
|
if (server.active_app == a) {
|
|
server.active_app = NULL;
|
|
struct kick_app *succ = pick_successor(a, -1);
|
|
if (!succ) succ = pick_successor(a, +1);
|
|
if (succ)
|
|
set_active_app(succ);
|
|
else {
|
|
/* the last tab died before ever connecting: tear down */
|
|
fprintf(stderr, "kick: no tabs left to show, exiting\n");
|
|
host_teardown_window();
|
|
shutdown_cleanup();
|
|
exit(0);
|
|
}
|
|
}
|
|
remove_app(i);
|
|
i--;
|
|
}
|
|
/* If the sweep's succession handed active to a slot that this same pass
|
|
then removed, put the crown back on some live tab (or nothing). */
|
|
if (!server.active_app) {
|
|
struct kick_app *succ = NULL;
|
|
for (int i = 0; i < server.num_apps; i++)
|
|
if (app_live(server.apps[i]) && !server.apps[i]->closing) {
|
|
succ = server.apps[i];
|
|
break;
|
|
}
|
|
if (succ)
|
|
set_active_app(succ);
|
|
}
|
|
}
|
|
|
|
/* Drain the SIGCHLD latch and reap dead startup groups; called from both
|
|
event loops after each dispatch. */
|
|
static void sweep_dead_startup(void) {
|
|
if (!sigchld_seen) return;
|
|
sigchld_seen = 0;
|
|
sweep_dead_startup_groups();
|
|
}
|
|
|
|
/* Add a new tab launching `cmd` and start it, exactly as if `cmd` had been
|
|
given as a command-line argument. The tab is inserted in the slot order
|
|
right after the currently active tab (falling back to the far right if
|
|
none is active), so the new tab sits to its right in the cycle. Returns
|
|
the new app index, or -1 on allocation failure. */
|
|
static int add_tab(const char *cmd) {
|
|
if (apps_ensure_cap(server.num_apps + 1) != 0) return -1;
|
|
struct kick_app *app = calloc(1, sizeof(*app));
|
|
if (!app) return -1;
|
|
app->cmd = strdup(cmd);
|
|
app->pid = -1;
|
|
app->pending_claim = true;
|
|
app->origin = ORIGIN_ADD_TAB;
|
|
wl_list_init(&app->sub_windows);
|
|
|
|
/* Anchor on the active slot itself, not its toplevel: while the active
|
|
tab is still black (its client has not connected yet) it has no
|
|
toplevel, so anchoring on the toplevel would append the new terminal
|
|
far right instead of beside the current tab. */
|
|
insert_app_after(app, server.active_app);
|
|
int idx = app_index(app);
|
|
launch_client(app);
|
|
return idx;
|
|
}
|
|
|
|
/* ===================== 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);
|
|
remove_socket_file();
|
|
_exit(139);
|
|
}
|
|
|
|
static void sigchld_handler(int sig) {
|
|
(void)sig;
|
|
sigchld_seen = 1;
|
|
int status;
|
|
pid_t pid;
|
|
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
|
|
last_child_pid = (sig_atomic_t)pid;
|
|
last_child_status = (sig_atomic_t)status;
|
|
if (kick_log_level() >= KICK_LOG_DEBUG) {
|
|
/* snprintf()/write() are async-signal-safe; fprintf() is not (it
|
|
takes the FILE lock, which can deadlock if we interrupt a logging
|
|
fprintf in the main loop). */
|
|
char buf[64];
|
|
int n = snprintf(buf, sizeof(buf),
|
|
"[child] pid %d exited status 0x%x\n", pid, status);
|
|
if (n > 0 && (size_t)n < sizeof(buf))
|
|
(void)!write(STDERR_FILENO, buf, (size_t)n);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* On SIGTERM or Ctrl-C (SIGINT), take down the clients and exit cleanly
|
|
rather than leaving them orphaned when the compositor dies. The clients
|
|
live in their own process groups, so a terminal Ctrl-C only reaches us.
|
|
|
|
These signals are delivered through the event loop (wl_event_loop_add_signal
|
|
in main) rather than a raw sigaction handler: the full teardown calls
|
|
malloc/free and libwayland proxy destroys, which are not async-signal-safe
|
|
and deadlock if the signal interrupts fork()/malloc()/logging (observed:
|
|
SIGTERM landing inside launch_client's fork() left libc's allocator lock
|
|
held, and shutdown_cleanup() -> bridge_cleanup() -> calloc() blocked on it
|
|
forever). Running it from a normal loop callback makes that impossible. */
|
|
static int close_signal_cb(int sig, void *data) {
|
|
(void)sig;
|
|
(void)data;
|
|
shutdown_cleanup();
|
|
_exit(0);
|
|
return 0;
|
|
}
|
|
|
|
/* SIGUSR1: save the current scene buffer and host back buffer on the next
|
|
frame, for inspecting what the client has actually drawn so far. */
|
|
static void sigusr1_handler(int sig) {
|
|
(void)sig;
|
|
dump_scene_req = 1;
|
|
dump_host_req = 1;
|
|
needs_redraw = 1;
|
|
/* A signal can land while the loop is blocked in poll on an idle
|
|
compositor; the write wakes it so the dump still renders. */
|
|
if (server.sig_pipe[1] >= 0) {
|
|
char c = 1;
|
|
(void)!write(server.sig_pipe[1], &c, 1);
|
|
}
|
|
}
|
|
|
|
/* Drain the SIGUSR1 self-pipe wake; the render gate (not this callback)
|
|
decides what to present. */
|
|
static int sigpipe_cb(int fd, uint32_t mask, void *data) {
|
|
(void)mask; (void)data;
|
|
char buf[64];
|
|
while (read(fd, buf, sizeof(buf)) > 0)
|
|
;
|
|
return 0;
|
|
}
|
|
|
|
/* Does the comma-separated WLR_BACKENDS list contain `name`? */
|
|
static bool backends_include(const char *env, const char *name) {
|
|
size_t len = strlen(name);
|
|
for (const char *p = env; p && *p; ) {
|
|
const char *comma = strchr(p, ',');
|
|
size_t n = comma ? (size_t)(comma - p) : strlen(p);
|
|
if (n == len && strncmp(p, name, len) == 0)
|
|
return true;
|
|
p = comma ? comma + 1 : NULL;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* ===================== Main ===================== */
|
|
|
|
/* Parse a --screen-mode value: WxH or WxH@Hz (the Hz is matched at the
|
|
mode's mHz precision, so "60" == 60.000). Stores into mode_req_w/h/
|
|
refresh; returns false on a malformed value. */
|
|
static bool parse_screen_mode(const char *val) {
|
|
int w = 0, h = 0;
|
|
const char *p = val;
|
|
for (; *p >= '0' && *p <= '9'; p++)
|
|
w = w * 10 + (*p - '0');
|
|
if (*p != 'x')
|
|
return false;
|
|
p++;
|
|
for (; *p >= '0' && *p <= '9'; p++)
|
|
h = h * 10 + (*p - '0');
|
|
if (w <= 0 || h <= 0)
|
|
return false;
|
|
unsigned refresh_mhz = 0;
|
|
if (*p == '@') {
|
|
char *end = NULL;
|
|
double hz = strtod(p + 1, &end);
|
|
if (end == p + 1 || hz <= 0.0 || *end != '\0')
|
|
return false;
|
|
refresh_mhz = (unsigned)(hz * 1000.0 + 0.5);
|
|
} else if (*p != '\0') {
|
|
return false;
|
|
}
|
|
mode_req_w = w;
|
|
mode_req_h = h;
|
|
mode_req_refresh = (int)refresh_mhz;
|
|
return true;
|
|
}
|
|
|
|
int main(int argc, char *argv[]) {
|
|
snprintf(kick_socket, sizeof(kick_socket), "kick-%d",
|
|
(int)getpid());
|
|
const char *runtime = getenv("XDG_RUNTIME_DIR");
|
|
if (!runtime) runtime = "/tmp";
|
|
snprintf(socket_path, sizeof(socket_path), "%s/%s", runtime, kick_socket);
|
|
/* Clear a stale leftover (e.g. from a crashed previous run with the same
|
|
name) before we try to bind; harmless if there is none. */
|
|
remove_socket_file();
|
|
|
|
/* Resolve KICK_LOG up front so signal handlers can read the cached
|
|
level without touching non-async-signal-safe functions. */
|
|
kick_log_level_init();
|
|
|
|
/* KICK_SYNTH_INPUT: keep real host keyboard events out of the test
|
|
channel so the stdin feed is the only source of key/modifier input. */
|
|
{
|
|
const char *v = getenv("KICK_SYNTH_INPUT");
|
|
synth_input_only = v && v[0] && v[0] != '0';
|
|
}
|
|
|
|
/* Alt+Shift+T opens a terminal named by $TERMINAL when set, else the
|
|
alacritty default. Every command-line argument is one tab launcher,
|
|
except --screen-mode WxH[@Hz], which selects the native output mode and
|
|
is consumed here, --modifier alt|super, which picks the modifier
|
|
prefix for the tab-management chords, and --start-cmd-index N, which
|
|
picks the command (1-based) that starts focused. Handle all of this up
|
|
front so a missing command (or a bad request) can be reported and the
|
|
process can exit before any window is created. */
|
|
{
|
|
const char *env_term = getenv("TERMINAL");
|
|
if (env_term && env_term[0])
|
|
term_cmd = env_term;
|
|
}
|
|
char **appv = calloc((size_t)argc, sizeof(*appv));
|
|
if (!appv) {
|
|
fprintf(stderr, "kick: out of memory\n");
|
|
return 1;
|
|
}
|
|
int napps = 0;
|
|
for (int i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
|
fprintf(stderr,
|
|
"kick Tab Compositor\n"
|
|
"\n"
|
|
"kick opens one tab per command-line argument. Each "
|
|
"argument is a shell\n"
|
|
"command; quote it when it takes arguments. With no "
|
|
"arguments a single\n"
|
|
"terminal tab opens ($TERMINAL, default alacritty).\n"
|
|
"\n"
|
|
"Tabs are managed with chords on a modifier prefix -- "
|
|
"Alt by default;\n"
|
|
"--modifier super moves them to Super.\n"
|
|
"\n"
|
|
"Usage:\n"
|
|
" %s [options] [prog...]\n"
|
|
" %s -h | --help\n"
|
|
"\n"
|
|
"Example:\n"
|
|
" # kick \"firefox -no-remote\"\n"
|
|
"\n"
|
|
"Options:\n"
|
|
" --screen-mode WxH[@Hz] Select the native output "
|
|
"mode (standalone\n"
|
|
" only), e.g. 1920x1080 or "
|
|
"1920x1080@60. A\n"
|
|
" request nothing supports "
|
|
"prints every mode\n"
|
|
" and exits.\n"
|
|
" --modifier alt|super Pick the tab-management "
|
|
"modifier (default\n"
|
|
" alt). 'super' suits "
|
|
"standalone/DRM and\n"
|
|
" labwc-style hosts.\n"
|
|
" --start-cmd-index N Pick the command (1-based) "
|
|
"that starts\n"
|
|
" focused; without it the "
|
|
"last command does.\n"
|
|
" --sub-windows tabs|stack How a client's second "
|
|
"window appears:\n"
|
|
" 'tabs' gives each its own "
|
|
"tab right of its\n"
|
|
" parent (default); 'stack' "
|
|
"stacks it over\n"
|
|
" the parent tab, sharing "
|
|
"its visibility and\n"
|
|
" focus.\n"
|
|
" -h, --help Show the full help.\n"
|
|
"\n"
|
|
"Keys (hold the modifier):\n"
|
|
" Up / Down Cycle tabs; hold to keep "
|
|
"cycling.\n"
|
|
" Shift+Up / Shift+Down Reorder the active tab.\n"
|
|
" Delete Close the active tab.\n"
|
|
" Shift+T Open a new terminal tab "
|
|
"($TERMINAL).\n"
|
|
" ` (grave) Cycle the tab's stacked "
|
|
"windows; hold to keep cycling.\n"
|
|
" Ctrl+Alt+F1..F12 Switch virtual terminals "
|
|
"(standalone only).\n",
|
|
argv[0], argv[0]);
|
|
free(appv);
|
|
return 0;
|
|
}
|
|
const char *val = NULL;
|
|
if (strcmp(argv[i], "--screen-mode") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "kick: --screen-mode needs a value, "
|
|
"e.g. --screen-mode 1920x1080\n");
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
val = argv[++i];
|
|
} else if (strncmp(argv[i], "--screen-mode=", 14) == 0) {
|
|
val = argv[i] + 14;
|
|
} else if (strcmp(argv[i], "--modifier") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "kick: --modifier needs a value, "
|
|
"e.g. --modifier super\n");
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
const char *m = argv[++i];
|
|
if (strcmp(m, "alt") == 0) {
|
|
tab_prefix = PREFIX_ALT;
|
|
} else if (strcmp(m, "super") == 0) {
|
|
tab_prefix = PREFIX_SUPER;
|
|
} else {
|
|
fprintf(stderr, "kick: bad --modifier '%s' "
|
|
"(use 'alt' or 'super')\n", m);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
continue;
|
|
} else if (strncmp(argv[i], "--modifier=", 11) == 0) {
|
|
const char *m = argv[i] + 11;
|
|
if (strcmp(m, "alt") == 0) {
|
|
tab_prefix = PREFIX_ALT;
|
|
} else if (strcmp(m, "super") == 0) {
|
|
tab_prefix = PREFIX_SUPER;
|
|
} else {
|
|
fprintf(stderr, "kick: bad --modifier '%s' "
|
|
"(use 'alt' or 'super')\n", m);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
continue;
|
|
} else if (strcmp(argv[i], "--sub-windows") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "kick: --sub-windows needs a value "
|
|
"(tabs or stack)\n");
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
const char *m = argv[++i];
|
|
if (strcmp(m, "tabs") == 0) {
|
|
stack_sub_windows = false;
|
|
} else if (strcmp(m, "stack") == 0) {
|
|
stack_sub_windows = true;
|
|
} else {
|
|
fprintf(stderr, "kick: bad --sub-windows '%s' "
|
|
"(use 'tabs' or 'stack')\n", m);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
continue;
|
|
} else if (strncmp(argv[i], "--sub-windows=", 14) == 0) {
|
|
const char *m = argv[i] + 14;
|
|
if (strcmp(m, "tabs") == 0) {
|
|
stack_sub_windows = false;
|
|
} else if (strcmp(m, "stack") == 0) {
|
|
stack_sub_windows = true;
|
|
} else {
|
|
fprintf(stderr, "kick: bad --sub-windows '%s' "
|
|
"(use 'tabs' or 'stack')\n", m);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
continue;
|
|
} else if (strcmp(argv[i], "--start-cmd-index") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "kick: --start-cmd-index needs a "
|
|
"value, e.g. --start-cmd-index 2\n");
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
const char *n = argv[++i];
|
|
char *end = NULL;
|
|
long idx = strtol(n, &end, 10);
|
|
if (end == n || *end != '\0' || idx < 1 || idx > 2147483647L) {
|
|
fprintf(stderr, "kick: bad --start-cmd-index '%s' "
|
|
"(use a 1-based command number, e.g. --start-cmd-index "
|
|
"2)\n", n);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
/* Convert to the program's 0-based index as soon as possible. */
|
|
start_cmd_index = (int)idx - 1;
|
|
continue;
|
|
} else if (strncmp(argv[i], "--start-cmd-index=",
|
|
sizeof("--start-cmd-index=") - 1) == 0) {
|
|
const char *n = argv[i] + sizeof("--start-cmd-index=") - 1;
|
|
char *end = NULL;
|
|
long idx = strtol(n, &end, 10);
|
|
if (end == n || *end != '\0' || idx < 1 || idx > 2147483647L) {
|
|
fprintf(stderr, "kick: bad --start-cmd-index '%s' "
|
|
"(use a 1-based command number)\n", n);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
start_cmd_index = (int)idx - 1;
|
|
continue;
|
|
}
|
|
if (val) {
|
|
if (!parse_screen_mode(val)) {
|
|
fprintf(stderr, "kick: bad --screen-mode '%s' "
|
|
"(use WxH or WxH@Hz, e.g. 1920x1080 or "
|
|
"1920x1080@60)\n", val);
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
continue;
|
|
}
|
|
appv[napps++] = argv[i];
|
|
}
|
|
int total_apps = napps;
|
|
bool default_tab = false;
|
|
if (total_apps == 0) {
|
|
/* No arguments: open one terminal tab, as Alt+Shift+T would. */
|
|
default_tab = true;
|
|
total_apps = 1;
|
|
}
|
|
if (start_cmd_index >= 0 && start_cmd_index >= total_apps) {
|
|
fprintf(stderr, "kick: --start-cmd-index %d is out of range: "
|
|
"1..%d (%d command%s)\n", start_cmd_index + 1, total_apps,
|
|
total_apps, total_apps == 1 ? "" : "s");
|
|
free(appv);
|
|
return 2;
|
|
}
|
|
for (int i = 0; i < total_apps; i++) {
|
|
const char *cmd = default_tab ? term_cmd : appv[i];
|
|
if (!cmd_in_path(cmd)) {
|
|
fprintf(stderr, "kick: '%s' not found in PATH%s\n",
|
|
cmd, default_tab ? " (set $TERMINAL)" : "");
|
|
free(appv);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
struct sigaction crash_sa = {
|
|
.sa_sigaction = crash_handler,
|
|
.sa_flags = SA_SIGINFO | SA_NODEFER,
|
|
};
|
|
sigaction(SIGSEGV, &crash_sa, NULL);
|
|
sigaction(SIGABRT, &crash_sa, NULL);
|
|
server.sig_pipe[0] = server.sig_pipe[1] = -1;
|
|
if (pipe(server.sig_pipe) != 0) {
|
|
fprintf(stderr, "kick: failed to create SIGUSR1 wake pipe\n");
|
|
return 1;
|
|
}
|
|
int pipe_flags = fcntl(server.sig_pipe[0], F_GETFL, 0);
|
|
if (pipe_flags < 0
|
|
|| fcntl(server.sig_pipe[0], F_SETFL,
|
|
pipe_flags | O_NONBLOCK) != 0) {
|
|
fprintf(stderr, "kick: failed to make SIGUSR1 pipe non-blocking\n");
|
|
return 1;
|
|
}
|
|
signal(SIGCHLD, sigchld_handler);
|
|
signal(SIGUSR1, sigusr1_handler);
|
|
|
|
/* wlroots logs follow KICK_LOG: quiet keeps only errors, info drops
|
|
protocol noise, debug shows everything. */
|
|
switch (kick_log_level()) {
|
|
case KICK_LOG_DEBUG:
|
|
wlr_log_init(WLR_DEBUG, NULL);
|
|
break;
|
|
case KICK_LOG_INFO:
|
|
wlr_log_init(WLR_INFO, NULL);
|
|
break;
|
|
default:
|
|
wlr_log_init(WLR_ERROR, NULL);
|
|
break;
|
|
}
|
|
|
|
/* Mode selection: KICK_MODE=standalone (or any forced WLR_BACKENDS)
|
|
drives a native output directly (DRM, or headless for tests); the
|
|
default nested mode blits a headless output into a host window. */
|
|
server.standalone = false;
|
|
{
|
|
const char *mode = getenv("KICK_MODE");
|
|
const char *wlr_backends = getenv("WLR_BACKENDS");
|
|
if (mode && strcmp(mode, "standalone") == 0)
|
|
server.standalone = true;
|
|
if (wlr_backends && wlr_backends[0])
|
|
server.standalone = true;
|
|
}
|
|
|
|
server.wl_display = wl_display_create();
|
|
if (!server.wl_display) {
|
|
fprintf(stderr, "kick: failed to create Wayland display\n");
|
|
return 1;
|
|
}
|
|
server.loop = wl_display_get_event_loop(server.wl_display);
|
|
wl_list_init(&server.kbdevs);
|
|
wl_list_init(&server.ptrdevs);
|
|
|
|
/* SIGUSR1 dump wake: the read end registers an event source so the byte
|
|
written by the handler interrupts the blocking dispatch below. */
|
|
server.sigpipe_src = wl_event_loop_add_fd(server.loop, server.sig_pipe[0],
|
|
WL_EVENT_READABLE, sigpipe_cb, NULL);
|
|
|
|
/* Route SIGTERM/SIGINT through the event loop (signalfd) so shutdown runs
|
|
in normal context; see close_signal_cb for why a raw handler is unsafe. */
|
|
wl_event_loop_add_signal(server.loop, SIGTERM, close_signal_cb, NULL);
|
|
wl_event_loop_add_signal(server.loop, SIGINT, close_signal_cb, NULL);
|
|
|
|
/* stdin event feed: lets tests/drivers inject raw keyboard events
|
|
into the real host key handlers without a host compositor. */
|
|
server.ctl_source = wl_event_loop_add_fd(server.loop, STDIN_FILENO,
|
|
WL_EVENT_READABLE,
|
|
ctl_cb, NULL);
|
|
|
|
if (server.standalone) {
|
|
/* autocreate honours WLR_BACKENDS and otherwise picks the current
|
|
session's wayland/x11 backend (we run inside some compositor
|
|
elsewhere), so force DRM+libinput unless told otherwise. */
|
|
const char *wlr_backends = getenv("WLR_BACKENDS");
|
|
if (!wlr_backends || !wlr_backends[0]) {
|
|
wlr_backends = "drm,libinput";
|
|
setenv("WLR_BACKENDS", wlr_backends, 1);
|
|
}
|
|
server.standalone_headless = backends_include(wlr_backends, "headless");
|
|
/* autocreate wraps every backend in a wlr_multi_backend, which breaks
|
|
the plain headless backend's manual wlr_headless_add_output() below
|
|
(it asserts wlr_backend_is_headless()), so create the headless
|
|
backend directly instead. */
|
|
if (server.standalone_headless)
|
|
server.backend = wlr_headless_backend_create(server.loop);
|
|
else
|
|
server.backend = wlr_backend_autocreate(server.loop, &server.session);
|
|
} else {
|
|
server.backend = wlr_headless_backend_create(server.loop);
|
|
}
|
|
if (!server.backend) {
|
|
fprintf(stderr, "kick: failed to create %s backend\n",
|
|
server.standalone ? "backend (autocreate)"
|
|
: "headless backend");
|
|
return 1;
|
|
}
|
|
|
|
server.renderer = wlr_renderer_autocreate(server.backend);
|
|
if (!server.renderer) {
|
|
fprintf(stderr, "kick: no GPU renderer available\n");
|
|
return 1;
|
|
}
|
|
wlr_renderer_init_wl_display(server.renderer, server.wl_display);
|
|
|
|
/* Standalone has no host EGL context: build the overlay glyph atlas with
|
|
the wlr renderer instead (nested mode does this inside init_egl()). */
|
|
if (server.standalone)
|
|
overlay_font_init();
|
|
|
|
server.allocator = wlr_allocator_autocreate(server.backend, server.renderer);
|
|
if (!server.allocator) {
|
|
fprintf(stderr, "kick: failed to create allocator\n");
|
|
return 1;
|
|
}
|
|
|
|
/* wl_surface at v4: legacy clients (e.g. VLC's Wayland vout) place video
|
|
via non-zero wl_surface.attach offsets, which is a protocol violation
|
|
from v5 on. Advertise v4 so those offsets are accepted and ignored,
|
|
like before the v5 breaking change. */
|
|
server.compositor = wlr_compositor_create(server.wl_display, 4,
|
|
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);
|
|
wlr_data_control_manager_v1_create(server.wl_display);
|
|
|
|
/* Never let wlroots present a client buffer to the headless output
|
|
directly (direct scanout). We must composite into our own swapchain so
|
|
the nested frame is exactly what the host window shows; scanout of a
|
|
client buffer hands us a buffer our host blit can't keep in sync and the
|
|
window goes black (seen with Firefox). The scene field is WLR_PRIVATE,
|
|
so the supported way is this env var, parsed at wlr_scene_create(). */
|
|
setenv("WLR_SCENE_DISABLE_DIRECT_SCANOUT", "1", 1);
|
|
|
|
server.scene = wlr_scene_create();
|
|
server.xdg_shell = wlr_xdg_shell_create(server.wl_display, 6);
|
|
|
|
/* Passive protocol globals that cost one global each and let clients run
|
|
at their best: presentation-time for frame pacing, xdg-output for
|
|
logical output geometry, viewporter for viewport-scaled buffers (e.g.
|
|
video). wlr_scene applies viewports to xdg surfaces automatically. */
|
|
server.presentation = wlr_presentation_create(server.wl_display,
|
|
server.backend, 1);
|
|
server.viewporter = wlr_viewporter_create(server.wl_display);
|
|
|
|
/* Let nested clients pick their cursor shape (cursor-shape-v1); the
|
|
manager is passive, so we handle request_set_shape ourselves below:
|
|
apply it to the standalone cursor or forward it to the host pointer. */
|
|
server.shape_mgr = wlr_cursor_shape_manager_v1_create(server.wl_display, 1);
|
|
if (server.shape_mgr) {
|
|
server.shape_listener.notify = handle_shape_set;
|
|
wl_signal_add(&server.shape_mgr->events.request_set_shape,
|
|
&server.shape_listener);
|
|
}
|
|
|
|
server.seat = wlr_seat_create(server.wl_display, "kick");
|
|
wlr_seat_set_capabilities(server.seat,
|
|
WL_SEAT_CAPABILITY_POINTER | WL_SEAT_CAPABILITY_KEYBOARD);
|
|
|
|
#if HAVE_XWAYLAND
|
|
/* XWayland: X11 apps get tabs like Wayland clients. Lazy start keeps an
|
|
idle session free of an Xwayland process; display_name is reserved at
|
|
create time, so launch_client can export DISPLAY right away. The XWM
|
|
bridges ICCCM focus and X11 selections onto the seat, which rides the
|
|
existing clipboard bridge out to the host. */
|
|
if (!getenv("KICK_NO_XWAYLAND")) {
|
|
/* Run our own plain Xwayland: ignore any WLR_XWAYLAND wrapper the
|
|
environment may point at (e.g. one that forces `-auth` and the
|
|
legacy X11 cookie dance). wlroots then execs the compiled-in
|
|
Xwayland with no -auth, so local clients connect freely and we
|
|
never touch the session Xauthority file. */
|
|
unsetenv("WLR_XWAYLAND");
|
|
server.xwayland = wlr_xwayland_create(server.wl_display,
|
|
server.compositor, true);
|
|
if (server.xwayland) {
|
|
wl_list_init(&server.or_windows);
|
|
wlr_xwayland_set_seat(server.xwayland, server.seat);
|
|
server.xwayland_new_surface.notify = handle_xwayland_new_surface;
|
|
wl_signal_add(&server.xwayland->events.new_surface,
|
|
&server.xwayland_new_surface);
|
|
server.xwayland_ready.notify = handle_xwayland_ready;
|
|
wl_signal_add(&server.xwayland->events.ready,
|
|
&server.xwayland_ready);
|
|
server.xwayland_destroy.notify = handle_xwayland_destroy;
|
|
wl_signal_add(&server.xwayland->events.destroy,
|
|
&server.xwayland_destroy);
|
|
} else {
|
|
kick_log(KICK_LOG_INFO, "kick: XWayland unavailable "
|
|
"(no Xwayland binary?)\n");
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* Pointer constraints + relative pointer: games and creative apps ask for
|
|
these to grab the cursor and to get raw relative motion. The constraint
|
|
is activated/deactivated on seat pointer focus change; relative deltas
|
|
are forwarded from the input paths. */
|
|
server.pointer_constraints = wlr_pointer_constraints_v1_create(server.wl_display);
|
|
server.relative_pointer_mgr = wlr_relative_pointer_manager_v1_create(server.wl_display);
|
|
server.seat_pointer_focus.notify = handle_seat_pointer_focus;
|
|
wl_signal_add(&server.seat->pointer_state.events.focus_change,
|
|
&server.seat_pointer_focus);
|
|
/* Primary selection (zwp_primary_selection_v1): advertise the manager so
|
|
clients like text editors can set the selection with their own serial
|
|
(no roundtrip needed); the request handler installs it on the seat,
|
|
which distributes it to the other focused tab clients. */
|
|
server.primary_selection_mgr =
|
|
wlr_primary_selection_v1_device_manager_create(server.wl_display);
|
|
server.request_set_primary_selection.notify = seat_request_set_primary_selection;
|
|
wl_signal_add(&server.seat->events.request_set_primary_selection,
|
|
&server.request_set_primary_selection);
|
|
/* The request_set_selection handler is what actually performs the
|
|
wlr_seat_set_selection for clients using the data-control manager; the
|
|
host-forwarding it triggers is a no-op without an outer host (backend
|
|
NONE), so standalone needs it too or wl-copy-style clipboard requests
|
|
are silently dropped. */
|
|
bridge_attach_seat();
|
|
|
|
/* The output layout exists in both modes so xdg-output can advertise
|
|
logical output geometry to clients; nested mode simply has one output
|
|
(the host window) in the layout. */
|
|
server.output_layout = wlr_output_layout_create(server.wl_display);
|
|
if (!server.output_layout) {
|
|
fprintf(stderr, "kick: failed to create output layout\n");
|
|
return 1;
|
|
}
|
|
server.xdg_output_mgr = wlr_xdg_output_manager_v1_create(server.wl_display,
|
|
server.output_layout);
|
|
|
|
/* Standalone has no host compositor to draw a pointer, so give the
|
|
output a real wlr_cursor. Theme/size follow the XDG cursor convention:
|
|
XCURSOR_THEME (default "default") and XCURSOR_SIZE (default 24px).
|
|
wlroots resolves the theme itself (search path, Inherits chains) and,
|
|
when nothing is installed, substitutes a builtin arrow -- so the pointer
|
|
is never invisible. We only report when the requested theme was
|
|
silently swapped for the builtin. */
|
|
if (server.standalone) {
|
|
server.cursor = wlr_cursor_create();
|
|
if (!server.cursor) {
|
|
fprintf(stderr, "kick: failed to create cursor\n");
|
|
return 1;
|
|
}
|
|
wlr_cursor_attach_output_layout(server.cursor, server.output_layout);
|
|
|
|
const char *theme_env = getenv("XCURSOR_THEME");
|
|
const char *theme = theme_env && *theme_env ? theme_env : "default";
|
|
|
|
uint32_t size = 24;
|
|
const char *size_env = getenv("XCURSOR_SIZE");
|
|
if (size_env) {
|
|
char *end = NULL;
|
|
unsigned long v = strtoul(size_env, &end, 10);
|
|
if (end != size_env && v > 0)
|
|
size = (uint32_t)v;
|
|
}
|
|
|
|
server.xcursor_mgr = wlr_xcursor_manager_create(theme, size);
|
|
if (server.xcursor_mgr)
|
|
wlr_xcursor_manager_load(server.xcursor_mgr, 1);
|
|
|
|
/* Detect the builtin from the manager's own state: wlr_xcursor_theme_load
|
|
renames the theme object to "default" when it substitutes the builtin
|
|
arrow (cursor_count == 0), so a mismatch with the requested name
|
|
means the theme didn't exist on disk. A theme literally named
|
|
"default" is indistinguishable (the builtin adopts that name), so
|
|
skip the warning there and only fail warm either way. */
|
|
struct wlr_xcursor *cur = server.xcursor_mgr ?
|
|
wlr_xcursor_manager_get_xcursor(server.xcursor_mgr, "default", 1)
|
|
: NULL;
|
|
if (cur && strcmp(theme, "default") != 0 &&
|
|
!wl_list_empty(&server.xcursor_mgr->scaled_themes)) {
|
|
struct wlr_xcursor_manager_theme *mt = wl_container_of(
|
|
server.xcursor_mgr->scaled_themes.next, mt, link);
|
|
if (mt->theme && mt->theme->name &&
|
|
strcmp(mt->theme->name, theme) != 0)
|
|
kick_log(KICK_LOG_INFO,
|
|
"[cursor] theme '%s' not found; wlroots builtin cursor "
|
|
"in use\n", theme);
|
|
}
|
|
if (!cur) {
|
|
kick_log(KICK_LOG_INFO,
|
|
"[cursor] no cursor theme available; no pointer shown\n");
|
|
} else {
|
|
wlr_cursor_set_xcursor(server.cursor, server.xcursor_mgr, "default");
|
|
wl_signal_add(&server.cursor->events.motion,
|
|
&standalone_cursor_motion_listener);
|
|
}
|
|
}
|
|
|
|
/* virtual keyboard fed from the host keyboard */
|
|
static const struct wlr_keyboard_impl keyboard_impl = {
|
|
.name = "kick-keyboard",
|
|
};
|
|
wlr_keyboard_init(&server.keyboard, &keyboard_impl, "kick-keyboard");
|
|
|
|
/* fallback keymap until the host sends one */
|
|
server.xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
|
if (server.xkb_ctx) {
|
|
xkb_context_set_log_level(server.xkb_ctx, XKB_LOG_LEVEL_DEBUG);
|
|
xkb_context_set_log_fn(server.xkb_ctx, kick_xkb_log);
|
|
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);
|
|
xkb_state_reload();
|
|
}
|
|
}
|
|
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);
|
|
server.new_popup.notify = handle_new_popup;
|
|
wl_signal_add(&server.xdg_shell->events.new_popup, &server.new_popup);
|
|
|
|
/* Standalone: attach the native (libinput) input devices to the seat. */
|
|
if (server.standalone) {
|
|
server.new_input.notify = handle_new_input;
|
|
wl_signal_add(&server.backend->events.new_input, &server.new_input);
|
|
}
|
|
|
|
/* The headless backend (nested always, and standalone under
|
|
WLR_BACKENDS=headless when no real display is available) has no
|
|
outputs of its own, so add ours manually; DRM emits its outputs via
|
|
new_output. new_output fires during backend start. */
|
|
if (!server.standalone || server.standalone_headless) {
|
|
int w = server.standalone && mode_req_w > 0 ? mode_req_w : 1280;
|
|
int h = server.standalone && mode_req_h > 0 ? mode_req_h : 800;
|
|
host.width = w;
|
|
host.height = h;
|
|
wlr_headless_add_output(server.backend, host.width, host.height);
|
|
}
|
|
|
|
if (!wlr_backend_start(server.backend)) {
|
|
fprintf(stderr, "kick: failed to start backend\n");
|
|
return 1;
|
|
}
|
|
|
|
if (wl_display_add_socket(server.wl_display, kick_socket) < 0) {
|
|
fprintf(stderr, "kick: failed to add socket '%s': %s\n",
|
|
kick_socket, strerror(errno));
|
|
return 1;
|
|
}
|
|
|
|
/* Standalone mode presents onto the native output directly; nested mode
|
|
connects to the host compositor and presents inside it. */
|
|
if (!server.standalone && !host_init()) {
|
|
fprintf(stderr, "kick: could not start nested session\n");
|
|
remove_socket_file();
|
|
return 1;
|
|
}
|
|
|
|
kick_log(KICK_LOG_INFO, "Wayland socket: %s\n", kick_socket);
|
|
|
|
server.apps = calloc((size_t)total_apps, sizeof(*server.apps));
|
|
if (!server.apps) {
|
|
fprintf(stderr, "kick: out of memory for %d apps\n",
|
|
total_apps);
|
|
remove_socket_file();
|
|
return 1;
|
|
}
|
|
server.apps_cap = total_apps;
|
|
|
|
for (int i = 0; i < total_apps; i++) {
|
|
struct kick_app *app = calloc(1, sizeof(*app));
|
|
if (!app) {
|
|
remove_socket_file();
|
|
return 1;
|
|
}
|
|
app->cmd = strdup(default_tab ? term_cmd : appv[i]);
|
|
app->pid = -1;
|
|
app->origin = ORIGIN_CLI;
|
|
/* Live-but-black until the command claims the slot with a toplevel. */
|
|
app->pending_claim = true;
|
|
wl_list_init(&app->sub_windows);
|
|
server.apps[i] = app;
|
|
}
|
|
server.num_apps = total_apps;
|
|
|
|
for (int i = 0; i < server.num_apps; i++) {
|
|
if (server.apps[i]->cmd[0] == '-') {
|
|
if (i > 0) {
|
|
fprintf(stderr,
|
|
"kick: warning: %s starts with '-'; it is "
|
|
"run as its own tab, not an option to %s\n"
|
|
" If intended, quote it: \"%s %s\"\n",
|
|
server.apps[i]->cmd, server.apps[i - 1]->cmd,
|
|
server.apps[i - 1]->cmd, server.apps[i]->cmd);
|
|
} else {
|
|
fprintf(stderr,
|
|
"kick: warning: %s starts with '-'; if this "
|
|
"is meant for a first command, quote them together, "
|
|
"e.g. \"%s\"\n",
|
|
server.apps[i]->cmd, server.apps[i]->cmd);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* The selected command-line command is active from the very first frame:
|
|
black, titled by its command line, until the client maps. The user
|
|
waits only on that one tab. --start-cmd-index picks it (1-based);
|
|
without the flag the last command is the one shown, so the startup tab
|
|
is always the one the caller can count on. */
|
|
int focus_idx = start_cmd_index >= 0 ? start_cmd_index : total_apps - 1;
|
|
set_active_app(server.apps[focus_idx]);
|
|
|
|
/* Fork every command-line command at once; the process-group match in
|
|
handle_new_toplevel gives each its own slot on arrival. */
|
|
launch_all_cli();
|
|
|
|
const char *prefix = tab_prefix == PREFIX_SUPER ? "Super" : "Alt";
|
|
kick_log(KICK_LOG_INFO, "kick: running. "
|
|
"%s+Up/Down switches tabs; %s+Shift+Up/Down reorders; "
|
|
"%s+Delete closes the active tab; "
|
|
"%s+Shift+T opens a terminal (%s).\n",
|
|
prefix, prefix, prefix, prefix, term_cmd);
|
|
|
|
/* Standalone event loop: no host display, so drive the server loop alone.
|
|
The native output's frame signal (or the headless 60Hz timer) wakes the
|
|
dispatcher and triggers a render. The loop fd is polled directly and
|
|
dispatched with timeout 0: dispatch(-1) would re-poll inside libwayland
|
|
after draining the SIGUSR1 self-pipe and never return, leaving the gate
|
|
below unevaluated on an idle screen. */
|
|
if (server.standalone) {
|
|
int server_loop_fd = wl_event_loop_get_fd(server.loop);
|
|
for (;;) {
|
|
struct pollfd fds[1] = {
|
|
{ .fd = server_loop_fd, .events = POLLIN },
|
|
};
|
|
int ret;
|
|
do {
|
|
ret = poll(fds, 1, -1);
|
|
} while (ret < 0 && errno == EINTR);
|
|
if (ret < 0)
|
|
continue;
|
|
|
|
do {
|
|
ret = wl_event_loop_dispatch(server.loop, 0);
|
|
} while (ret == -1 && errno == EINTR);
|
|
wl_display_flush_clients(server.wl_display);
|
|
sweep_dead_startup();
|
|
if (server.output && (wlr_output_is_headless(server.output)
|
|
|| !server.output->frame_pending)
|
|
&& (needs_redraw || server.output->needs_frame)) {
|
|
frame_pending = false;
|
|
needs_redraw = false;
|
|
render_and_present();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Nested event loop: service the server, the host display, and libdecor
|
|
(whose decoration plugin runs its own event handling). */
|
|
int server_fd = wl_event_loop_get_fd(server.loop);
|
|
int host_fd = wl_display_get_fd(host.display);
|
|
int decor_fd = host.libdecor ? libdecor_get_fd(host.libdecor) : -1;
|
|
|
|
for (;;) {
|
|
wl_display_dispatch_pending(host.display);
|
|
libdecor_dispatch(host.libdecor, 0);
|
|
/* Flush queued nested-client messages before blocking: a data-control
|
|
`send` queued while dispatching the host side would otherwise sit
|
|
in the client connection until some later event wakes the poll
|
|
below, stalling host-pasted clipboard data for seconds. */
|
|
wl_display_flush_clients(server.wl_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[3] = {
|
|
{ .fd = server_fd, .events = POLLIN },
|
|
{ .fd = host_fd, .events = POLLIN },
|
|
{ .fd = decor_fd, .events = POLLIN },
|
|
};
|
|
int nfds = decor_fd >= 0 ? 3 : 2;
|
|
int ret;
|
|
do {
|
|
ret = poll(fds, nfds, -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);
|
|
sweep_dead_startup();
|
|
wl_display_dispatch_pending(host.display);
|
|
libdecor_dispatch(host.libdecor, 0);
|
|
|
|
/* Render only when something changed (kick's own redraws or scene
|
|
damage) and no page flip is in flight; an idle screen never
|
|
repaints. wl_scene schedules its own frame wake on client
|
|
damage, so a dirty scene always gets here. */
|
|
if (server.output && (wlr_output_is_headless(server.output)
|
|
|| !server.output->frame_pending)
|
|
&& (needs_redraw || server.output->needs_frame)) {
|
|
frame_pending = false;
|
|
needs_redraw = false;
|
|
render_and_present();
|
|
}
|
|
wl_display_flush(host.display);
|
|
}
|
|
|
|
/* Unreachable in practice: every shutdown path exits via
|
|
shutdown_cleanup(). Kept for completeness. */
|
|
shutdown_cleanup();
|
|
return 0;
|
|
}
|