mirror of
https://github.com/labwc/labwc.git
synced 2026-06-13 14:33:18 -04:00
Add swaymsg-compatible IPC interface with labmsg client
This commit is contained in:
parent
bce14a5ad7
commit
8328c05041
20 changed files with 2291 additions and 11 deletions
389
clients/labmsg.c
Normal file
389
clients/labmsg.c
Normal file
|
|
@ -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 <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 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 <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 = 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;
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
76
include/ipc.h
Normal file
76
include/ipc.h
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
/* 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_shutdown(void);
|
||||||
|
|
||||||
|
#endif /* LABWC_IPC_H */
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
/* SPDX-License-Identifier: GPL-2.0-only */
|
/* SPDX-License-Identifier: GPL-2.0-only */
|
||||||
#ifndef LABWC_H
|
#ifndef LABWC_H
|
||||||
#define LABWC_H
|
#define LABWC_H
|
||||||
#include "config.h"
|
|
||||||
#include <wlr/util/box.h>
|
#include <wlr/util/box.h>
|
||||||
#include <wlr/util/log.h>
|
#include <wlr/util/log.h>
|
||||||
#include "common/set.h"
|
#include "common/set.h"
|
||||||
|
#include "config.h"
|
||||||
#include "cycle.h"
|
#include "cycle.h"
|
||||||
#include "input/cursor.h"
|
#include "input/cursor.h"
|
||||||
#include "overlay.h"
|
#include "overlay.h"
|
||||||
|
|
@ -65,9 +65,9 @@ struct seat {
|
||||||
struct input_method_relay *input_method_relay;
|
struct input_method_relay *input_method_relay;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cursor context saved when a mouse button is pressed on a view/surface.
|
* Cursor context saved when a mouse button is pressed on a
|
||||||
* It is used to send cursor motion events to a surface even though
|
* view/surface. It is used to send cursor motion events to a surface
|
||||||
* the cursor has left the surface in the meantime.
|
* even though the cursor has left the surface in the meantime.
|
||||||
*
|
*
|
||||||
* This allows to keep dragging a scrollbar or selecting text even
|
* This allows to keep dragging a scrollbar or selecting text even
|
||||||
* when moving outside of the window.
|
* when moving outside of the window.
|
||||||
|
|
@ -245,7 +245,8 @@ struct server {
|
||||||
*/
|
*/
|
||||||
struct wlr_scene_tree *xdg_popup_tree;
|
struct wlr_scene_tree *xdg_popup_tree;
|
||||||
#if HAVE_XWAYLAND
|
#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;
|
struct wlr_scene_tree *unmanaged_tree;
|
||||||
#endif
|
#endif
|
||||||
struct wlr_scene_tree *cycle_preview_tree;
|
struct wlr_scene_tree *cycle_preview_tree;
|
||||||
|
|
@ -254,7 +255,7 @@ struct server {
|
||||||
|
|
||||||
/* Workspaces */
|
/* Workspaces */
|
||||||
struct {
|
struct {
|
||||||
struct wl_list all; /* struct workspace.link */
|
struct wl_list all; /* struct workspace.link */
|
||||||
struct workspace *current;
|
struct workspace *current;
|
||||||
struct workspace *last;
|
struct workspace *last;
|
||||||
struct wlr_ext_workspace_manager_v1 *ext_manager;
|
struct wlr_ext_workspace_manager_v1 *ext_manager;
|
||||||
|
|
@ -324,6 +325,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 */
|
||||||
|
|
@ -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_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);
|
void seat_output_layout_changed(struct seat *seat);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -457,7 +461,6 @@ void server_start(void);
|
||||||
void server_finish(void);
|
void server_finish(void);
|
||||||
|
|
||||||
void create_constraint(struct wl_listener *listener, void *data);
|
void create_constraint(struct wl_listener *listener, void *data);
|
||||||
void constrain_cursor(struct wlr_pointer_constraint_v1
|
void constrain_cursor(struct wlr_pointer_constraint_v1 *constraint);
|
||||||
*constraint);
|
|
||||||
|
|
||||||
#endif /* LABWC_H */
|
#endif /* LABWC_H */
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 += [
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 "";
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
40
src/view.c
40
src/view.c
|
|
@ -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"
|
||||||
|
|
@ -80,6 +81,7 @@ struct view_query *
|
||||||
view_query_create(void)
|
view_query_create(void)
|
||||||
{
|
{
|
||||||
struct view_query *query = znew(*query);
|
struct view_query *query = znew(*query);
|
||||||
|
wl_list_init(&query->link);
|
||||||
/* Must be synced with view_matches_rule() in window-rules.c */
|
/* Must be synced with view_matches_rule() in window-rules.c */
|
||||||
query->window_type = LAB_WINDOW_TYPE_INVALID;
|
query->window_type = LAB_WINDOW_TYPE_INVALID;
|
||||||
query->maximized = VIEW_AXIS_INVALID;
|
query->maximized = VIEW_AXIS_INVALID;
|
||||||
|
|
@ -567,6 +569,25 @@ 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. 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
|
void
|
||||||
|
|
@ -590,6 +611,25 @@ 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) {
|
||||||
|
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
|
void
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -867,6 +869,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
|
||||||
|
|
@ -876,6 +880,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue