From 8328c05041998d226620aa4d27e56366b385d933 Mon Sep 17 00:00:00 2001 From: stormshadow <190884359+st0rm-shad0w@users.noreply.github.com> Date: Wed, 15 Apr 2026 23:25:06 +0530 Subject: [PATCH] Add swaymsg-compatible IPC interface with labmsg client --- clients/labmsg.c | 389 +++++++ clients/meson.build | 8 + include/action.h | 1 + include/config/rcxml.h | 1 + include/foreign-toplevel/foreign.h | 1 + include/ipc.h | 76 ++ include/labwc.h | 21 +- include/view.h | 6 + meson.build | 2 + src/action.c | 2 +- src/desktop.c | 3 + src/foreign-toplevel/foreign.c | 18 +- src/ipc.c | 1713 ++++++++++++++++++++++++++++ src/meson.build | 1 + src/output.c | 2 + src/server.c | 5 + src/view.c | 40 + src/workspaces.c | 3 + src/xdg.c | 5 + src/xwayland.c | 5 + 20 files changed, 2291 insertions(+), 11 deletions(-) create mode 100644 clients/labmsg.c create mode 100644 include/ipc.h create mode 100644 src/ipc.c diff --git a/clients/labmsg.c b/clients/labmsg.c new file mode 100644 index 00000000..ec7e116d --- /dev/null +++ b/clients/labmsg.c @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * labmsg - IPC client for labwc (swaymsg-compatible interface) + */ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define IPC_MAGIC "labwc-ipc" +#define IPC_MAGIC_LEN 9 +#define IPC_HEADER_SIZE (IPC_MAGIC_LEN + 4 + 4) + +/* Message types */ +enum ipc_msg_type { + IPC_RUN_COMMAND = 0, + IPC_GET_WORKSPACES = 1, + IPC_SUBSCRIBE = 2, + IPC_GET_OUTPUTS = 3, + IPC_GET_TREE = 4, + IPC_GET_BAR_CONFIG = 6, + IPC_GET_VERSION = 7, + IPC_GET_CONFIG = 9, + IPC_SEND_TICK = 10, + IPC_SYNC = 11, + IPC_GET_INPUTS = 100, + IPC_GET_SEATS = 101, +}; + +static const char *version_str = "labmsg 0.1"; + +static const struct option long_options[] = {{"help", no_argument, NULL, 'h'}, + {"monitor", no_argument, NULL, 'm'}, {"pretty", no_argument, NULL, 'p'}, + {"quiet", no_argument, NULL, 'q'}, {"raw", no_argument, NULL, 'r'}, + {"socket", required_argument, NULL, 's'}, + {"type", required_argument, NULL, 't'}, + {"version", no_argument, NULL, 'v'}, {0, 0, 0, 0}}; + +static const char usage_str[] = + "Usage: labmsg [options] [message]\n" + "\n" + "Options:\n" + " -h, --help Show help and exit\n" + " -m, --monitor Monitor for events (subscribe mode)\n" + " -p, --pretty Force pretty-printed JSON output\n" + " -q, --quiet Suppress response output\n" + " -r, --raw Force raw JSON output\n" + " -s, --socket Override socket path\n" + " -t, --type Message type (default: run_command)\n" + " -v, --version Show version and exit\n" + "\n" + "Message types:\n" + " run_command, get_workspaces, subscribe, get_outputs,\n" + " get_tree, get_bar_config, get_version, get_config,\n" + " send_tick, get_inputs, get_seats\n"; + +static int +parse_msg_type(const char *name) +{ + if (!name || !strcmp(name, "run_command")) { + return IPC_RUN_COMMAND; + } else if (!strcmp(name, "get_workspaces")) { + return IPC_GET_WORKSPACES; + } else if (!strcmp(name, "subscribe")) { + return IPC_SUBSCRIBE; + } else if (!strcmp(name, "get_outputs")) { + return IPC_GET_OUTPUTS; + } else if (!strcmp(name, "get_tree")) { + return IPC_GET_TREE; + } else if (!strcmp(name, "get_bar_config")) { + return IPC_GET_BAR_CONFIG; + } else if (!strcmp(name, "get_version")) { + return IPC_GET_VERSION; + } else if (!strcmp(name, "get_config")) { + return IPC_GET_CONFIG; + } else if (!strcmp(name, "send_tick")) { + return IPC_SEND_TICK; + } else if (!strcmp(name, "get_inputs")) { + return IPC_GET_INPUTS; + } else if (!strcmp(name, "get_seats")) { + return IPC_GET_SEATS; + } + fprintf(stderr, "Unknown message type: %s\n", name); + return -1; +} + +static int +ipc_connect(const char *socket_path) +{ + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { + perror("socket"); + return -1; + } + + struct sockaddr_un addr = {0}; + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("connect"); + close(fd); + return -1; + } + return fd; +} + +static bool +ipc_send(int fd, uint32_t type, const char *payload, uint32_t len) +{ + char header[IPC_HEADER_SIZE]; + memcpy(header, IPC_MAGIC, IPC_MAGIC_LEN); + memcpy(header + IPC_MAGIC_LEN, &len, sizeof(uint32_t)); + memcpy(header + IPC_MAGIC_LEN + 4, &type, sizeof(uint32_t)); + + if (write(fd, header, IPC_HEADER_SIZE) != IPC_HEADER_SIZE) { + return false; + } + if (len > 0 && write(fd, payload, len) != (ssize_t)len) { + return false; + } + return true; +} + +static bool +read_exact(int fd, void *buf, size_t count) +{ + size_t total = 0; + while (total < count) { + ssize_t n = read(fd, (char *)buf + total, count - total); + if (n <= 0) { + return false; + } + total += n; + } + return true; +} + +static bool +ipc_recv(int fd, uint32_t *type, char **payload, uint32_t *len) +{ + char header[IPC_HEADER_SIZE]; + if (!read_exact(fd, header, IPC_HEADER_SIZE)) { + return false; + } + + if (memcmp(header, IPC_MAGIC, IPC_MAGIC_LEN) != 0) { + fprintf(stderr, "Invalid IPC response magic\n"); + return false; + } + + memcpy(len, header + IPC_MAGIC_LEN, sizeof(uint32_t)); + memcpy(type, header + IPC_MAGIC_LEN + 4, sizeof(uint32_t)); + + if (*len > 0) { + *payload = malloc(*len + 1); + if (!*payload) { + return false; + } + if (!read_exact(fd, *payload, *len)) { + free(*payload); + *payload = NULL; + return false; + } + (*payload)[*len] = '\0'; + } else { + *payload = NULL; + } + return true; +} + +static void +print_json(const char *data, bool pretty) +{ + struct json_object *obj = json_tokener_parse(data); + if (!obj) { + /* Not valid JSON, print as-is */ + printf("%s\n", data); + return; + } + + int flags = pretty ? (JSON_C_TO_STRING_PRETTY | JSON_C_TO_STRING_SPACED) + : JSON_C_TO_STRING_PLAIN; + + printf("%s\n", json_object_to_json_string_ext(obj, flags)); + json_object_put(obj); +} + +/* Check if any command in the result array failed */ +static bool +check_command_success(const char *data) +{ + struct json_object *obj = json_tokener_parse(data); + if (!obj) { + return false; + } + + bool success = true; + if (json_object_get_type(obj) == json_type_array) { + int len = json_object_array_length(obj); + for (int i = 0; i < len; i++) { + struct json_object *item = + json_object_array_get_idx(obj, i); + struct json_object *s = NULL; + if (json_object_object_get_ex(item, "success", &s)) { + if (!json_object_get_boolean(s)) { + success = false; + /* Print error to stderr */ + struct json_object *err = NULL; + if (json_object_object_get_ex(item, + "error", &err)) { + fprintf(stderr, "Error: %s\n", + json_object_get_string( + err)); + } + } + } + } + } else if (json_object_get_type(obj) == json_type_object) { + struct json_object *s = NULL; + if (json_object_object_get_ex(obj, "success", &s)) { + success = json_object_get_boolean(s); + } + } + + json_object_put(obj); + return success; +} + +int +main(int argc, char *argv[]) +{ + setvbuf(stdout, NULL, _IOLBF, 0); + + bool monitor = false; + bool pretty = false; + bool raw = false; + bool quiet = false; + const char *socket_path = NULL; + const char *type_str = NULL; + bool force_pretty = false; + + int c; + while (1) { + int index = 0; + c = getopt_long(argc, argv, "hmpqrs:t:v", long_options, &index); + if (c == -1) { + break; + } + switch (c) { + case 'h': + printf("%s", usage_str); + return 0; + case 'm': + monitor = true; + break; + case 'p': + pretty = true; + force_pretty = true; + break; + case 'q': + quiet = true; + break; + case 'r': + raw = true; + break; + case 's': + socket_path = optarg; + break; + case 't': + type_str = optarg; + break; + case 'v': + printf("%s\n", version_str); + return 0; + default: + fprintf(stderr, "%s", usage_str); + return 1; + } + } + + /* Auto-detect pretty mode */ + if (!force_pretty && !raw) { + pretty = isatty(STDOUT_FILENO); + } + + /* Determine message type */ + int msg_type = parse_msg_type(type_str); + if (msg_type < 0) { + return 1; + } + + /* Build payload from remaining args */ + char *payload = NULL; + if (optind < argc) { + /* Concatenate remaining args with spaces */ + size_t total = 0; + for (int i = optind; i < argc; i++) { + total += strlen(argv[i]) + 1; + } + payload = malloc(total); + payload[0] = '\0'; + for (int i = optind; i < argc; i++) { + if (i > optind) { + strcat(payload, " "); + } + strcat(payload, argv[i]); + } + } + + /* Get socket path */ + if (!socket_path) { + socket_path = getenv("LABWC_IPC_SOCK"); + } + if (!socket_path) { + fprintf(stderr, "LABWC_IPC_SOCK not set and no -s option\n"); + free(payload); + return 1; + } + + int fd = ipc_connect(socket_path); + if (fd < 0) { + fprintf(stderr, "Failed to connect to %s\n", socket_path); + free(payload); + return 1; + } + + /* Send message */ + uint32_t payload_len = payload ? strlen(payload) : 0; + if (!ipc_send(fd, msg_type, payload, payload_len)) { + fprintf(stderr, "Failed to send IPC message\n"); + close(fd); + free(payload); + return 1; + } + free(payload); + + /* Receive response */ + uint32_t resp_type = 0; + char *resp_payload = NULL; + uint32_t resp_len = 0; + + if (!ipc_recv(fd, &resp_type, &resp_payload, &resp_len)) { + fprintf(stderr, "Failed to receive IPC response\n"); + close(fd); + return 1; + } + + int exit_code = 0; + + if (!quiet && resp_payload) { + print_json(resp_payload, pretty); + } + + /* Check for server-reported errors */ + if (resp_payload && msg_type == IPC_RUN_COMMAND) { + if (!check_command_success(resp_payload)) { + exit_code = 2; + } + } + + /* Monitor mode: keep reading events */ + if (monitor) { + free(resp_payload); + while (1) { + resp_payload = NULL; + if (!ipc_recv(fd, &resp_type, &resp_payload, + &resp_len)) { + break; + } + if (!quiet && resp_payload) { + print_json(resp_payload, pretty); + } + free(resp_payload); + } + } + + free(resp_payload); + close(fd); + return exit_code; +} diff --git a/clients/meson.build b/clients/meson.build index 55a4c0e5..5b9be2e9 100644 --- a/clients/meson.build +++ b/clients/meson.build @@ -59,3 +59,11 @@ endif clients = files('lab-sensible-terminal') install_data(clients, install_dir: get_option('bindir')) + +jsonc_client = dependency('json-c') +executable( + 'labmsg', + files('labmsg.c'), + dependencies: [jsonc_client], + install: true, +) diff --git a/include/action.h b/include/action.h index b09aa35c..5775b2a8 100644 --- a/include/action.h +++ b/include/action.h @@ -29,6 +29,7 @@ bool action_is_valid(struct action *action); bool action_is_show_menu(struct action *action); void action_arg_add_str(struct action *action, const char *key, const char *value); +void action_arg_add_int(struct action *action, const char *key, int value); void action_arg_add_actionlist(struct action *action, const char *key); void action_arg_add_querylist(struct action *action, const char *key); diff --git a/include/config/rcxml.h b/include/config/rcxml.h index 9c2183a8..0722f2dd 100644 --- a/include/config/rcxml.h +++ b/include/config/rcxml.h @@ -73,6 +73,7 @@ struct rcxml { char *config_dir; char *config_file; bool merge_config; + char *loaded_config_file; /* core */ bool xdg_shell_server_side_deco; diff --git a/include/foreign-toplevel/foreign.h b/include/foreign-toplevel/foreign.h index bc226776..46a80684 100644 --- a/include/foreign-toplevel/foreign.h +++ b/include/foreign-toplevel/foreign.h @@ -9,5 +9,6 @@ struct foreign_toplevel *foreign_toplevel_create(struct view *view); void foreign_toplevel_set_parent(struct foreign_toplevel *toplevel, struct foreign_toplevel *parent); void foreign_toplevel_destroy(struct foreign_toplevel *toplevel); +const char *foreign_toplevel_get_identifier(struct foreign_toplevel *toplevel); #endif /* LABWC_FOREIGN_TOPLEVEL_H */ diff --git a/include/ipc.h b/include/ipc.h new file mode 100644 index 00000000..eb69f1db --- /dev/null +++ b/include/ipc.h @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef LABWC_IPC_H +#define LABWC_IPC_H + +#include +#include +#include + +struct view; +struct workspace; + +/* Wire protocol */ +#define IPC_MAGIC "labwc-ipc" +#define IPC_MAGIC_LEN 9 +#define IPC_HEADER_SIZE (IPC_MAGIC_LEN + 4 + 4) /* 17 bytes */ + +/* Message types (same numbering as sway/i3) */ +enum ipc_msg_type { + IPC_RUN_COMMAND = 0, + IPC_GET_WORKSPACES = 1, + IPC_SUBSCRIBE = 2, + IPC_GET_OUTPUTS = 3, + IPC_GET_TREE = 4, + IPC_GET_MARKS = 5, + IPC_GET_BAR_CONFIG = 6, + IPC_GET_VERSION = 7, + IPC_GET_BINDING_MODES = 8, + IPC_GET_CONFIG = 9, + IPC_SEND_TICK = 10, + IPC_SYNC = 11, + IPC_GET_BINDING_STATE = 12, + IPC_GET_INPUTS = 100, + IPC_GET_SEATS = 101, +}; + +/* Event types (bit 31 set) */ +#define IPC_EVENT_FLAG 0x80000000 +enum ipc_event_type { + IPC_EVENT_WORKSPACE = (IPC_EVENT_FLAG | 0), + IPC_EVENT_OUTPUT = (IPC_EVENT_FLAG | 1), + IPC_EVENT_WINDOW = (IPC_EVENT_FLAG | 3), + IPC_EVENT_SHUTDOWN = (IPC_EVENT_FLAG | 6), + IPC_EVENT_TICK = (IPC_EVENT_FLAG | 7), +}; + +/* Subscription bitmask */ +#define IPC_SUB_WORKSPACE (1 << 0) +#define IPC_SUB_OUTPUT (1 << 1) +#define IPC_SUB_WINDOW (1 << 3) +#define IPC_SUB_SHUTDOWN (1 << 6) +#define IPC_SUB_TICK (1 << 7) + +struct ipc_client { + struct wl_list link; /* server.ipc_clients */ + int fd; + struct wl_event_source *readable; + struct wl_event_source *writable; + uint32_t subscriptions; + /* Write buffer for partial writes */ + char *write_buf; + size_t write_buf_len; + size_t write_buf_cap; +}; + +/* Server lifecycle */ +void ipc_init(void); +void ipc_finish(void); + +/* Event emitters (called from compositor hooks) */ +void ipc_event_workspace(const char *change, struct workspace *current, + struct workspace *old); +void ipc_event_output(const char *change); +void ipc_event_window(const char *change, struct view *view); +void ipc_event_shutdown(void); + +#endif /* LABWC_IPC_H */ \ No newline at end of file diff --git a/include/labwc.h b/include/labwc.h index 87a42198..e6f646b7 100644 --- a/include/labwc.h +++ b/include/labwc.h @@ -1,10 +1,10 @@ /* SPDX-License-Identifier: GPL-2.0-only */ #ifndef LABWC_H #define LABWC_H -#include "config.h" #include #include #include "common/set.h" +#include "config.h" #include "cycle.h" #include "input/cursor.h" #include "overlay.h" @@ -65,9 +65,9 @@ struct seat { struct input_method_relay *input_method_relay; /** - * Cursor context saved when a mouse button is pressed on a view/surface. - * It is used to send cursor motion events to a surface even though - * the cursor has left the surface in the meantime. + * Cursor context saved when a mouse button is pressed on a + * view/surface. It is used to send cursor motion events to a surface + * even though the cursor has left the surface in the meantime. * * This allows to keep dragging a scrollbar or selecting text even * when moving outside of the window. @@ -245,7 +245,8 @@ struct server { */ struct wlr_scene_tree *xdg_popup_tree; #if HAVE_XWAYLAND - /* Tree for unmanaged xsurfaces without initialized view (usually popups) */ + /* Tree for unmanaged xsurfaces without initialized view (usually + * popups) */ struct wlr_scene_tree *unmanaged_tree; #endif struct wlr_scene_tree *cycle_preview_tree; @@ -254,7 +255,7 @@ struct server { /* Workspaces */ struct { - struct wl_list all; /* struct workspace.link */ + struct wl_list all; /* struct workspace.link */ struct workspace *current; struct workspace *last; struct wlr_ext_workspace_manager_v1 *ext_manager; @@ -324,6 +325,8 @@ struct server { pid_t primary_client_pid; char *title_fmt; + + struct wl_list ipc_clients; }; /* defined in main.c */ @@ -406,7 +409,8 @@ void seat_pointer_end_grab(struct seat *seat, struct wlr_surface *surface); */ void seat_focus_lock_surface(struct seat *seat, struct wlr_surface *surface); -void seat_set_focus_layer(struct seat *seat, struct wlr_layer_surface_v1 *layer); +void seat_set_focus_layer(struct seat *seat, + struct wlr_layer_surface_v1 *layer); void seat_output_layout_changed(struct seat *seat); /* @@ -457,7 +461,6 @@ void server_start(void); void server_finish(void); void create_constraint(struct wl_listener *listener, void *data); -void constrain_cursor(struct wlr_pointer_constraint_v1 - *constraint); +void constrain_cursor(struct wlr_pointer_constraint_v1 *constraint); #endif /* LABWC_H */ diff --git a/include/view.h b/include/view.h index 46943ab2..d33b733d 100644 --- a/include/view.h +++ b/include/view.h @@ -242,6 +242,12 @@ struct view { /* Set temporarily when moving view due to layout change */ bool adjusting_for_layout_change; + /* + * Last geometry reported to IPC subscribers. Used to detect + * actual position/size changes and emit move/resize events. + */ + struct wlr_box ipc_last_geo; + /* used by xdg-shell views */ uint32_t pending_configure_serial; struct wl_event_source *pending_configure_timeout; diff --git a/meson.build b/meson.build index 22dc5bdc..3f41fc41 100644 --- a/meson.build +++ b/meson.build @@ -75,6 +75,7 @@ input = dependency('libinput', version: '>=1.26', required: wlroots.get_variable pixman = dependency('pixman-1') math = cc.find_library('m') png = dependency('libpng') +jsonc = dependency('json-c') svg = dependency('librsvg-2.0', version: '>=2.46', required: false) sfdo_basedir = dependency( 'libsfdo-basedir', @@ -174,6 +175,7 @@ labwc_deps = [ pixman, math, png, + jsonc, ] if have_rsvg labwc_deps += [ diff --git a/src/action.c b/src/action.c index 265ff6e7..e7197168 100644 --- a/src/action.c +++ b/src/action.c @@ -203,7 +203,7 @@ action_arg_add_bool(struct action *action, const char *key, bool value) wl_list_append(&action->args, &arg->base.link); } -static void +void action_arg_add_int(struct action *action, const char *key, int value) { assert(action); diff --git a/src/desktop.c b/src/desktop.c index 57ef9e3c..d312e8aa 100644 --- a/src/desktop.c +++ b/src/desktop.c @@ -12,6 +12,7 @@ #include "config/rcxml.h" #include "dnd.h" #include "labwc.h" +#include "ipc.h" #include "layers.h" #include "node.h" #include "output.h" @@ -168,6 +169,8 @@ desktop_focus_view_internal(struct view *view, bool raise, bool allow_delay) struct view *dialog = view_get_modal_dialog(view); set_or_offer_focus(dialog ? dialog : view); + ipc_event_window("focus", view); + show_desktop_reset(); } diff --git a/src/foreign-toplevel/foreign.c b/src/foreign-toplevel/foreign.c index 4290ffd0..a36cb485 100644 --- a/src/foreign-toplevel/foreign.c +++ b/src/foreign-toplevel/foreign.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-only #include "foreign-toplevel/foreign.h" #include +#include #include "common/mem.h" #include "foreign-toplevel/ext-foreign.h" #include "foreign-toplevel/wlr-foreign.h" @@ -27,7 +28,8 @@ foreign_toplevel_create(struct view *view) } void -foreign_toplevel_set_parent(struct foreign_toplevel *toplevel, struct foreign_toplevel *parent) +foreign_toplevel_set_parent(struct foreign_toplevel *toplevel, + struct foreign_toplevel *parent) { assert(toplevel); wlr_foreign_toplevel_set_parent(&toplevel->wlr_toplevel, @@ -42,3 +44,17 @@ foreign_toplevel_destroy(struct foreign_toplevel *toplevel) ext_foreign_toplevel_finish(&toplevel->ext_toplevel); free(toplevel); } + +const char * +foreign_toplevel_get_identifier(struct foreign_toplevel *toplevel) +{ + if (!toplevel) { + return ""; + } + struct wlr_ext_foreign_toplevel_handle_v1 *handle = + toplevel->ext_toplevel.handle; + if (handle && handle->identifier) { + return handle->identifier; + } + return ""; +} diff --git a/src/ipc.c b/src/ipc.c new file mode 100644 index 00000000..0ab1fa4b --- /dev/null +++ b/src/ipc.c @@ -0,0 +1,1713 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * ipc.c - labwc IPC server (sway-compatible protocol) + * + * Implements: + * - UNIX domain socket with "labwc-ipc" wire protocol + * - All message types 0-12 and 100-101 + * - Core events: workspace, output, window, shutdown, tick + * - Sway-compatible command parser for RUN_COMMAND + */ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "config.h" + +#if HAVE_XWAYLAND +#include +#endif + +#include +#include "action.h" +#include "common/buf.h" +#include "common/mem.h" +#include "config/rcxml.h" +#include "foreign-toplevel/foreign.h" +#include "input/input.h" +#include "ipc.h" +#include "labwc.h" +#include "output.h" +#include "view.h" +#include "workspaces.h" + +/* =================================================================== + * Section 1: Protocol & Client Management + * =================================================================== */ + +static int ipc_socket_fd = -1; +static char *ipc_socket_path; +static struct wl_event_source *ipc_event_source; + +static void ipc_client_disconnect(struct ipc_client *client); +static void ipc_handle_message(struct ipc_client *client, uint32_t type, + const char *payload, uint32_t len); + +static void +ipc_send_reply(struct ipc_client *client, uint32_t type, const char *payload, + uint32_t len) +{ + char header[IPC_HEADER_SIZE]; + memcpy(header, IPC_MAGIC, IPC_MAGIC_LEN); + memcpy(header + IPC_MAGIC_LEN, &len, sizeof(uint32_t)); + memcpy(header + IPC_MAGIC_LEN + 4, &type, sizeof(uint32_t)); + + size_t total = IPC_HEADER_SIZE + len; + size_t old_len = client->write_buf_len; + size_t new_len = old_len + total; + + if (new_len > client->write_buf_cap) { + size_t new_cap = new_len * 2; + if (new_cap < 4096) { + new_cap = 4096; + } + client->write_buf = realloc(client->write_buf, new_cap); + client->write_buf_cap = new_cap; + } + memcpy(client->write_buf + old_len, header, IPC_HEADER_SIZE); + if (len > 0) { + memcpy(client->write_buf + old_len + IPC_HEADER_SIZE, payload, + len); + } + client->write_buf_len = new_len; + + /* Try to flush immediately */ + while (client->write_buf_len > 0) { + ssize_t n = write(client->fd, client->write_buf, + client->write_buf_len); + if (n < 0) { + if (errno == EAGAIN || errno == EINTR) { + break; + } + ipc_client_disconnect(client); + return; + } + memmove(client->write_buf, client->write_buf + n, + client->write_buf_len - n); + client->write_buf_len -= n; + } +} + +static void +ipc_send_reply_json(struct ipc_client *client, uint32_t type, + struct json_object *obj) +{ + const char *str = json_object_to_json_string(obj); + ipc_send_reply(client, type, str, strlen(str)); + json_object_put(obj); +} + +static int +handle_client_readable(int fd, uint32_t mask, void *data) +{ + struct ipc_client *client = data; + + if (mask & (WL_EVENT_HANGUP | WL_EVENT_ERROR)) { + ipc_client_disconnect(client); + return 0; + } + + /* Read header */ + char header[IPC_HEADER_SIZE]; + size_t total_read = 0; + while (total_read < IPC_HEADER_SIZE) { + ssize_t n = read(fd, header + total_read, + IPC_HEADER_SIZE - total_read); + if (n <= 0) { + ipc_client_disconnect(client); + return 0; + } + total_read += n; + } + + /* Validate magic */ + if (memcmp(header, IPC_MAGIC, IPC_MAGIC_LEN) != 0) { + wlr_log(WLR_ERROR, "IPC: invalid magic from client"); + ipc_client_disconnect(client); + return 0; + } + + uint32_t payload_len, msg_type; + memcpy(&payload_len, header + IPC_MAGIC_LEN, sizeof(uint32_t)); + memcpy(&msg_type, header + IPC_MAGIC_LEN + 4, sizeof(uint32_t)); + + /* Sanity limit: 1MB */ + if (payload_len > 1024 * 1024) { + wlr_log(WLR_ERROR, "IPC: message too large (%u bytes)", + payload_len); + ipc_client_disconnect(client); + return 0; + } + + char *payload = NULL; + if (payload_len > 0) { + payload = malloc(payload_len + 1); + if (!payload) { + ipc_client_disconnect(client); + return 0; + } + size_t read_so_far = 0; + while (read_so_far < payload_len) { + ssize_t n = read(fd, payload + read_so_far, + payload_len - read_so_far); + if (n <= 0) { + free(payload); + ipc_client_disconnect(client); + return 0; + } + read_so_far += n; + } + payload[payload_len] = '\0'; + } + + ipc_handle_message(client, msg_type, payload, payload_len); + free(payload); + return 0; +} + +static int +handle_new_connection(int fd, uint32_t mask, void *data) +{ + int client_fd = accept(fd, NULL, NULL); + if (client_fd < 0) { + wlr_log_errno(WLR_ERROR, "IPC: accept failed"); + return 0; + } + + int flags = fcntl(client_fd, F_GETFD); + if (flags >= 0) { + fcntl(client_fd, F_SETFD, flags | FD_CLOEXEC); + } + + struct ipc_client *client = znew(*client); + client->fd = client_fd; + client->readable = wl_event_loop_add_fd(server.wl_event_loop, client_fd, + WL_EVENT_READABLE, handle_client_readable, client); + wl_list_insert(&server.ipc_clients, &client->link); + + return 0; +} + +static void +ipc_client_disconnect(struct ipc_client *client) +{ + wl_event_source_remove(client->readable); + if (client->writable) { + wl_event_source_remove(client->writable); + } + close(client->fd); + wl_list_remove(&client->link); + free(client->write_buf); + free(client); +} + +void +ipc_init(void) +{ + wl_list_init(&server.ipc_clients); + + ipc_socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (ipc_socket_fd < 0) { + wlr_log_errno(WLR_ERROR, "IPC: failed to create socket"); + return; + } + + int flags = fcntl(ipc_socket_fd, F_GETFD); + if (flags >= 0) { + fcntl(ipc_socket_fd, F_SETFD, flags | FD_CLOEXEC); + } + + const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); + if (!runtime_dir) { + wlr_log(WLR_ERROR, "IPC: XDG_RUNTIME_DIR not set"); + close(ipc_socket_fd); + ipc_socket_fd = -1; + return; + } + + int path_len = snprintf(NULL, 0, "%s/labwc-ipc.%d.sock", runtime_dir, + getpid()); + ipc_socket_path = malloc(path_len + 1); + snprintf(ipc_socket_path, path_len + 1, "%s/labwc-ipc.%d.sock", + runtime_dir, getpid()); + + /* Remove stale socket */ + unlink(ipc_socket_path); + + struct sockaddr_un addr = {0}; + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, ipc_socket_path, sizeof(addr.sun_path) - 1); + + if (bind(ipc_socket_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + wlr_log_errno(WLR_ERROR, "IPC: bind failed on %s", + ipc_socket_path); + close(ipc_socket_fd); + ipc_socket_fd = -1; + free(ipc_socket_path); + ipc_socket_path = NULL; + return; + } + + chmod(ipc_socket_path, 0600); + + if (listen(ipc_socket_fd, 16) < 0) { + wlr_log_errno(WLR_ERROR, "IPC: listen failed"); + close(ipc_socket_fd); + ipc_socket_fd = -1; + unlink(ipc_socket_path); + free(ipc_socket_path); + ipc_socket_path = NULL; + return; + } + + ipc_event_source = wl_event_loop_add_fd(server.wl_event_loop, + ipc_socket_fd, WL_EVENT_READABLE, handle_new_connection, NULL); + + setenv("LABWC_IPC_SOCK", ipc_socket_path, 1); + wlr_log(WLR_INFO, "IPC: listening on %s", ipc_socket_path); +} + +void +ipc_finish(void) +{ + if (ipc_socket_fd < 0) { + return; + } + + struct ipc_client *client, *tmp; + wl_list_for_each_safe(client, tmp, &server.ipc_clients, link) { + ipc_client_disconnect(client); + } + + wl_event_source_remove(ipc_event_source); + close(ipc_socket_fd); + ipc_socket_fd = -1; + + if (ipc_socket_path) { + unlink(ipc_socket_path); + unsetenv("LABWC_IPC_SOCK"); + free(ipc_socket_path); + ipc_socket_path = NULL; + } +} + +/* =================================================================== + * Section 2: JSON Builders + * =================================================================== */ + +static struct json_object * +ipc_json_position(struct wlr_box box) +{ + struct json_object *obj = json_object_new_object(); + json_object_object_add(obj, "x", json_object_new_int(box.x)); + json_object_object_add(obj, "y", json_object_new_int(box.y)); + return obj; +} + +static struct json_object * +ipc_json_dimension(struct wlr_box box) +{ + struct json_object *obj = json_object_new_object(); + json_object_object_add(obj, "width", json_object_new_int(box.width)); + json_object_object_add(obj, "height", json_object_new_int(box.height)); + return obj; +} + +static struct json_object * +ipc_json_rect(struct wlr_box box) +{ + struct json_object *obj = json_object_new_object(); + json_object_object_add(obj, "position", ipc_json_position(box)); + json_object_object_add(obj, "dimension", ipc_json_dimension(box)); + return obj; +} + +static struct json_object * +ipc_json_output_mode(int width, int height, int refresh) +{ + struct json_object *obj = json_object_new_object(); + json_object_object_add(obj, "width", json_object_new_int(width)); + json_object_object_add(obj, "height", json_object_new_int(height)); + json_object_object_add(obj, "refresh", json_object_new_int(refresh)); + return obj; +} + +static const char * +transform_name(enum wl_output_transform t) +{ + switch (t) { + case WL_OUTPUT_TRANSFORM_NORMAL: + return "normal"; + case WL_OUTPUT_TRANSFORM_90: + return "90"; + case WL_OUTPUT_TRANSFORM_180: + return "180"; + case WL_OUTPUT_TRANSFORM_270: + return "270"; + case WL_OUTPUT_TRANSFORM_FLIPPED: + return "flipped"; + case WL_OUTPUT_TRANSFORM_FLIPPED_90: + return "flipped-90"; + case WL_OUTPUT_TRANSFORM_FLIPPED_180: + return "flipped-180"; + case WL_OUTPUT_TRANSFORM_FLIPPED_270: + return "flipped-270"; + } + return "normal"; +} + +static const char * +subpixel_name(enum wl_output_subpixel sp) +{ + switch (sp) { + case WL_OUTPUT_SUBPIXEL_UNKNOWN: + return "unknown"; + case WL_OUTPUT_SUBPIXEL_NONE: + return "none"; + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB: + return "rgb"; + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR: + return "bgr"; + case WL_OUTPUT_SUBPIXEL_VERTICAL_RGB: + return "vrgb"; + case WL_OUTPUT_SUBPIXEL_VERTICAL_BGR: + return "vbgr"; + } + return "unknown"; +} + +/* --- Type 1: GET_WORKSPACES --- */ +static struct json_object * +ipc_json_get_workspaces(void) +{ + struct json_object *arr = json_object_new_array(); + int index = 0; + struct workspace *ws; + wl_list_for_each(ws, &server.workspaces.all, link) { + index++; + struct json_object *obj = json_object_new_object(); + json_object_object_add(obj, "num", json_object_new_int(index)); + json_object_object_add(obj, "name", + json_object_new_string(ws->name)); + json_object_object_add(obj, "visible", + json_object_new_boolean(ws->tree->node.enabled)); + json_object_object_add(obj, "focused", + json_object_new_boolean(ws + == server.workspaces.current)); + json_object_object_add(obj, "urgent", + json_object_new_boolean(false)); + + /* Use the first usable output's area as workspace rect */ + struct wlr_box rect = {0}; + struct output *out; + wl_list_for_each(out, &server.outputs, link) { + if (output_is_usable(out)) { + rect = output_usable_area_in_layout_coords(out); + json_object_object_add(obj, "output", + json_object_new_string( + out->wlr_output->name)); + break; + } + } + json_object_object_add(obj, "rect", ipc_json_rect(rect)); + json_object_array_add(arr, obj); + } + return arr; +} + +/* --- Type 3: GET_OUTPUTS --- */ +static struct json_object * +ipc_json_get_outputs(void) +{ + struct json_object *arr = json_object_new_array(); + struct output *output; + wl_list_for_each(output, &server.outputs, link) { + struct wlr_output *o = output->wlr_output; + struct json_object *obj = json_object_new_object(); + json_object_object_add(obj, "name", + json_object_new_string(o->name ? o->name : "")); + json_object_object_add(obj, "make", + json_object_new_string(o->make ? o->make : "Unknown")); + json_object_object_add(obj, "model", + json_object_new_string(o->model ? o->model + : "Unknown")); + json_object_object_add(obj, "serial", + json_object_new_string(o->serial ? o->serial : "")); + json_object_object_add(obj, "active", + json_object_new_boolean(o->enabled)); + json_object_object_add(obj, "dpms", + json_object_new_boolean(o->enabled)); + json_object_object_add(obj, "power", + json_object_new_boolean(o->enabled)); + json_object_object_add(obj, "primary", + json_object_new_boolean(false)); + json_object_object_add(obj, "scale", + json_object_new_double(o->scale)); + json_object_object_add(obj, "subpixel_hinting", + json_object_new_string(subpixel_name(o->subpixel))); + json_object_object_add(obj, "transform", + json_object_new_string(transform_name(o->transform))); + + /* current_workspace */ + const char *ws_name = NULL; + if (output_is_usable(output) && server.workspaces.current) { + ws_name = server.workspaces.current->name; + } + json_object_object_add(obj, "current_workspace", + ws_name ? json_object_new_string(ws_name) + : json_object_new_null()); + + /* modes */ + struct json_object *modes = json_object_new_array(); + struct wlr_output_mode *mode; + wl_list_for_each(mode, &o->modes, link) { + json_object_array_add(modes, + ipc_json_output_mode(mode->width, mode->height, + mode->refresh)); + } + json_object_object_add(obj, "modes", modes); + + /* current_mode */ + if (o->current_mode) { + json_object_object_add(obj, "current_mode", + ipc_json_output_mode(o->current_mode->width, + o->current_mode->height, + o->current_mode->refresh)); + } else { + json_object_object_add(obj, "current_mode", + ipc_json_output_mode(o->width, o->height, + o->refresh)); + } + + /* rect */ + struct wlr_box rect = {0}; + if (output_is_usable(output)) { + rect.x = output->scene_output->x; + rect.y = output->scene_output->y; + } + rect.width = o->width; + rect.height = o->height; + json_object_object_add(obj, "rect", ipc_json_rect(rect)); + + json_object_array_add(arr, obj); + } + return arr; +} + +/* --- Type 4: GET_TREE helpers --- */ + +static const char * +border_name(struct view *view) +{ + if (view->ssd_preference == LAB_SSD_PREF_CLIENT + && view->ssd_mode == LAB_SSD_MODE_NONE) { + return "csd"; + } + switch (view->ssd_mode) { + case LAB_SSD_MODE_FULL: + return "normal"; + case LAB_SSD_MODE_BORDER: + return "pixel"; + case LAB_SSD_MODE_NONE: + return "none"; + default: + break; + } + return "none"; +} + +static struct json_object * +ipc_json_view_node(struct view *view) +{ + struct json_object *obj = json_object_new_object(); + json_object_object_add(obj, "id", + json_object_new_int64((int64_t)view->creation_id)); + json_object_object_add(obj, "name", + json_object_new_string(view->title)); + json_object_object_add(obj, "type", + json_object_new_string("floating_con")); + + /* app_id / shell / window_properties */ + if (view->type == LAB_XDG_SHELL_VIEW) { + json_object_object_add(obj, "app_id", + json_object_new_string(view->app_id)); + json_object_object_add(obj, "shell", + json_object_new_string("xdg_shell")); + json_object_object_add(obj, "window", json_object_new_null()); + } +#if HAVE_XWAYLAND + else if (view->type == LAB_XWAYLAND_VIEW) { + json_object_object_add(obj, "app_id", json_object_new_null()); + json_object_object_add(obj, "shell", + json_object_new_string("xwayland")); + + struct wlr_xwayland_surface *xsurface = NULL; + if (view->surface) { + xsurface = wlr_xwayland_surface_try_from_wlr_surface( + view->surface); + } + if (xsurface) { + json_object_object_add(obj, "window", + json_object_new_int64((int64_t) + xsurface->window_id)); + struct json_object *wp = json_object_new_object(); + json_object_object_add(wp, "class", + json_object_new_string(xsurface->class + ? xsurface->class + : "")); + json_object_object_add(wp, "instance", + json_object_new_string(xsurface->instance + ? xsurface->instance + : "")); + json_object_object_add(wp, "title", + json_object_new_string(view->title)); + json_object_object_add(wp, "window_role", + json_object_new_string(xsurface->role + ? xsurface->role + : "")); + json_object_object_add(wp, "window_type", + json_object_new_string("normal")); + json_object_object_add(wp, "transient_for", + json_object_new_null()); + json_object_object_add(obj, "window_properties", wp); + } else { + json_object_object_add(obj, "window", + json_object_new_null()); + } + } +#endif + + /* identifier for grim -T / ext-foreign-toplevel-list-v1 support */ + json_object_object_add(obj, "identifier", + json_object_new_string(foreign_toplevel_get_identifier( + view->foreign_toplevel))); + + pid_t pid = -1; + if (view->impl && view->impl->get_pid) { + pid = view->impl->get_pid(view); + } + json_object_object_add(obj, "pid", json_object_new_int(pid)); + + json_object_object_add(obj, "visible", + json_object_new_boolean(view->mapped + && view->workspace == server.workspaces.current)); + json_object_object_add(obj, "focused", + json_object_new_boolean(view == server.active_view)); + json_object_object_add(obj, "fullscreen_mode", + json_object_new_int(view->fullscreen ? 1 : 0)); + json_object_object_add(obj, "sticky", + json_object_new_boolean(view->visible_on_all_workspaces)); + + json_object_object_add(obj, "border", + json_object_new_string(border_name(view))); + json_object_object_add(obj, "current_border_width", + json_object_new_int(view->ssd_mode != LAB_SSD_MODE_NONE ? 1 + : 0)); + + /* geometry */ + json_object_object_add(obj, "position", + ipc_json_position(view->current)); + json_object_object_add(obj, "dimension", + ipc_json_dimension(view->current)); + + return obj; +} + +static struct json_object * +ipc_json_get_tree(void) +{ + struct json_object *root = json_object_new_object(); + json_object_object_add(root, "id", json_object_new_int(1)); + json_object_object_add(root, "name", json_object_new_string("root")); + json_object_object_add(root, "type", json_object_new_string("root")); + + /* Compute bounding box of all outputs */ + struct wlr_box root_rect = {0}; + struct output *out; + wl_list_for_each(out, &server.outputs, link) { + if (!output_is_usable(out)) { + continue; + } + int ox = out->scene_output->x; + int oy = out->scene_output->y; + int ow = out->wlr_output->width; + int oh = out->wlr_output->height; + if (ox + ow > root_rect.width) { + root_rect.width = ox + ow; + } + if (oy + oh > root_rect.height) { + root_rect.height = oy + oh; + } + } + json_object_object_add(root, "position", ipc_json_position(root_rect)); + json_object_object_add(root, "dimension", + ipc_json_dimension(root_rect)); + + struct json_object *output_nodes = json_object_new_array(); + int out_idx = 0; + wl_list_for_each(out, &server.outputs, link) { + out_idx++; + struct json_object *out_obj = json_object_new_object(); + json_object_object_add(out_obj, "id", + json_object_new_int(0x10000000 | out_idx)); + json_object_object_add(out_obj, "name", + json_object_new_string(out->wlr_output->name + ? out->wlr_output->name + : "")); + json_object_object_add(out_obj, "type", + json_object_new_string("output")); + + struct wlr_box out_rect = {0}; + if (output_is_usable(out)) { + out_rect.x = out->scene_output->x; + out_rect.y = out->scene_output->y; + } + out_rect.width = out->wlr_output->width; + out_rect.height = out->wlr_output->height; + json_object_object_add(out_obj, "position", + ipc_json_position(out_rect)); + json_object_object_add(out_obj, "dimension", + ipc_json_dimension(out_rect)); + json_object_object_add(out_obj, "layout", + json_object_new_string("output")); + + /* Build workspace nodes */ + struct json_object *ws_nodes = json_object_new_array(); + int ws_idx = 0; + struct workspace *ws; + wl_list_for_each(ws, &server.workspaces.all, link) { + ws_idx++; + struct json_object *ws_obj = json_object_new_object(); + json_object_object_add(ws_obj, "id", + json_object_new_int(0x20000000 | ws_idx)); + json_object_object_add(ws_obj, "name", + json_object_new_string(ws->name)); + json_object_object_add(ws_obj, "type", + json_object_new_string("workspace")); + json_object_object_add(ws_obj, "position", + ipc_json_position(out_rect)); + json_object_object_add(ws_obj, "dimension", + ipc_json_dimension(out_rect)); + + /* Floating nodes (views) */ + struct json_object *floating = json_object_new_array(); + struct json_object *focus_order = + json_object_new_array(); + struct view *view; + wl_list_for_each(view, &server.views, link) { + if (!view->mapped) { + continue; + } + if (view->workspace != ws + && !view->visible_on_all_workspaces) { + continue; + } + json_object_array_add(floating, + ipc_json_view_node(view)); + json_object_array_add(focus_order, + json_object_new_int64((int64_t) + view->creation_id)); + } + json_object_object_add(ws_obj, "floating_nodes", + floating); + json_object_object_add(ws_obj, "focus", focus_order); + json_object_array_add(ws_nodes, ws_obj); + } + json_object_object_add(out_obj, "nodes", ws_nodes); + json_object_array_add(output_nodes, out_obj); + } + + json_object_object_add(root, "nodes", output_nodes); + json_object_object_add(root, "focus", json_object_new_array()); + return root; +} + +/* --- Type 7: GET_VERSION --- */ +static struct json_object * +ipc_json_get_version(void) +{ + struct json_object *obj = json_object_new_object(); + + /* Parse version string X.Y.Z */ + const char *ver = LABWC_VERSION; + int major = 0, minor = 0, patch = 0; + sscanf(ver, "%d.%d.%d", &major, &minor, &patch); + + json_object_object_add(obj, "major", json_object_new_int(major)); + json_object_object_add(obj, "minor", json_object_new_int(minor)); + json_object_object_add(obj, "patch", json_object_new_int(patch)); + + char human[128]; + snprintf(human, sizeof(human), "labwc %s", ver); + json_object_object_add(obj, "human_readable", + json_object_new_string(human)); + + json_object_object_add(obj, "loaded_config_file_name", + json_object_new_string(rc.loaded_config_file + ? rc.loaded_config_file + : "")); + + return obj; +} + +/* --- Type 9: GET_CONFIG --- */ +static struct json_object * +ipc_json_get_config(void) +{ + struct json_object *obj = json_object_new_object(); + struct buf b = BUF_INIT; + if (rc.loaded_config_file) { + b = buf_from_file(rc.loaded_config_file); + } + json_object_object_add(obj, "config", + json_object_new_string(b.data ? b.data : "")); + buf_reset(&b); + return obj; +} + +/* --- Type 100: GET_INPUTS --- */ + +static const char * +input_type_name(enum wlr_input_device_type type) +{ + switch (type) { + case WLR_INPUT_DEVICE_KEYBOARD: + return "keyboard"; + case WLR_INPUT_DEVICE_POINTER: + return "pointer"; + case WLR_INPUT_DEVICE_TOUCH: + return "touch"; + case WLR_INPUT_DEVICE_TABLET: + return "tablet_tool"; + case WLR_INPUT_DEVICE_TABLET_PAD: + return "tablet_pad"; + default: + return "unknown"; + } +} + +static struct json_object * +ipc_json_input_device(struct input *input) +{ + struct wlr_input_device *dev = input->wlr_input_device; + struct json_object *obj = json_object_new_object(); + + /* Get vendor/product from libinput if available */ + unsigned int vendor = 0, product = 0; + if (wlr_input_device_is_libinput(dev)) { + struct libinput_device *ldev = + wlr_libinput_get_device_handle(dev); + if (ldev) { + vendor = libinput_device_get_id_vendor(ldev); + product = libinput_device_get_id_product(ldev); + } + } + + /* identifier: vendor:product:name */ + char ident[256]; + snprintf(ident, sizeof(ident), "%u:%u:%s", vendor, product, + dev->name ? dev->name : ""); + /* Replace spaces with underscores in identifier */ + for (char *p = ident; *p; p++) { + if (*p == ' ') { + *p = '_'; + } + } + json_object_object_add(obj, "identifier", + json_object_new_string(ident)); + json_object_object_add(obj, "name", + json_object_new_string(dev->name ? dev->name : "")); + json_object_object_add(obj, "vendor", json_object_new_int(vendor)); + json_object_object_add(obj, "product", json_object_new_int(product)); + json_object_object_add(obj, "type", + json_object_new_string(input_type_name(dev->type))); + + /* Keyboard-specific: xkb layout info */ + if (dev->type == WLR_INPUT_DEVICE_KEYBOARD) { + struct wlr_keyboard *kb = server.seat.keyboard_group + ? &server.seat.keyboard_group->keyboard + : NULL; + if (kb && kb->keymap) { + struct xkb_keymap *keymap = kb->keymap; + xkb_layout_index_t num_layouts = + xkb_keymap_num_layouts(keymap); + xkb_layout_index_t active = + kb->modifiers.group < num_layouts + ? kb->modifiers.group + : 0; + + const char *active_name = + xkb_keymap_layout_get_name(keymap, active); + json_object_object_add(obj, "xkb_active_layout_name", + json_object_new_string(active_name ? active_name + : "")); + json_object_object_add(obj, "xkb_active_layout_index", + json_object_new_int(active)); + + struct json_object *layouts = json_object_new_array(); + for (xkb_layout_index_t i = 0; i < num_layouts; i++) { + const char *name = + xkb_keymap_layout_get_name(keymap, i); + json_object_array_add(layouts, + json_object_new_string(name ? name + : "")); + } + json_object_object_add(obj, "xkb_layout_names", + layouts); + } + } + + /* Pointer-specific: scroll_factor */ + if (dev->type == WLR_INPUT_DEVICE_POINTER) { + json_object_object_add(obj, "scroll_factor", + json_object_new_double(input->scroll_factor)); + } + + return obj; +} + +static struct json_object * +ipc_json_get_inputs(void) +{ + struct json_object *arr = json_object_new_array(); + struct input *input; + wl_list_for_each(input, &server.seat.inputs, link) { + json_object_array_add(arr, ipc_json_input_device(input)); + } + return arr; +} + +/* --- Type 101: GET_SEATS --- */ +static struct json_object * +ipc_json_get_seats(void) +{ + struct json_object *arr = json_object_new_array(); + struct json_object *seat_obj = json_object_new_object(); + + json_object_object_add(seat_obj, "name", + json_object_new_string(server.seat.wlr_seat->name + ? server.seat.wlr_seat->name + : "seat0")); + json_object_object_add(seat_obj, "capabilities", + json_object_new_int(server.seat.wlr_seat->capabilities)); + json_object_object_add(seat_obj, "focus", + json_object_new_int64(server.active_view + ? (int64_t)server.active_view->creation_id + : 0)); + + struct json_object *devices = json_object_new_array(); + struct input *input; + wl_list_for_each(input, &server.seat.inputs, link) { + json_object_array_add(devices, ipc_json_input_device(input)); + } + json_object_object_add(seat_obj, "devices", devices); + json_object_array_add(arr, seat_obj); + return arr; +} + +/* =================================================================== + * Section 3: Sway-Compatible Command Parser + * =================================================================== */ + +/* + * Helper to create an action, add a single string arg, execute it via + * the action system, then free it. + */ +static bool +run_action_simple(const char *action_name, struct view *target) +{ + struct action *a = action_create(action_name); + if (!a) { + return false; + } + struct wl_list actions; + wl_list_init(&actions); + wl_list_insert(&actions, &a->link); + actions_run(target, &actions, NULL); + action_list_free(&actions); + return true; +} + +static bool +run_action_with_str(const char *action_name, const char *key, const char *value, + struct view *target) +{ + struct action *a = action_create(action_name); + if (!a) { + return false; + } + action_arg_add_str(a, key, value); + struct wl_list actions; + wl_list_init(&actions); + wl_list_insert(&actions, &a->link); + actions_run(target, &actions, NULL); + action_list_free(&actions); + return true; +} + +/* + * Parse criteria like [app_id="firefox" title="..."] + * Returns the start of the command after criteria, or cmd if no criteria. + * Sets *query to a newly allocated query if criteria found, else NULL. + */ +static const char * +parse_criteria(const char *cmd, struct view_query **query) +{ + *query = NULL; + while (*cmd == ' ' || *cmd == '\t') { + cmd++; + } + if (*cmd != '[') { + return cmd; + } + + const char *p = cmd + 1; + *query = view_query_create(); + + while (*p && *p != ']') { + while (*p == ' ') { + p++; + } + if (*p == ']') { + break; + } + + /* Parse key=value or key="value" */ + const char *key_start = p; + while (*p && *p != '=' && *p != ']' && *p != ' ') { + p++; + } + size_t key_len = p - key_start; + if (*p != '=') { + break; + } + p++; /* skip '=' */ + + /* Parse value (optionally quoted) */ + const char *val_start; + const char *val_end; + if (*p == '"') { + p++; + val_start = p; + while (*p && *p != '"') { + p++; + } + val_end = p; + if (*p == '"') { + p++; + } + } else { + val_start = p; + while (*p && *p != ' ' && *p != ']') { + p++; + } + val_end = p; + } + + char *val = strndup(val_start, val_end - val_start); + if (key_len == 6 && !strncmp(key_start, "app_id", 6)) { + free((*query)->identifier); + (*query)->identifier = val; + } else if (key_len == 5 && !strncmp(key_start, "title", 5)) { + free((*query)->title); + (*query)->title = val; + } else if (key_len == 5 && !strncmp(key_start, "class", 5)) { + free((*query)->identifier); + (*query)->identifier = val; + } else { + free(val); + } + } + + if (*p == ']') { + p++; + } + while (*p == ' ' || *p == '\t') { + p++; + } + return p; +} + +/* Find the first matching view for a query */ +static struct view * +find_view_for_query(struct view_query *query) +{ + struct view *view; + wl_list_for_each(view, &server.views, link) { + if (view->mapped && view_matches_query(view, query)) { + return view; + } + } + return NULL; +} + +static struct json_object * +cmd_result(bool success, const char *error) +{ + struct json_object *obj = json_object_new_object(); + json_object_object_add(obj, "success", + json_object_new_boolean(success)); + if (error) { + json_object_object_add(obj, "error", + json_object_new_string(error)); + } + return obj; +} + +static struct json_object * +execute_single_command(const char *cmd) +{ + /* Parse criteria */ + struct view_query *query = NULL; + cmd = parse_criteria(cmd, &query); + + /* Determine target view */ + struct view *target = NULL; + if (query) { + target = find_view_for_query(query); + view_query_free(query); + } else { + target = server.active_view; + } + + /* Skip leading whitespace */ + while (*cmd == ' ' || *cmd == '\t') { + cmd++; + } + + /* --- nop --- */ + if (!strncmp(cmd, "nop", 3)) { + return cmd_result(true, NULL); + } + + /* --- exit --- */ + if (!strcmp(cmd, "exit")) { + run_action_simple("Exit", NULL); + return cmd_result(true, NULL); + } + + /* --- reload --- */ + if (!strcmp(cmd, "reload")) { + kill(getpid(), SIGHUP); + return cmd_result(true, NULL); + } + + /* --- exec --- */ + if (!strncmp(cmd, "exec ", 5)) { + const char *exec_cmd = cmd + 5; + while (*exec_cmd == ' ') { + exec_cmd++; + } + /* skip optional --no-startup-id */ + if (!strncmp(exec_cmd, "--no-startup-id ", 16)) { + exec_cmd += 16; + while (*exec_cmd == ' ') { + exec_cmd++; + } + } + if (*exec_cmd) { + run_action_with_str("Execute", "command", exec_cmd, + NULL); + return cmd_result(true, NULL); + } + return cmd_result(false, "exec requires a command"); + } + + /* --- kill --- */ + if (!strcmp(cmd, "kill")) { + if (!target) { + return cmd_result(false, "No focused window"); + } + run_action_simple("Close", target); + return cmd_result(true, NULL); + } + + /* --- fullscreen toggle --- */ + if (!strcmp(cmd, "fullscreen toggle") || !strcmp(cmd, "fullscreen")) { + if (!target) { + return cmd_result(false, "No focused window"); + } + run_action_simple("ToggleFullscreen", target); + return cmd_result(true, NULL); + } + + /* --- floating toggle --- */ + if (!strcmp(cmd, "floating toggle")) { + if (!target) { + return cmd_result(false, "No focused window"); + } + /* In stacking WM: untile if tiled, otherwise no-op */ + if (view_is_tiled(target)) { + run_action_simple("UnSnap", target); + } + return cmd_result(true, NULL); + } + + /* --- sticky toggle --- */ + if (!strcmp(cmd, "sticky toggle")) { + if (!target) { + return cmd_result(false, "No focused window"); + } + run_action_simple("ToggleOmnipresent", target); + return cmd_result(true, NULL); + } + + /* --- border --- */ + if (!strncmp(cmd, "border ", 7)) { + if (!target) { + return cmd_result(false, "No focused window"); + } + const char *arg = cmd + 7; + if (!strcmp(arg, "none")) { + run_action_with_str("SetDecorations", "decorations", + "none", target); + } else if (!strcmp(arg, "normal")) { + run_action_with_str("SetDecorations", "decorations", + "full", target); + } else if (!strcmp(arg, "pixel")) { + run_action_with_str("SetDecorations", "decorations", + "border", target); + } else if (!strcmp(arg, "toggle")) { + run_action_simple("ToggleDecorations", target); + } else { + return cmd_result(false, "Unknown border type"); + } + return cmd_result(true, NULL); + } + + /* --- workspace --- */ + if (!strncmp(cmd, "workspace ", 10)) { + const char *ws_name = cmd + 10; + while (*ws_name == ' ') { + ws_name++; + } + const char *to = ws_name; + if (!strcmp(ws_name, "next") + || !strcmp(ws_name, "next_on_output")) { + to = "right"; + } else if (!strcmp(ws_name, "prev") + || !strcmp(ws_name, "prev_on_output")) { + to = "left"; + } + run_action_with_str("GoToDesktop", "to", to, NULL); + return cmd_result(true, NULL); + } + + /* --- focus output --- */ + if (!strncmp(cmd, "focus output ", 13)) { + const char *arg = cmd + 13; + while (*arg == ' ') { + arg++; + } + /* Try as direction first */ + if (!strcmp(arg, "left") || !strcmp(arg, "right") + || !strcmp(arg, "up") || !strcmp(arg, "down")) { + run_action_with_str("FocusOutput", "direction", arg, + NULL); + } else { + run_action_with_str("FocusOutput", "output", arg, NULL); + } + return cmd_result(true, NULL); + } + + /* --- move ... --- */ + if (!strncmp(cmd, "move ", 5)) { + const char *arg = cmd + 5; + while (*arg == ' ') { + arg++; + } + + /* move position X Y */ + if (!strncmp(arg, "position ", 9)) { + if (!target) { + return cmd_result(false, "No focused window"); + } + int x = 0, y = 0; + sscanf(arg + 9, "%d %d", &x, &y); + struct action *a = action_create("MoveTo"); + if (a) { + action_arg_add_int(a, "x", x); + action_arg_add_int(a, "y", y); + struct wl_list actions; + wl_list_init(&actions); + wl_list_insert(&actions, &a->link); + actions_run(target, &actions, NULL); + action_list_free(&actions); + } + return cmd_result(true, NULL); + } + + /* move [container|window] [to] workspace */ + const char *ws_needle = strstr(arg, "workspace "); + if (ws_needle) { + if (!target) { + return cmd_result(false, "No focused window"); + } + const char *ws_name = ws_needle + 10; + while (*ws_name == ' ') { + ws_name++; + } + run_action_with_str("SendToDesktop", "to", ws_name, + target); + return cmd_result(true, NULL); + } + + /* move [container|window] [to] output */ + const char *out_needle = strstr(arg, "output "); + if (out_needle) { + if (!target) { + return cmd_result(false, "No focused window"); + } + const char *out_name = out_needle + 7; + while (*out_name == ' ') { + out_name++; + } + if (!strcmp(out_name, "left") + || !strcmp(out_name, "right") + || !strcmp(out_name, "up") + || !strcmp(out_name, "down")) { + run_action_with_str("MoveToOutput", "direction", + out_name, target); + } else { + run_action_with_str("MoveToOutput", "output", + out_name, target); + } + return cmd_result(true, NULL); + } + + /* move left|right|up|down [N] */ + if (!strncmp(arg, "left", 4) || !strncmp(arg, "right", 5) + || !strncmp(arg, "up", 2) || !strncmp(arg, "down", 4)) { + if (!target) { + return cmd_result(false, "No focused window"); + } + int amount = 10; + const char *num = arg; + /* Skip past direction word */ + while (*num && *num != ' ') { + num++; + } + while (*num == ' ') { + num++; + } + if (*num >= '0' && *num <= '9') { + amount = atoi(num); + } + + struct action *a = action_create("MoveRelative"); + if (a) { + int dx = 0, dy = 0; + if (!strncmp(arg, "left", 4)) { + dx = -amount; + } else if (!strncmp(arg, "right", 5)) { + dx = amount; + } else if (!strncmp(arg, "up", 2)) { + dy = -amount; + } else if (!strncmp(arg, "down", 4)) { + dy = amount; + } + action_arg_add_int(a, "x", dx); + action_arg_add_int(a, "y", dy); + struct wl_list actions; + wl_list_init(&actions); + wl_list_insert(&actions, &a->link); + actions_run(target, &actions, NULL); + action_list_free(&actions); + } + return cmd_result(true, NULL); + } + + return cmd_result(false, "Unknown move command"); + } + + /* --- resize --- */ + if (!strncmp(cmd, "resize ", 7)) { + if (!target) { + return cmd_result(false, "No focused window"); + } + const char *arg = cmd + 7; + + /* resize set W H */ + if (!strncmp(arg, "set ", 4)) { + int w = 0, h = 0; + sscanf(arg + 4, "%d %d", &w, &h); + struct action *a = action_create("ResizeTo"); + if (a) { + action_arg_add_int(a, "width", w); + action_arg_add_int(a, "height", h); + struct wl_list actions; + wl_list_init(&actions); + wl_list_insert(&actions, &a->link); + actions_run(target, &actions, NULL); + action_list_free(&actions); + } + return cmd_result(true, NULL); + } + + /* resize grow|shrink width|height N [px|ppt] */ + if (!strncmp(arg, "grow ", 5) || !strncmp(arg, "shrink ", 7)) { + bool grow = !strncmp(arg, "grow", 4); + const char *rest = arg + (grow ? 5 : 7); + bool is_width = !strncmp(rest, "width", 5); + /* Skip past width/height */ + while (*rest && *rest != ' ') { + rest++; + } + while (*rest == ' ') { + rest++; + } + int amount = 10; + if (*rest >= '0' && *rest <= '9') { + amount = atoi(rest); + } + if (!grow) { + amount = -amount; + } + + struct action *a = action_create("ResizeRelative"); + if (a) { + if (is_width) { + action_arg_add_int(a, "right", amount); + } else { + action_arg_add_int(a, "bottom", amount); + } + struct wl_list actions; + wl_list_init(&actions); + wl_list_insert(&actions, &a->link); + actions_run(target, &actions, NULL); + action_list_free(&actions); + } + return cmd_result(true, NULL); + } + + return cmd_result(false, "Unknown resize command"); + } + + /* --- Unsupported commands --- */ + if (!strncmp(cmd, "layout ", 7) || !strncmp(cmd, "split", 5)) { + return cmd_result(false, + "layout/split not supported (stacking compositor)"); + } + if (!strncmp(cmd, "mark ", 5) || !strncmp(cmd, "unmark", 6)) { + return cmd_result(false, "marks not supported"); + } + if (!strncmp(cmd, "scratchpad", 10)) { + return cmd_result(false, "scratchpad not supported"); + } + + return cmd_result(false, "Unknown command"); +} + +/* Parse and run possibly multi-command string (separated by ; or ,) */ +static struct json_object * +ipc_cmd_run(const char *payload) +{ + struct json_object *results = json_object_new_array(); + if (!payload || !*payload) { + json_object_array_add(results, + cmd_result(false, "No command given")); + return results; + } + + /* Split on ; */ + char *buf = strdup(payload); + char *saveptr = NULL; + char *token = strtok_r(buf, ";", &saveptr); + while (token) { + /* Trim whitespace */ + while (*token == ' ' || *token == '\t') { + token++; + } + char *end = token + strlen(token) - 1; + while (end > token && (*end == ' ' || *end == '\t')) { + *end-- = '\0'; + } + if (*token) { + json_object_array_add(results, + execute_single_command(token)); + } + token = strtok_r(NULL, ";", &saveptr); + } + free(buf); + + if (json_object_array_length(results) == 0) { + json_object_array_add(results, + cmd_result(false, "No command given")); + } + return results; +} + +/* =================================================================== + * Section 3b: SUBSCRIBE handler + * =================================================================== */ + +static struct json_object * +ipc_cmd_subscribe(struct ipc_client *client, const char *payload) +{ + struct json_object *parsed = json_tokener_parse(payload); + if (!parsed || json_object_get_type(parsed) != json_type_array) { + if (parsed) { + json_object_put(parsed); + } + return cmd_result(false, "Invalid subscription payload"); + } + + int len = json_object_array_length(parsed); + for (int i = 0; i < len; i++) { + struct json_object *item = json_object_array_get_idx(parsed, i); + const char *name = json_object_get_string(item); + if (!name) { + continue; + } + if (!strcmp(name, "workspace")) { + client->subscriptions |= IPC_SUB_WORKSPACE; + } else if (!strcmp(name, "output")) { + client->subscriptions |= IPC_SUB_OUTPUT; + } else if (!strcmp(name, "window")) { + client->subscriptions |= IPC_SUB_WINDOW; + } else if (!strcmp(name, "shutdown")) { + client->subscriptions |= IPC_SUB_SHUTDOWN; + } else if (!strcmp(name, "tick")) { + client->subscriptions |= IPC_SUB_TICK; + /* Send initial tick */ + struct json_object *tick = json_object_new_object(); + json_object_object_add(tick, "first", + json_object_new_boolean(true)); + json_object_object_add(tick, "payload", + json_object_new_string("")); + ipc_send_reply_json(client, IPC_EVENT_TICK, tick); + } + } + json_object_put(parsed); + return cmd_result(true, NULL); +} + +/* =================================================================== + * Message dispatch + * =================================================================== */ + +static void +ipc_handle_message(struct ipc_client *client, uint32_t type, + const char *payload, uint32_t len) +{ + struct json_object *reply = NULL; + + switch (type) { + case IPC_RUN_COMMAND: + reply = ipc_cmd_run(payload); + ipc_send_reply_json(client, IPC_RUN_COMMAND, reply); + return; + + case IPC_GET_WORKSPACES: + reply = ipc_json_get_workspaces(); + break; + + case IPC_SUBSCRIBE: + reply = ipc_cmd_subscribe(client, payload); + break; + + case IPC_GET_OUTPUTS: + reply = ipc_json_get_outputs(); + break; + + case IPC_GET_TREE: + reply = ipc_json_get_tree(); + break; + + case IPC_GET_BAR_CONFIG: + /* labwc has no built-in bar */ + if (payload && *payload) { + /* Requesting specific bar config - return empty object + */ + reply = json_object_new_object(); + } else { + reply = json_object_new_array(); + } + break; + + case IPC_GET_VERSION: + reply = ipc_json_get_version(); + break; + + case IPC_GET_CONFIG: + reply = ipc_json_get_config(); + break; + + case IPC_SEND_TICK: { + /* Broadcast tick event to subscribers */ + struct json_object *tick_event = json_object_new_object(); + json_object_object_add(tick_event, "first", + json_object_new_boolean(false)); + json_object_object_add(tick_event, "payload", + json_object_new_string(payload ? payload : "")); + const char *tick_str = json_object_to_json_string(tick_event); + struct ipc_client *c; + wl_list_for_each(c, &server.ipc_clients, link) { + if (c->subscriptions & IPC_SUB_TICK) { + ipc_send_reply(c, IPC_EVENT_TICK, tick_str, + strlen(tick_str)); + } + } + json_object_put(tick_event); + reply = cmd_result(true, NULL); + break; + } + + case IPC_SYNC: + /* Not applicable to Wayland (same as sway) */ + reply = cmd_result(false, NULL); + break; + + case IPC_GET_INPUTS: + reply = ipc_json_get_inputs(); + break; + + case IPC_GET_SEATS: + reply = ipc_json_get_seats(); + break; + + default: + reply = cmd_result(false, "Unknown IPC message type"); + break; + } + + ipc_send_reply_json(client, type, reply); +} + +/* =================================================================== + * Section 4: Event Emitters + * =================================================================== */ + +static void +ipc_broadcast_event(uint32_t event_type, uint32_t sub_bit, + struct json_object *event) +{ + if (ipc_socket_fd < 0) { + json_object_put(event); + return; + } + + const char *str = json_object_to_json_string(event); + size_t len = strlen(str); + struct ipc_client *client; + wl_list_for_each(client, &server.ipc_clients, link) { + if (client->subscriptions & sub_bit) { + ipc_send_reply(client, event_type, str, len); + } + } + json_object_put(event); +} + +static struct json_object * +ipc_json_workspace_obj(struct workspace *ws) +{ + if (!ws) { + return json_object_new_null(); + } + struct json_object *obj = json_object_new_object(); + int index = 0; + struct workspace *w; + wl_list_for_each(w, &server.workspaces.all, link) { + index++; + if (w == ws) { + break; + } + } + json_object_object_add(obj, "num", json_object_new_int(index)); + json_object_object_add(obj, "name", json_object_new_string(ws->name)); + json_object_object_add(obj, "visible", + json_object_new_boolean(ws->tree->node.enabled)); + json_object_object_add(obj, "focused", + json_object_new_boolean(ws == server.workspaces.current)); + json_object_object_add(obj, "urgent", json_object_new_boolean(false)); + + struct wlr_box rect = {0}; + struct output *out; + wl_list_for_each(out, &server.outputs, link) { + if (output_is_usable(out)) { + rect = output_usable_area_in_layout_coords(out); + json_object_object_add(obj, "output", + json_object_new_string(out->wlr_output->name)); + break; + } + } + json_object_object_add(obj, "rect", ipc_json_rect(rect)); + return obj; +} + +void +ipc_event_workspace(const char *change, struct workspace *current, + struct workspace *old) +{ + if (ipc_socket_fd < 0) { + return; + } + struct json_object *event = json_object_new_object(); + json_object_object_add(event, "change", json_object_new_string(change)); + json_object_object_add(event, "current", + ipc_json_workspace_obj(current)); + json_object_object_add(event, "old", ipc_json_workspace_obj(old)); + ipc_broadcast_event(IPC_EVENT_WORKSPACE, IPC_SUB_WORKSPACE, event); +} + +void +ipc_event_output(const char *change) +{ + if (ipc_socket_fd < 0) { + return; + } + struct json_object *event = json_object_new_object(); + json_object_object_add(event, "change", json_object_new_string(change)); + ipc_broadcast_event(IPC_EVENT_OUTPUT, IPC_SUB_OUTPUT, event); +} + +void +ipc_event_window(const char *change, struct view *view) +{ + if (ipc_socket_fd < 0) { + return; + } + struct json_object *event = json_object_new_object(); + json_object_object_add(event, "change", json_object_new_string(change)); + if (view) { + json_object_object_add(event, "container", + ipc_json_view_node(view)); + } else { + json_object_object_add(event, "container", + json_object_new_null()); + } + ipc_broadcast_event(IPC_EVENT_WINDOW, IPC_SUB_WINDOW, event); +} + +void +ipc_event_shutdown(void) +{ + if (ipc_socket_fd < 0) { + return; + } + struct json_object *event = json_object_new_object(); + json_object_object_add(event, "change", json_object_new_string("exit")); + ipc_broadcast_event(IPC_EVENT_SHUTDOWN, IPC_SUB_SHUTDOWN, event); +} diff --git a/src/meson.build b/src/meson.build index 05163cfa..f0f9fc18 100644 --- a/src/meson.build +++ b/src/meson.build @@ -7,6 +7,7 @@ labwc_sources = files( 'edges.c', 'idle.c', 'interactive.c', + 'ipc.c', 'layers.c', 'magnifier.c', 'main.c', diff --git a/src/output.c b/src/output.c index 2eab8ec3..3add54f8 100644 --- a/src/output.c +++ b/src/output.c @@ -30,6 +30,7 @@ #include "common/string-helpers.h" #include "config/rcxml.h" #include "labwc.h" +#include "ipc.h" #include "layers.h" #include "node.h" #include "output-state.h" @@ -285,6 +286,7 @@ handle_output_destroy(struct wl_listener *listener, void *data) wl_list_remove(&output->destroy.link); wl_list_remove(&output->request_state.link); seat_output_layout_changed(seat); + ipc_event_output("destroy"); for (size_t i = 0; i < ARRAY_SIZE(output->layer_tree); i++) { wlr_scene_node_destroy(&output->layer_tree[i]->node); diff --git a/src/server.c b/src/server.c index d0c8c04c..1d44751d 100644 --- a/src/server.c +++ b/src/server.c @@ -61,6 +61,7 @@ #include "desktop-entry.h" #include "idle.h" #include "input/keyboard.h" +#include "ipc.h" #include "labwc.h" #include "layers.h" #include "magnifier.h" @@ -833,6 +834,8 @@ server_start(void) /* Potentially set up the initial fallback output */ output_virtual_update_fallback(); + ipc_init(); + if (setenv("WAYLAND_DISPLAY", socket, true) < 0) { wlr_log_errno(WLR_ERROR, "unable to set WAYLAND_DISPLAY"); } else { @@ -881,5 +884,7 @@ server_finish(void) workspaces_destroy(); wlr_scene_node_destroy(&server.scene->tree.node); + ipc_finish(); + wl_display_destroy(server.wl_display); } diff --git a/src/view.c b/src/view.c index 21005f20..08a7d0c5 100644 --- a/src/view.c +++ b/src/view.c @@ -19,6 +19,7 @@ #include "cycle.h" #include "foreign-toplevel/foreign.h" #include "input/keyboard.h" +#include "ipc.h" #include "labwc.h" #include "menu/menu.h" #include "output.h" @@ -80,6 +81,7 @@ struct view_query * view_query_create(void) { struct view_query *query = znew(*query); + wl_list_init(&query->link); /* Must be synced with view_matches_rule() in window-rules.c */ query->window_type = LAB_WINDOW_TYPE_INVALID; query->maximized = VIEW_AXIS_INVALID; @@ -567,6 +569,25 @@ view_moved(struct view *view) if (rc.resize_indicator && server.grabbed_view == view) { resize_indicator_update(view); } + + /* + * Fallback IPC emission: catch geometry corrections made by + * the client on commit (e.g. terminal snapping to char grid). + * The primary emission point is in view_move_resize() which + * fires immediately using view->pending. The ipc_last_geo + * dedup ensures no duplicates when pending == current. + */ + if (view->mapped) { + struct wlr_box *last = &view->ipc_last_geo; + struct wlr_box *cur = &view->current; + if (cur->x != last->x || cur->y != last->y) { + ipc_event_window("move", view); + } + if (cur->width != last->width || cur->height != last->height) { + ipc_event_window("resize", view); + } + *last = *cur; + } } void @@ -590,6 +611,25 @@ view_move_resize(struct view *view, struct wlr_box geo) if (!view->adjusting_for_layout_change) { view_save_last_placement(view); } + + /* + * Emit IPC move/resize events based on pending geometry. + * This fires immediately when the resize is requested rather + * than waiting for the client to commit, giving subscribers + * realtime tracking that matches interactive move behaviour. + */ + if (view->mapped) { + struct wlr_box *last = &view->ipc_last_geo; + struct wlr_box *pending = &view->pending; + if (pending->x != last->x || pending->y != last->y) { + ipc_event_window("move", view); + } + if (pending->width != last->width + || pending->height != last->height) { + ipc_event_window("resize", view); + } + *last = *pending; + } } void diff --git a/src/workspaces.c b/src/workspaces.c index a1ed9112..e1f9b085 100644 --- a/src/workspaces.c +++ b/src/workspaces.c @@ -20,6 +20,7 @@ #include "config/rcxml.h" #include "input/keyboard.h" #include "labwc.h" +#include "ipc.h" #include "output.h" #include "show-desktop.h" #include "theme.h" @@ -497,6 +498,8 @@ workspaces_switch_to(struct workspace *target, bool update_focus) wlr_ext_workspace_handle_v1_set_active(target->ext_workspace, true); + ipc_event_workspace("focus", target, server.workspaces.last); + show_desktop_reset(); } diff --git a/src/xdg.c b/src/xdg.c index 03035d63..aeac326f 100644 --- a/src/xdg.c +++ b/src/xdg.c @@ -17,6 +17,7 @@ #include "config/rcxml.h" #include "decorations.h" #include "foreign-toplevel/foreign.h" +#include "ipc.h" #include "labwc.h" #include "menu/menu.h" #include "node.h" @@ -592,6 +593,7 @@ handle_set_title(struct wl_listener *listener, void *data) struct wlr_xdg_toplevel *toplevel = xdg_toplevel_from_view(view); view_set_title(view, toplevel->title); + ipc_event_window("title", view); } static void @@ -867,6 +869,8 @@ handle_map(struct wl_listener *listener, void *data) view_impl_map(view); view->been_mapped = true; + ipc_event_window("new", view); + view->ipc_last_geo = view->current; } static void @@ -876,6 +880,7 @@ handle_unmap(struct wl_listener *listener, void *data) if (view->mapped) { view->mapped = false; view_impl_unmap(view); + ipc_event_window("close", view); } } diff --git a/src/xwayland.c b/src/xwayland.c index 5838d412..9270e922 100644 --- a/src/xwayland.c +++ b/src/xwayland.c @@ -17,6 +17,7 @@ #include "config/rcxml.h" #include "config/session.h" #include "foreign-toplevel/foreign.h" +#include "ipc.h" #include "labwc.h" #include "node.h" #include "output.h" @@ -580,6 +581,7 @@ handle_set_title(struct wl_listener *listener, void *data) struct view *view = wl_container_of(listener, view, set_title); struct xwayland_view *xwayland_view = xwayland_view_from_view(view); view_set_title(view, xwayland_view->xwayland_surface->title); + ipc_event_window("title", view); } static void @@ -825,6 +827,8 @@ handle_map(struct wl_listener *listener, void *data) view_impl_map(view); view->been_mapped = true; + ipc_event_window("new", view); + view->ipc_last_geo = view->current; } static void @@ -836,6 +840,7 @@ handle_unmap(struct wl_listener *listener, void *data) } view->mapped = false; view_impl_unmap(view); + ipc_event_window("close", view); /* * Destroy the content_tree at unmap. Alternatively, we could