This commit is contained in:
st0rm-shad0w 2026-06-10 11:40:39 +09:00 committed by GitHub
commit 81666b458d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 2674 additions and 3 deletions

391
clients/labmsg.c Normal file
View file

@ -0,0 +1,391 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* labmsg - IPC client for labwc (swaymsg-compatible interface)
*/
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <json-c/json.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#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 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 <path> Override socket path\n"
" -t, --type <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 = 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);
}
/* 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("labmsg " LABWC_VERSION "\n");
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;
}

View file

@ -59,3 +59,11 @@ endif
clients = files('lab-sensible-terminal') clients = files('lab-sensible-terminal')
install_data(clients, install_dir: get_option('bindir')) install_data(clients, install_dir: get_option('bindir'))
jsonc_client = dependency('json-c')
executable(
'labmsg',
files('labmsg.c'),
dependencies: [jsonc_client],
install: true,
)

287
docs/labmsg.1.scd Normal file
View file

@ -0,0 +1,287 @@
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* <path>
Use _path_ as the IPC socket instead of *LABWC_IPC_SOCK*.
*-t, --type* <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="<pattern>" title="<pattern>" class="<pattern>"\]
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] _<command>_
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* _<name>_
Switch to the named workspace. The special names *next*, *prev*,
*next_on_output*, and *prev_on_output* cycle through workspaces.
*focus output* _<name>_|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* _<x>_ _<y>_
Move the target window to absolute coordinates.
*move* [container|window] [to] workspace _<name>_
Move the target window to the named workspace.
*move* [container|window] [to] output _<name>_|left|right|up|down
Move the target window to the given output.
*move* left|right|up|down [_<pixels>_]
Move the target window by _pixels_ in the given direction (default 10).
*resize set* _<width>_ _<height>_
Resize the target window to exact dimensions.
*resize* grow|shrink width|height _<amount>_ [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'
```
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)

View file

@ -170,6 +170,13 @@ example: *LABWC_DEBUG_FOO=1 labwc*.
Enable logging of press and release events for bound keys (generally Enable logging of press and release events for bound keys (generally
key-combinations like *Ctrl-Alt-t*). 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.<pid>.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 # 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)

View file

@ -8,6 +8,7 @@ if scdoc.found()
'labwc-menu.5', 'labwc-menu.5',
'labwc-theme.5', 'labwc-theme.5',
'labnag.1', 'labnag.1',
'labmsg.1',
] ]
foreach manpage : manpages foreach manpage : manpages
markdown = manpage + '.scd' markdown = manpage + '.scd'

View file

@ -29,6 +29,7 @@ bool action_is_valid(struct action *action);
bool action_is_show_menu(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_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_actionlist(struct action *action, const char *key);
void action_arg_add_querylist(struct action *action, const char *key); void action_arg_add_querylist(struct action *action, const char *key);

View file

@ -73,6 +73,7 @@ struct rcxml {
char *config_dir; char *config_dir;
char *config_file; char *config_file;
bool merge_config; bool merge_config;
char *loaded_config_file;
/* core */ /* core */
bool xdg_shell_server_side_deco; bool xdg_shell_server_side_deco;

View file

@ -9,5 +9,6 @@ struct foreign_toplevel *foreign_toplevel_create(struct view *view);
void foreign_toplevel_set_parent(struct foreign_toplevel *toplevel, void foreign_toplevel_set_parent(struct foreign_toplevel *toplevel,
struct foreign_toplevel *parent); struct foreign_toplevel *parent);
void foreign_toplevel_destroy(struct foreign_toplevel *toplevel); void foreign_toplevel_destroy(struct foreign_toplevel *toplevel);
const char *foreign_toplevel_get_identifier(struct foreign_toplevel *toplevel);
#endif /* LABWC_FOREIGN_TOPLEVEL_H */ #endif /* LABWC_FOREIGN_TOPLEVEL_H */

77
include/ipc.h Normal file
View file

@ -0,0 +1,77 @@
/* SPDX-License-Identifier: GPL-2.0-only */
#ifndef LABWC_IPC_H
#define LABWC_IPC_H
#include <stdbool.h>
#include <stdint.h>
#include <wayland-util.h>
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_window_geometry(struct view *view, struct wlr_box *new_geo);
void ipc_event_shutdown(void);
#endif /* LABWC_IPC_H */

View file

@ -324,6 +324,8 @@ struct server {
pid_t primary_client_pid; pid_t primary_client_pid;
char *title_fmt; char *title_fmt;
struct wl_list ipc_clients;
}; };
/* defined in main.c */ /* defined in main.c */

View file

@ -242,6 +242,12 @@ struct view {
/* Set temporarily when moving view due to layout change */ /* Set temporarily when moving view due to layout change */
bool adjusting_for_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 */ /* used by xdg-shell views */
uint32_t pending_configure_serial; uint32_t pending_configure_serial;
struct wl_event_source *pending_configure_timeout; struct wl_event_source *pending_configure_timeout;

View file

@ -75,6 +75,7 @@ input = dependency('libinput', version: '>=1.26', required: wlroots.get_variable
pixman = dependency('pixman-1') pixman = dependency('pixman-1')
math = cc.find_library('m') math = cc.find_library('m')
png = dependency('libpng') png = dependency('libpng')
jsonc = dependency('json-c')
svg = dependency('librsvg-2.0', version: '>=2.46', required: false) svg = dependency('librsvg-2.0', version: '>=2.46', required: false)
sfdo_basedir = dependency( sfdo_basedir = dependency(
'libsfdo-basedir', 'libsfdo-basedir',
@ -174,6 +175,7 @@ labwc_deps = [
pixman, pixman,
math, math,
png, png,
jsonc,
] ]
if have_rsvg if have_rsvg
labwc_deps += [ labwc_deps += [

View file

@ -203,7 +203,7 @@ action_arg_add_bool(struct action *action, const char *key, bool value)
wl_list_append(&action->args, &arg->base.link); wl_list_append(&action->args, &arg->base.link);
} }
static void void
action_arg_add_int(struct action *action, const char *key, int value) action_arg_add_int(struct action *action, const char *key, int value)
{ {
assert(action); assert(action);

View file

@ -2030,6 +2030,10 @@ rcxml_read(const char *filename)
continue; continue;
} }
if (!rc.loaded_config_file) {
xstrdup_replace(rc.loaded_config_file, path->string);
}
wlr_log(WLR_INFO, "read config file %s", path->string); wlr_log(WLR_INFO, "read config file %s", path->string);
rcxml_parse_xml(&b); rcxml_parse_xml(&b);
@ -2059,6 +2063,7 @@ rcxml_finish(void)
zfree(rc.workspace_config.initial_workspace_name); zfree(rc.workspace_config.initial_workspace_name);
zfree(rc.tablet.output_name); zfree(rc.tablet.output_name);
zfree(rc.window_switcher.osd.thumbnail_label_format); zfree(rc.window_switcher.osd.thumbnail_label_format);
zfree(rc.loaded_config_file);
clear_title_layout(); clear_title_layout();

View file

@ -12,6 +12,7 @@
#include "config/rcxml.h" #include "config/rcxml.h"
#include "dnd.h" #include "dnd.h"
#include "labwc.h" #include "labwc.h"
#include "ipc.h"
#include "layers.h" #include "layers.h"
#include "node.h" #include "node.h"
#include "output.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); struct view *dialog = view_get_modal_dialog(view);
set_or_offer_focus(dialog ? dialog : view); set_or_offer_focus(dialog ? dialog : view);
ipc_event_window("focus", view);
show_desktop_reset(); show_desktop_reset();
} }

View file

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-only // SPDX-License-Identifier: GPL-2.0-only
#include "foreign-toplevel/foreign.h" #include "foreign-toplevel/foreign.h"
#include <assert.h> #include <assert.h>
#include <wlr/types/wlr_ext_foreign_toplevel_list_v1.h>
#include "common/mem.h" #include "common/mem.h"
#include "foreign-toplevel/ext-foreign.h" #include "foreign-toplevel/ext-foreign.h"
#include "foreign-toplevel/wlr-foreign.h" #include "foreign-toplevel/wlr-foreign.h"
@ -27,7 +28,8 @@ foreign_toplevel_create(struct view *view)
} }
void 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); assert(toplevel);
wlr_foreign_toplevel_set_parent(&toplevel->wlr_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); ext_foreign_toplevel_finish(&toplevel->ext_toplevel);
free(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 "";
}

1816
src/ipc.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@ labwc_sources = files(
'edges.c', 'edges.c',
'idle.c', 'idle.c',
'interactive.c', 'interactive.c',
'ipc.c',
'layers.c', 'layers.c',
'magnifier.c', 'magnifier.c',
'main.c', 'main.c',

View file

@ -30,6 +30,7 @@
#include "common/string-helpers.h" #include "common/string-helpers.h"
#include "config/rcxml.h" #include "config/rcxml.h"
#include "labwc.h" #include "labwc.h"
#include "ipc.h"
#include "layers.h" #include "layers.h"
#include "node.h" #include "node.h"
#include "output-state.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->destroy.link);
wl_list_remove(&output->request_state.link); wl_list_remove(&output->request_state.link);
seat_output_layout_changed(seat); seat_output_layout_changed(seat);
ipc_event_output("destroy");
for (size_t i = 0; i < ARRAY_SIZE(output->layer_tree); i++) { for (size_t i = 0; i < ARRAY_SIZE(output->layer_tree); i++) {
wlr_scene_node_destroy(&output->layer_tree[i]->node); wlr_scene_node_destroy(&output->layer_tree[i]->node);

View file

@ -5,6 +5,7 @@
#include "common/border.h" #include "common/border.h"
#include "common/lab-scene-rect.h" #include "common/lab-scene-rect.h"
#include "config/rcxml.h" #include "config/rcxml.h"
#include "ipc.h"
#include "resize-indicator.h" #include "resize-indicator.h"
#include "ssd.h" #include "ssd.h"
#include "theme.h" #include "theme.h"
@ -50,6 +51,9 @@ resize_outlines_update(struct view *view, struct wlr_box new_geo)
outlines->view_geo = new_geo; outlines->view_geo = new_geo;
/* Required in case <resize drawContents="no" /> is used */
ipc_event_window_geometry(view, &new_geo);
resize_indicator_update(view); resize_indicator_update(view);
} }

View file

@ -61,6 +61,7 @@
#include "desktop-entry.h" #include "desktop-entry.h"
#include "idle.h" #include "idle.h"
#include "input/keyboard.h" #include "input/keyboard.h"
#include "ipc.h"
#include "labwc.h" #include "labwc.h"
#include "layers.h" #include "layers.h"
#include "magnifier.h" #include "magnifier.h"
@ -833,6 +834,8 @@ server_start(void)
/* Potentially set up the initial fallback output */ /* Potentially set up the initial fallback output */
output_virtual_update_fallback(); output_virtual_update_fallback();
ipc_init();
if (setenv("WAYLAND_DISPLAY", socket, true) < 0) { if (setenv("WAYLAND_DISPLAY", socket, true) < 0) {
wlr_log_errno(WLR_ERROR, "unable to set WAYLAND_DISPLAY"); wlr_log_errno(WLR_ERROR, "unable to set WAYLAND_DISPLAY");
} else { } else {
@ -881,5 +884,7 @@ server_finish(void)
workspaces_destroy(); workspaces_destroy();
wlr_scene_node_destroy(&server.scene->tree.node); wlr_scene_node_destroy(&server.scene->tree.node);
ipc_finish();
wl_display_destroy(server.wl_display); wl_display_destroy(server.wl_display);
} }

View file

@ -19,6 +19,7 @@
#include "cycle.h" #include "cycle.h"
#include "foreign-toplevel/foreign.h" #include "foreign-toplevel/foreign.h"
#include "input/keyboard.h" #include "input/keyboard.h"
#include "ipc.h"
#include "labwc.h" #include "labwc.h"
#include "menu/menu.h" #include "menu/menu.h"
#include "output.h" #include "output.h"
@ -567,6 +568,17 @@ view_moved(struct view *view)
if (rc.resize_indicator && server.grabbed_view == view) { if (rc.resize_indicator && server.grabbed_view == view) {
resize_indicator_update(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. Events are
* deduplicated in ipc_event_window_geometry().
*/
if (view->mapped) {
ipc_event_window_geometry(view, &view->current);
}
} }
void void
@ -590,6 +602,16 @@ view_move_resize(struct view *view, struct wlr_box geo)
if (!view->adjusting_for_layout_change) { if (!view->adjusting_for_layout_change) {
view_save_last_placement(view); 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) {
ipc_event_window_geometry(view, &view->pending);
}
} }
void void

View file

@ -20,6 +20,7 @@
#include "config/rcxml.h" #include "config/rcxml.h"
#include "input/keyboard.h" #include "input/keyboard.h"
#include "labwc.h" #include "labwc.h"
#include "ipc.h"
#include "output.h" #include "output.h"
#include "show-desktop.h" #include "show-desktop.h"
#include "theme.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); wlr_ext_workspace_handle_v1_set_active(target->ext_workspace, true);
ipc_event_workspace("focus", target, server.workspaces.last);
show_desktop_reset(); show_desktop_reset();
} }

View file

@ -17,6 +17,7 @@
#include "config/rcxml.h" #include "config/rcxml.h"
#include "decorations.h" #include "decorations.h"
#include "foreign-toplevel/foreign.h" #include "foreign-toplevel/foreign.h"
#include "ipc.h"
#include "labwc.h" #include "labwc.h"
#include "menu/menu.h" #include "menu/menu.h"
#include "node.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); struct wlr_xdg_toplevel *toplevel = xdg_toplevel_from_view(view);
view_set_title(view, toplevel->title); view_set_title(view, toplevel->title);
ipc_event_window("title", view);
} }
static void static void
@ -884,6 +886,8 @@ handle_map(struct wl_listener *listener, void *data)
view_impl_map(view); view_impl_map(view);
view->been_mapped = true; view->been_mapped = true;
ipc_event_window("new", view);
view->ipc_last_geo = view->current;
} }
static void static void
@ -893,6 +897,7 @@ handle_unmap(struct wl_listener *listener, void *data)
if (view->mapped) { if (view->mapped) {
view->mapped = false; view->mapped = false;
view_impl_unmap(view); view_impl_unmap(view);
ipc_event_window("close", view);
} }
} }

View file

@ -17,6 +17,7 @@
#include "config/rcxml.h" #include "config/rcxml.h"
#include "config/session.h" #include "config/session.h"
#include "foreign-toplevel/foreign.h" #include "foreign-toplevel/foreign.h"
#include "ipc.h"
#include "labwc.h" #include "labwc.h"
#include "node.h" #include "node.h"
#include "output.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 view *view = wl_container_of(listener, view, set_title);
struct xwayland_view *xwayland_view = xwayland_view_from_view(view); struct xwayland_view *xwayland_view = xwayland_view_from_view(view);
view_set_title(view, xwayland_view->xwayland_surface->title); view_set_title(view, xwayland_view->xwayland_surface->title);
ipc_event_window("title", view);
} }
static void static void
@ -825,6 +827,8 @@ handle_map(struct wl_listener *listener, void *data)
view_impl_map(view); view_impl_map(view);
view->been_mapped = true; view->been_mapped = true;
ipc_event_window("new", view);
view->ipc_last_geo = view->current;
} }
static void static void
@ -836,6 +840,7 @@ handle_unmap(struct wl_listener *listener, void *data)
} }
view->mapped = false; view->mapped = false;
view_impl_unmap(view); view_impl_unmap(view);
ipc_event_window("close", view);
/* /*
* Destroy the content_tree at unmap. Alternatively, we could * Destroy the content_tree at unmap. Alternatively, we could