mirror of
https://github.com/labwc/labwc.git
synced 2026-06-13 14:33:18 -04:00
Merge 0985a44982 into f4b9bdab65
This commit is contained in:
commit
81666b458d
25 changed files with 2674 additions and 3 deletions
391
clients/labmsg.c
Normal file
391
clients/labmsg.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
287
docs/labmsg.1.scd
Normal file
287
docs/labmsg.1.scd
Normal 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)
|
||||
|
|
@ -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.<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
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ if scdoc.found()
|
|||
'labwc-menu.5',
|
||||
'labwc-theme.5',
|
||||
'labnag.1',
|
||||
'labmsg.1',
|
||||
]
|
||||
foreach manpage : manpages
|
||||
markdown = manpage + '.scd'
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
77
include/ipc.h
Normal file
77
include/ipc.h
Normal 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 */
|
||||
|
|
@ -324,6 +324,8 @@ struct server {
|
|||
pid_t primary_client_pid;
|
||||
|
||||
char *title_fmt;
|
||||
|
||||
struct wl_list ipc_clients;
|
||||
};
|
||||
|
||||
/* defined in main.c */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 += [
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
#include "foreign-toplevel/foreign.h"
|
||||
#include <assert.h>
|
||||
#include <wlr/types/wlr_ext_foreign_toplevel_list_v1.h>
|
||||
#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 "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ labwc_sources = files(
|
|||
'edges.c',
|
||||
'idle.c',
|
||||
'interactive.c',
|
||||
'ipc.c',
|
||||
'layers.c',
|
||||
'magnifier.c',
|
||||
'main.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);
|
||||
|
|
|
|||
|
|
@ -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 <resize drawContents="no" /> is used */
|
||||
ipc_event_window_geometry(view, &new_geo);
|
||||
|
||||
resize_indicator_update(view);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
22
src/view.c
22
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"
|
||||
|
|
@ -567,6 +568,17 @@ 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. Events are
|
||||
* deduplicated in ipc_event_window_geometry().
|
||||
*/
|
||||
if (view->mapped) {
|
||||
ipc_event_window_geometry(view, &view->current);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
|
@ -590,6 +602,16 @@ 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) {
|
||||
ipc_event_window_geometry(view, &view->pending);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -884,6 +886,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
|
||||
|
|
@ -893,6 +897,7 @@ handle_unmap(struct wl_listener *listener, void *data)
|
|||
if (view->mapped) {
|
||||
view->mapped = false;
|
||||
view_impl_unmap(view);
|
||||
ipc_event_window("close", view);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue