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 01/13] 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 From a28c4ce8e459f4fed8afd3cff26e6007fb0efbbb Mon Sep 17 00:00:00 2001 From: stormshadow <190884359+st0rm-shad0w@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:29:37 +0530 Subject: [PATCH 02/13] ipc: emit resize events during interactive resize --- src/input/cursor.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/input/cursor.c b/src/input/cursor.c index e8ac7d96..a66cee02 100644 --- a/src/input/cursor.c +++ b/src/input/cursor.c @@ -28,6 +28,7 @@ #include "input/keyboard.h" #include "input/tablet.h" #include "input/touch.h" +#include "ipc.h" #include "labwc.h" #include "layers.h" #include "menu/menu.h" @@ -374,6 +375,22 @@ process_cursor_resize(uint32_t time) view_move_resize(view, new_view_geo); } else { resize_outlines_update(view, new_view_geo); + if (view->mapped) { + view->current = new_view_geo; + struct wlr_box *last = &view->ipc_last_geo; + if (new_view_geo.x != last->x + || new_view_geo.y != last->y) { + ipc_event_window("move", view); + } + if (new_view_geo.width != last->width + || new_view_geo.height != last->height) { + ipc_event_window("resize", view); + } + last->x = new_view_geo.x; + last->y = new_view_geo.y; + last->width = new_view_geo.width; + last->height = new_view_geo.height; + } } } From 6e14cd29f1785e1f9dfcb32161a9c91faa53b920 Mon Sep 17 00:00:00 2001 From: stormshadow <190884359+st0rm-shad0w@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:46:12 +0530 Subject: [PATCH 03/13] ipc: move identifier field after app_id in get_tree output --- src/ipc.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ipc.c b/src/ipc.c index 0ab1fa4b..91f368fd 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -553,6 +553,9 @@ ipc_json_view_node(struct view *view) 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, "identifier", + json_object_new_string(foreign_toplevel_get_identifier( + view->foreign_toplevel))); json_object_object_add(obj, "shell", json_object_new_string("xdg_shell")); json_object_object_add(obj, "window", json_object_new_null()); @@ -560,6 +563,9 @@ ipc_json_view_node(struct view *view) #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, "identifier", + json_object_new_string(foreign_toplevel_get_identifier( + view->foreign_toplevel))); json_object_object_add(obj, "shell", json_object_new_string("xwayland")); @@ -599,11 +605,6 @@ ipc_json_view_node(struct view *view) } #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); From 8b1b59249d1b5703475ed2e2e3f6028753016ca4 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Wed, 15 Apr 2026 23:28:32 +0200 Subject: [PATCH 04/13] move ipc_clients list to ipc.c --- src/ipc.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/ipc.c b/src/ipc.c index 91f368fd..92f57bf7 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -60,6 +60,8 @@ static int ipc_socket_fd = -1; static char *ipc_socket_path; static struct wl_event_source *ipc_event_source; +static struct wl_list ipc_clients; + static void ipc_client_disconnect(struct ipc_client *client); static void ipc_handle_message(struct ipc_client *client, uint32_t type, @@ -205,7 +207,7 @@ handle_new_connection(int fd, uint32_t mask, void *data) 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); + wl_list_insert(&ipc_clients, &client->link); return 0; } @@ -226,7 +228,7 @@ ipc_client_disconnect(struct ipc_client *client) void ipc_init(void) { - wl_list_init(&server.ipc_clients); + wl_list_init(&ipc_clients); ipc_socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (ipc_socket_fd < 0) { @@ -297,7 +299,7 @@ ipc_finish(void) } struct ipc_client *client, *tmp; - wl_list_for_each_safe(client, tmp, &server.ipc_clients, link) { + wl_list_for_each_safe(client, tmp, &ipc_clients, link) { ipc_client_disconnect(client); } @@ -1565,7 +1567,7 @@ ipc_handle_message(struct ipc_client *client, uint32_t type, 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) { + wl_list_for_each(c, &ipc_clients, link) { if (c->subscriptions & IPC_SUB_TICK) { ipc_send_reply(c, IPC_EVENT_TICK, tick_str, strlen(tick_str)); @@ -1613,7 +1615,7 @@ ipc_broadcast_event(uint32_t event_type, uint32_t sub_bit, 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) { + wl_list_for_each(client, &ipc_clients, link) { if (client->subscriptions & sub_bit) { ipc_send_reply(client, event_type, str, len); } From 784cf42e4cd4fa5daf89d93f7be1470d0e0f055d Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Wed, 15 Apr 2026 23:39:58 +0200 Subject: [PATCH 05/13] Revert unrelated changes to labwc.h --- include/labwc.h | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/include/labwc.h b/include/labwc.h index e6f646b7..c2f8d0b4 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,8 +245,7 @@ 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; @@ -255,7 +254,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; @@ -409,8 +408,7 @@ 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); /* @@ -461,6 +459,7 @@ 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 */ From 1d40640188382be208a1621dd80767bd9592d7cc Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Wed, 15 Apr 2026 23:47:03 +0200 Subject: [PATCH 06/13] Move initialization of view_query->link to ipc.c --- src/ipc.c | 3 +++ src/view.c | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ipc.c b/src/ipc.c index 92f57bf7..f3f07c65 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -989,6 +989,9 @@ parse_criteria(const char *cmd, struct view_query **query) const char *p = cmd + 1; *query = view_query_create(); + /* Allow safe destruction of view query */ + wl_list_init(&(*query)->link); + while (*p && *p != ']') { while (*p == ' ') { p++; diff --git a/src/view.c b/src/view.c index 08a7d0c5..ca959185 100644 --- a/src/view.c +++ b/src/view.c @@ -81,7 +81,6 @@ 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; From 0076c528b7df0fc04098d3ccc5bfcdb66abe0ea3 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Thu, 16 Apr 2026 00:27:57 +0200 Subject: [PATCH 07/13] Add ipc_event_window_geometry() helper This allows creating events based on different labwc internal geometries. Also use ipc internal geometry for clients and not just deduplication. --- include/ipc.h | 3 ++- src/input/cursor.c | 17 ----------------- src/ipc.c | 22 ++++++++++++++++++++-- src/resize-outlines.c | 4 ++++ src/view.c | 25 ++++--------------------- 5 files changed, 30 insertions(+), 41 deletions(-) diff --git a/include/ipc.h b/include/ipc.h index eb69f1db..7b563693 100644 --- a/include/ipc.h +++ b/include/ipc.h @@ -71,6 +71,7 @@ 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_window_geometry(struct view *view, struct wlr_box *new_geo); void ipc_event_shutdown(void); -#endif /* LABWC_IPC_H */ \ No newline at end of file +#endif /* LABWC_IPC_H */ diff --git a/src/input/cursor.c b/src/input/cursor.c index a66cee02..e8ac7d96 100644 --- a/src/input/cursor.c +++ b/src/input/cursor.c @@ -28,7 +28,6 @@ #include "input/keyboard.h" #include "input/tablet.h" #include "input/touch.h" -#include "ipc.h" #include "labwc.h" #include "layers.h" #include "menu/menu.h" @@ -375,22 +374,6 @@ process_cursor_resize(uint32_t time) view_move_resize(view, new_view_geo); } else { resize_outlines_update(view, new_view_geo); - if (view->mapped) { - view->current = new_view_geo; - struct wlr_box *last = &view->ipc_last_geo; - if (new_view_geo.x != last->x - || new_view_geo.y != last->y) { - ipc_event_window("move", view); - } - if (new_view_geo.width != last->width - || new_view_geo.height != last->height) { - ipc_event_window("resize", view); - } - last->x = new_view_geo.x; - last->y = new_view_geo.y; - last->width = new_view_geo.width; - last->height = new_view_geo.height; - } } } diff --git a/src/ipc.c b/src/ipc.c index f3f07c65..ec062338 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -631,9 +631,9 @@ ipc_json_view_node(struct view *view) /* geometry */ json_object_object_add(obj, "position", - ipc_json_position(view->current)); + ipc_json_position(view->ipc_last_geo)); json_object_object_add(obj, "dimension", - ipc_json_dimension(view->current)); + ipc_json_dimension(view->ipc_last_geo)); return obj; } @@ -1707,6 +1707,24 @@ ipc_event_window(const char *change, struct view *view) ipc_broadcast_event(IPC_EVENT_WINDOW, IPC_SUB_WINDOW, event); } +void +ipc_event_window_geometry(struct view *view, struct wlr_box *new_geo) +{ + struct wlr_box *last = &view->ipc_last_geo; + + bool moved = new_geo->x != last->x || new_geo->y != last->y; + bool resized = new_geo->width != last->width || new_geo->height != last->height; + *last = *new_geo; + + if (moved) { + ipc_event_window("move", view); + } + + if (resized) { + ipc_event_window("resize", view); + } +} + void ipc_event_shutdown(void) { diff --git a/src/resize-outlines.c b/src/resize-outlines.c index 113f128d..56127d5e 100644 --- a/src/resize-outlines.c +++ b/src/resize-outlines.c @@ -5,6 +5,7 @@ #include "common/border.h" #include "common/lab-scene-rect.h" #include "config/rcxml.h" +#include "ipc.h" #include "resize-indicator.h" #include "ssd.h" #include "theme.h" @@ -50,6 +51,9 @@ resize_outlines_update(struct view *view, struct wlr_box new_geo) outlines->view_geo = new_geo; + /* Required in case is used */ + ipc_event_window_geometry(view, &new_geo); + resize_indicator_update(view); } diff --git a/src/view.c b/src/view.c index ca959185..1aa89072 100644 --- a/src/view.c +++ b/src/view.c @@ -573,19 +573,11 @@ view_moved(struct view *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. + * fires immediately using view->pending. Events are + * deduplicated in ipc_event_window_geometry(). */ 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; + ipc_event_window_geometry(view, &view->current); } } @@ -618,16 +610,7 @@ view_move_resize(struct view *view, struct wlr_box geo) * 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; + ipc_event_window_geometry(view, &view->pending); } } From c675da04aa1bbff2247842ad15b34955f636ee87 Mon Sep 17 00:00:00 2001 From: stormshadow <190884359+st0rm-shad0w@users.noreply.github.com> Date: Fri, 17 Apr 2026 04:25:44 +0530 Subject: [PATCH 08/13] rcxml: track first loaded config file for IPC --- src/config/rcxml.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/config/rcxml.c b/src/config/rcxml.c index ab0639d1..d48dc7e0 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -2030,6 +2030,10 @@ rcxml_read(const char *filename) continue; } + if (!rc.loaded_config_file) { + xstrdup_replace(rc.loaded_config_file, path->string); + } + wlr_log(WLR_INFO, "read config file %s", path->string); rcxml_parse_xml(&b); @@ -2059,6 +2063,7 @@ rcxml_finish(void) zfree(rc.workspace_config.initial_workspace_name); zfree(rc.tablet.output_name); zfree(rc.window_switcher.osd.thumbnail_label_format); + zfree(rc.loaded_config_file); clear_title_layout(); From fa8d19808228c9b72f1bb8134966808acc58390f Mon Sep 17 00:00:00 2001 From: stormshadow <190884359+st0rm-shad0w@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:39:47 +0530 Subject: [PATCH 09/13] ipc: remove forward slash escaping in JSON output --- clients/labmsg.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/clients/labmsg.c b/clients/labmsg.c index ec7e116d..e75bc110 100644 --- a/clients/labmsg.c +++ b/clients/labmsg.c @@ -187,8 +187,9 @@ print_json(const char *data, bool pretty) return; } - int flags = pretty ? (JSON_C_TO_STRING_PRETTY | JSON_C_TO_STRING_SPACED) - : JSON_C_TO_STRING_PLAIN; + int flags = JSON_C_TO_STRING_NOSLASHESCAPE + | (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); From 523fa92a09cb879d759e8de492f722a945155cc6 Mon Sep 17 00:00:00 2001 From: stormshadow <190884359+st0rm-shad0w@users.noreply.github.com> Date: Mon, 20 Apr 2026 01:01:18 +0530 Subject: [PATCH 10/13] ipc: add sscanf return checks and clean up formatting --- clients/labmsg.c | 13 ++++++++----- src/ipc.c | 39 +++++++++++++++++++++++++-------------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/clients/labmsg.c b/clients/labmsg.c index e75bc110..3392007b 100644 --- a/clients/labmsg.c +++ b/clients/labmsg.c @@ -187,9 +187,12 @@ print_json(const char *data, bool pretty) return; } - int flags = JSON_C_TO_STRING_NOSLASHESCAPE - | (pretty ? (JSON_C_TO_STRING_PRETTY | JSON_C_TO_STRING_SPACED) - : JSON_C_TO_STRING_PLAIN); + int flags = JSON_C_TO_STRING_NOSLASHESCAPE; + if (pretty) { + flags |= JSON_C_TO_STRING_PRETTY | JSON_C_TO_STRING_SPACED; + } else { + flags |= JSON_C_TO_STRING_PLAIN; + } printf("%s\n", json_object_to_json_string_ext(obj, flags)); json_object_put(obj); @@ -217,7 +220,7 @@ check_command_success(const char *data) /* Print error to stderr */ struct json_object *err = NULL; if (json_object_object_get_ex(item, - "error", &err)) { + "error", &err)) { fprintf(stderr, "Error: %s\n", json_object_get_string( err)); @@ -374,7 +377,7 @@ main(int argc, char *argv[]) while (1) { resp_payload = NULL; if (!ipc_recv(fd, &resp_type, &resp_payload, - &resp_len)) { + &resp_len)) { break; } if (!quiet && resp_payload) { diff --git a/src/ipc.c b/src/ipc.c index ec062338..566d0c28 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -55,14 +55,14 @@ /* =================================================================== * 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 struct wl_list ipc_clients; - 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); @@ -317,7 +317,8 @@ ipc_finish(void) /* =================================================================== * Section 2: JSON Builders - * =================================================================== */ + * =================================================================== + */ static struct json_object * ipc_json_position(struct wlr_box box) @@ -756,7 +757,9 @@ ipc_json_get_version(void) /* 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); + if (sscanf(ver, "%d.%d.%d", &major, &minor, &patch) < 3) { + wlr_log(WLR_ERROR, "IPC: failed to parse version: %s", ver); + } json_object_object_add(obj, "major", json_object_new_int(major)); json_object_object_add(obj, "minor", json_object_new_int(minor)); @@ -864,8 +867,8 @@ ipc_json_input_device(struct input *input) 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_new_string( + active_name ? active_name : "")); json_object_object_add(obj, "xkb_active_layout_index", json_object_new_int(active)); @@ -874,8 +877,8 @@ ipc_json_input_device(struct input *input) const char *name = xkb_keymap_layout_get_name(keymap, i); json_object_array_add(layouts, - json_object_new_string(name ? name - : "")); + json_object_new_string( + name ? name : "")); } json_object_object_add(obj, "xkb_layout_names", layouts); @@ -932,7 +935,8 @@ ipc_json_get_seats(void) /* =================================================================== * Section 3: Sway-Compatible Command Parser - * =================================================================== */ + * =================================================================== + */ /* * Helper to create an action, add a single string arg, execute it via @@ -1251,7 +1255,9 @@ execute_single_command(const char *cmd) return cmd_result(false, "No focused window"); } int x = 0, y = 0; - sscanf(arg + 9, "%d %d", &x, &y); + if (sscanf(arg + 9, "%d %d", &x, &y) < 2) { + return cmd_result(false, "Invalid position arguments"); + } struct action *a = action_create("MoveTo"); if (a) { action_arg_add_int(a, "x", x); @@ -1358,7 +1364,9 @@ execute_single_command(const char *cmd) /* resize set W H */ if (!strncmp(arg, "set ", 4)) { int w = 0, h = 0; - sscanf(arg + 4, "%d %d", &w, &h); + if (sscanf(arg + 4, "%d %d", &w, &h) < 2) { + return cmd_result(false, "Invalid resize arguments"); + } struct action *a = action_create("ResizeTo"); if (a) { action_arg_add_int(a, "width", w); @@ -1467,7 +1475,8 @@ ipc_cmd_run(const char *payload) /* =================================================================== * Section 3b: SUBSCRIBE handler - * =================================================================== */ + * =================================================================== + */ static struct json_object * ipc_cmd_subscribe(struct ipc_client *client, const char *payload) @@ -1512,7 +1521,8 @@ ipc_cmd_subscribe(struct ipc_client *client, const char *payload) /* =================================================================== * Message dispatch - * =================================================================== */ + * =================================================================== + */ static void ipc_handle_message(struct ipc_client *client, uint32_t type, @@ -1604,7 +1614,8 @@ ipc_handle_message(struct ipc_client *client, uint32_t type, /* =================================================================== * Section 4: Event Emitters - * =================================================================== */ + * =================================================================== + */ static void ipc_broadcast_event(uint32_t event_type, uint32_t sub_bit, From 5e09725c01f9113b6d74c7ad119614dd0ded297e Mon Sep 17 00:00:00 2001 From: stormshadow <190884359+st0rm-shad0w@users.noreply.github.com> Date: Mon, 20 Apr 2026 01:58:30 +0530 Subject: [PATCH 11/13] docs: add labmsg(1) man page for IPC client --- docs/labmsg.1.scd | 256 ++++++++++++++++++++++++++++++++++++++++++++++ docs/labwc.1.scd | 9 +- docs/meson.build | 1 + 3 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 docs/labmsg.1.scd diff --git a/docs/labmsg.1.scd b/docs/labmsg.1.scd new file mode 100644 index 00000000..f371ad42 --- /dev/null +++ b/docs/labmsg.1.scd @@ -0,0 +1,256 @@ +labmsg(1) + +# NAME + +labmsg - IPC client for labwc + +# SYNOPSIS + +_labmsg_ [options...] [message] + +# DESCRIPTION + +labmsg is a command-line IPC client for *labwc*(1). It communicates with a +running labwc instance over a UNIX domain socket using a swaymsg-compatible +binary protocol. + +The socket path is obtained from the *LABWC_IPC_SOCK* environment variable, +which labwc sets automatically for child processes. It can also be specified +explicitly with the *-s* option. + +# OPTIONS + +*-h, --help* + Show help message and quit. + +*-m, --monitor* + After sending the initial message, remain connected and continuously + print events received from the compositor. Typically used with + *-t subscribe*. + +*-p, --pretty* + Force pretty-printed (indented) JSON output. + +*-q, --quiet* + Suppress response output. The exit code still reflects success or + failure. + +*-r, --raw* + Force compact (single-line) JSON output. When neither *-p* nor *-r* is + given, labmsg auto-detects: pretty if stdout is a terminal, raw + otherwise. + +*-s, --socket* + Use _path_ as the IPC socket instead of *LABWC_IPC_SOCK*. + +*-t, --type* + Set the IPC message type. The default is *run_command*. See *MESSAGE + TYPES* below for the full list. + +*-v, --version* + Show the version number and quit. + +# MESSAGE TYPES + +The following message types are supported via *-t*: + +*run_command* + Execute one or more compositor commands (the default). Commands are + passed as the trailing message argument. Multiple commands can be + separated by semicolons. See *COMMANDS* below. + +*get_workspaces* + Return a JSON array of workspace objects, each containing _num_, _name_, + _visible_, _focused_, _urgent_, _output_, and _rect_ fields. + +*subscribe* + Subscribe to compositor events. The message payload must be a JSON array + of event type names. See *EVENTS* below. Typically combined with *-m*. + +*get_outputs* + Return a JSON array of output objects with properties such as _name_, + _make_, _model_, _serial_, _active_, _scale_, _transform_, + _current_workspace_, _modes_, _current_mode_, and _rect_. + +*get_tree* + Return the full window tree as a nested JSON object. The root contains + output nodes, each containing workspace nodes, each containing + floating view nodes. + +*get_bar_config* + Return bar configuration. Since labwc has no built-in bar, this returns + an empty array (or empty object if a bar ID is given). + +*get_version* + Return a JSON object with _major_, _minor_, _patch_, + _human_readable_, and _loaded_config_file_name_ fields. + +*get_config* + Return a JSON object with a _config_ field containing the raw text of + the currently loaded rc.xml configuration file. + +*send_tick* + Broadcast a tick event to all subscribers. The message payload is + forwarded as the tick _payload_ string. + +*get_inputs* + Return a JSON array of input device objects with _identifier_, _name_, + _vendor_, _product_, _type_, and device-specific fields such as + _xkb_active_layout_name_ for keyboards or _scroll_factor_ for + pointers. + +*get_seats* + Return a JSON array of seat objects with _name_, _capabilities_, + _focus_, and a _devices_ array. + +# COMMANDS + +When the message type is *run_command* (the default), the trailing arguments +form a command string. Multiple commands can be separated by semicolons (*;*). + +Commands may be prefixed with a criteria block to target specific windows: + + \[app_id="" title="" class=""\] + +If no criteria are given, the command operates on the currently focused window +where applicable. + +The following commands are supported: + +*nop* + No operation. + +*exit* + Exit the compositor. + +*reload* + Reload the compositor configuration. + +*exec* [--no-startup-id] __ + Execute _command_ via the shell. + +*kill* + Close the target window. + +*fullscreen* [toggle] + Toggle fullscreen mode on the target window. + +*floating toggle* + If the target window is tiled, untile it. Otherwise no-op (labwc is a + stacking compositor). + +*sticky toggle* + Toggle the target window's omnipresent (visible on all workspaces) state. + +*border* none|normal|pixel|toggle + Set or toggle the target window's border decoration mode. + +*workspace* __ + Switch to the named workspace. The special names *next*, *prev*, + *next_on_output*, and *prev_on_output* cycle through workspaces. + +*focus output* __|left|right|up|down + Focus the given output by name or direction. + +*move position* __ __ + Move the target window to absolute coordinates. + +*move* [container|window] [to] workspace __ + Move the target window to the named workspace. + +*move* [container|window] [to] output __|left|right|up|down + Move the target window to the given output. + +*move* left|right|up|down [__] + Move the target window by _pixels_ in the given direction (default 10). + +*resize set* __ __ + Resize the target window to exact dimensions. + +*resize* grow|shrink width|height __ [px|ppt] + Resize the target window incrementally (default amount 10). + +# EVENTS + +When using *-t subscribe*, the message payload must be a JSON array of event +type names. For example: + +``` +labmsg -t subscribe -m '["workspace", "window"]' +``` + +The following event types are available: + +*workspace* + Emitted on workspace changes. The event object contains _change_ + (*"focus"*, *"init"*, etc.), _current_, and _old_ fields. + +*output* + Emitted on output changes. Contains a _change_ field. + +*window* + Emitted on window changes. Contains _change_ (*"new"*, *"close"*, + *"focus"*, *"title"*, *"fullscreen_mode"*, *"move"*, *"resize"*) and a + _container_ field with the view properties. + +*shutdown* + Emitted when the compositor is about to exit. Contains _change_ + (*"exit"*). + +*tick* + Emitted when a *send_tick* message is received. Contains _first_ (boolean, + true for the initial tick on subscription) and _payload_ fields. + +# ENVIRONMENT + +*LABWC_IPC_SOCK* + Path to the labwc IPC UNIX domain socket. Set automatically by labwc + for child processes. Overridden by *-s*. + +# EXAMPLES + +Query the current workspaces: + +``` +labmsg -t get_workspaces +``` + +Get the window tree in compact form: + +``` +labmsg -r -t get_tree +``` + +Close the focused window: + +``` +labmsg kill +``` + +Move a specific window to workspace 2: + +``` +labmsg '[app_id="firefox"] move container to workspace 2' +``` + +Subscribe to window events: + +``` +labmsg -t subscribe -m '["window"]' +``` + +Execute a command: + +``` +labmsg exec foot +``` + +Chain multiple commands: + +``` +labmsg 'workspace 3; exec firefox' +``` + +# SEE ALSO + +labwc(1), labwc-actions(5) diff --git a/docs/labwc.1.scd b/docs/labwc.1.scd index 56dfe775..4ce315ba 100644 --- a/docs/labwc.1.scd +++ b/docs/labwc.1.scd @@ -170,6 +170,13 @@ example: *LABWC_DEBUG_FOO=1 labwc*. Enable logging of press and release events for bound keys (generally key-combinations like *Ctrl-Alt-t*). +*LABWC_IPC_SOCK* + Path to the IPC UNIX domain socket for the running labwc instance. + Set automatically by labwc at startup to + _$XDG_RUNTIME_DIR/labwc-ipc..sock_ and exported to child + processes. Used by *labmsg*(1) to connect to the compositor. Can be + overridden to connect to a specific instance. + # SEE ALSO -labwc-actions(5), labwc-config(5), labwc-menu(5), labwc-theme(5) +labmsg(1), labwc-actions(5), labwc-config(5), labwc-menu(5), labwc-theme(5) diff --git a/docs/meson.build b/docs/meson.build index 2f89f2e7..98026ff6 100644 --- a/docs/meson.build +++ b/docs/meson.build @@ -8,6 +8,7 @@ if scdoc.found() 'labwc-menu.5', 'labwc-theme.5', 'labnag.1', + 'labmsg.1', ] foreach manpage : manpages markdown = manpage + '.scd' From 8063daef5251da61236a68e9f622fa008b02a52c Mon Sep 17 00:00:00 2001 From: stormshadow <190884359+st0rm-shad0w@users.noreply.github.com> Date: Wed, 22 Apr 2026 00:16:52 +0530 Subject: [PATCH 12/13] labmsg: use LABWC_VERSION instead of hardcoded version string --- clients/labmsg.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/clients/labmsg.c b/clients/labmsg.c index 3392007b..9b87b71d 100644 --- a/clients/labmsg.c +++ b/clients/labmsg.c @@ -35,8 +35,6 @@ enum ipc_msg_type { 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'}, @@ -283,7 +281,7 @@ main(int argc, char *argv[]) type_str = optarg; break; case 'v': - printf("%s\n", version_str); + printf("labmsg " LABWC_VERSION "\n"); return 0; default: fprintf(stderr, "%s", usage_str); From 0985a44982ef01346832b92a673e4d782fda65c5 Mon Sep 17 00:00:00 2001 From: stormshadow <190884359+st0rm-shad0w@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:04:41 +0530 Subject: [PATCH 13/13] ipc: add focus, focus next, focus prev commands --- docs/labmsg.1.scd | 31 +++++++++++++++++++++ src/ipc.c | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/docs/labmsg.1.scd b/docs/labmsg.1.scd index f371ad42..2454b627 100644 --- a/docs/labmsg.1.scd +++ b/docs/labmsg.1.scd @@ -152,6 +152,19 @@ The following commands are supported: *focus output* __|left|right|up|down Focus the given output by name or direction. +*focus* + Focus the target window. Raises the window to the front and gives it + keyboard focus. Typically used with criteria to focus a specific + window, e.g. *labmsg '[app_id="firefox"] focus'*. + +*focus next* + Focus the next window in z-order below the current one. Wraps to the + topmost window if the current window is at the bottom. + +*focus prev* (or *focus last*) + Focus the previous window in z-order above the current one. Wraps to + the bottommost window if the current window is at the top. + *move position* __ __ Move the target window to absolute coordinates. @@ -251,6 +264,24 @@ Chain multiple commands: labmsg 'workspace 3; exec firefox' ``` +Focus a specific window by application ID: + +``` +labmsg '[app_id="foot"] focus' +``` + +Cycle focus to the next window: + +``` +labmsg 'focus next' +``` + +Cycle focus to the previous window: + +``` +labmsg 'focus prev' +``` + # SEE ALSO labwc(1), labwc-actions(5) diff --git a/src/ipc.c b/src/ipc.c index 566d0c28..bc84e9c5 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -1242,6 +1242,74 @@ execute_single_command(const char *cmd) return cmd_result(true, NULL); } + /* --- focus --- */ + if (!strcmp(cmd, "focus")) { + if (!target) { + return cmd_result(false, "No focused window"); + } + desktop_focus_view(target, /*raise*/ true); + return cmd_result(true, NULL); + } + + /* --- focus next --- */ + if (!strcmp(cmd, "focus next")) { + struct view *next = NULL; + struct view *first = NULL; + bool found_active = false; + struct view *view; + for_each_view(view, &server.views, LAB_VIEW_CRITERIA_CURRENT_WORKSPACE) { + if (view->minimized) { + continue; + } + if (!first) { + first = view; + } + if (found_active) { + next = view; + break; + } + if (view == server.active_view) { + found_active = true; + } + } + if (!next) { + next = first; /* wrap */ + } + if (next) { + desktop_focus_view(next, true); + } else { + return cmd_result(false, "No focusable window"); + } + return cmd_result(true, NULL); + } + + /* --- focus prev --- */ + if (!strcmp(cmd, "focus prev") || !strcmp(cmd, "focus last")) { + struct view *prev = NULL; + struct view *last = NULL; + struct view *view; + for_each_view(view, &server.views, LAB_VIEW_CRITERIA_CURRENT_WORKSPACE) { + if (view->minimized) { + continue; + } + if (view == server.active_view) { + break; + } + prev = view; + last = view; /* keep track of last seen for wrap */ + } + if (!prev) { + /* Find the last focusable view for wrap */ + prev = last; + } + if (prev) { + desktop_focus_view(prev, true); + } else { + return cmd_result(false, "No focusable window"); + } + return cmd_result(true, NULL); + } + /* --- move ... --- */ if (!strncmp(cmd, "move ", 5)) { const char *arg = cmd + 5;