4a230d362a
This patch currently supports the following requests: * Run custom commands with arguments (similar to key bind functions) * Get monitor properties * Get all available layouts * Get available tags * Get client properties * Subscribe to tag change, client focus change, and layout change, monitor focus change, focused title change, and client state change events This patch includes a dwm-msg cli program that supports all of the above requests for easy integration into shell scripts. The messages are sent in a JSON format to promote integration to increase scriptability in languages like Python/JavaScript. The patch requires YAJL for JSON parsing and a system with epoll support. Portability is planned to be increased in the future. This patch is best applied after all other patches to avoid merge conflicts. For more info on the IPC implementation and how to send/receive messages, documentation can be found at https://github.com/mihirlad55/dwm-ipc
549 lines
13 KiB
C
549 lines
13 KiB
C
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <inttypes.h>
|
|
#include <stdarg.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <unistd.h>
|
|
#include <yajl/yajl_gen.h>
|
|
|
|
#define IPC_MAGIC "DWM-IPC"
|
|
// clang-format off
|
|
#define IPC_MAGIC_ARR { 'D', 'W', 'M', '-', 'I', 'P', 'C' }
|
|
// clang-format on
|
|
#define IPC_MAGIC_LEN 7 // Not including null char
|
|
|
|
#define IPC_EVENT_TAG_CHANGE "tag_change_event"
|
|
#define IPC_EVENT_CLIENT_FOCUS_CHANGE "client_focus_change_event"
|
|
#define IPC_EVENT_LAYOUT_CHANGE "layout_change_event"
|
|
#define IPC_EVENT_MONITOR_FOCUS_CHANGE "monitor_focus_change_event"
|
|
#define IPC_EVENT_FOCUSED_TITLE_CHANGE "focused_title_change_event"
|
|
#define IPC_EVENT_FOCUSED_STATE_CHANGE "focused_state_change_event"
|
|
|
|
#define YSTR(str) yajl_gen_string(gen, (unsigned char *)str, strlen(str))
|
|
#define YINT(num) yajl_gen_integer(gen, num)
|
|
#define YDOUBLE(num) yajl_gen_double(gen, num)
|
|
#define YBOOL(v) yajl_gen_bool(gen, v)
|
|
#define YNULL() yajl_gen_null(gen)
|
|
#define YARR(body) \
|
|
{ \
|
|
yajl_gen_array_open(gen); \
|
|
body; \
|
|
yajl_gen_array_close(gen); \
|
|
}
|
|
#define YMAP(body) \
|
|
{ \
|
|
yajl_gen_map_open(gen); \
|
|
body; \
|
|
yajl_gen_map_close(gen); \
|
|
}
|
|
|
|
typedef unsigned long Window;
|
|
|
|
const char *DEFAULT_SOCKET_PATH = "/tmp/dwm.sock";
|
|
static int sock_fd = -1;
|
|
static unsigned int ignore_reply = 0;
|
|
|
|
typedef enum IPCMessageType {
|
|
IPC_TYPE_RUN_COMMAND = 0,
|
|
IPC_TYPE_GET_MONITORS = 1,
|
|
IPC_TYPE_GET_TAGS = 2,
|
|
IPC_TYPE_GET_LAYOUTS = 3,
|
|
IPC_TYPE_GET_DWM_CLIENT = 4,
|
|
IPC_TYPE_SUBSCRIBE = 5,
|
|
IPC_TYPE_EVENT = 6
|
|
} IPCMessageType;
|
|
|
|
// Every IPC message must begin with this
|
|
typedef struct dwm_ipc_header {
|
|
uint8_t magic[IPC_MAGIC_LEN];
|
|
uint32_t size;
|
|
uint8_t type;
|
|
} __attribute((packed)) dwm_ipc_header_t;
|
|
|
|
static int
|
|
recv_message(uint8_t *msg_type, uint32_t *reply_size, uint8_t **reply)
|
|
{
|
|
uint32_t read_bytes = 0;
|
|
const int32_t to_read = sizeof(dwm_ipc_header_t);
|
|
char header[to_read];
|
|
char *walk = header;
|
|
|
|
// Try to read header
|
|
while (read_bytes < to_read) {
|
|
ssize_t n = read(sock_fd, header + read_bytes, to_read - read_bytes);
|
|
|
|
if (n == 0) {
|
|
if (read_bytes == 0) {
|
|
fprintf(stderr, "Unexpectedly reached EOF while reading header.");
|
|
fprintf(stderr,
|
|
"Read %" PRIu32 " bytes, expected %" PRIu32 " total bytes.\n",
|
|
read_bytes, to_read);
|
|
return -2;
|
|
} else {
|
|
fprintf(stderr, "Unexpectedly reached EOF while reading header.");
|
|
fprintf(stderr,
|
|
"Read %" PRIu32 " bytes, expected %" PRIu32 " total bytes.\n",
|
|
read_bytes, to_read);
|
|
return -3;
|
|
}
|
|
} else if (n == -1) {
|
|
return -1;
|
|
}
|
|
|
|
read_bytes += n;
|
|
}
|
|
|
|
// Check if magic string in header matches
|
|
if (memcmp(walk, IPC_MAGIC, IPC_MAGIC_LEN) != 0) {
|
|
fprintf(stderr, "Invalid magic string. Got '%.*s', expected '%s'\n",
|
|
IPC_MAGIC_LEN, walk, IPC_MAGIC);
|
|
return -3;
|
|
}
|
|
|
|
walk += IPC_MAGIC_LEN;
|
|
|
|
// Extract reply size
|
|
memcpy(reply_size, walk, sizeof(uint32_t));
|
|
walk += sizeof(uint32_t);
|
|
|
|
// Extract message type
|
|
memcpy(msg_type, walk, sizeof(uint8_t));
|
|
walk += sizeof(uint8_t);
|
|
|
|
(*reply) = malloc(*reply_size);
|
|
|
|
// Extract payload
|
|
read_bytes = 0;
|
|
while (read_bytes < *reply_size) {
|
|
ssize_t n = read(sock_fd, *reply + read_bytes, *reply_size - read_bytes);
|
|
|
|
if (n == 0) {
|
|
fprintf(stderr, "Unexpectedly reached EOF while reading payload.");
|
|
fprintf(stderr, "Read %" PRIu32 " bytes, expected %" PRIu32 " bytes.\n",
|
|
read_bytes, *reply_size);
|
|
free(*reply);
|
|
return -2;
|
|
} else if (n == -1) {
|
|
if (errno == EINTR || errno == EAGAIN) continue;
|
|
free(*reply);
|
|
return -1;
|
|
}
|
|
|
|
read_bytes += n;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
read_socket(IPCMessageType *msg_type, uint32_t *msg_size, char **msg)
|
|
{
|
|
int ret = -1;
|
|
|
|
while (ret != 0) {
|
|
ret = recv_message((uint8_t *)msg_type, msg_size, (uint8_t **)msg);
|
|
|
|
if (ret < 0) {
|
|
// Try again (non-fatal error)
|
|
if (ret == -1 && (errno == EINTR || errno == EAGAIN)) continue;
|
|
|
|
fprintf(stderr, "Error receiving response from socket. ");
|
|
fprintf(stderr, "The connection might have been lost.\n");
|
|
exit(2);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t
|
|
write_socket(const void *buf, size_t count)
|
|
{
|
|
size_t written = 0;
|
|
|
|
while (written < count) {
|
|
const ssize_t n =
|
|
write(sock_fd, ((uint8_t *)buf) + written, count - written);
|
|
|
|
if (n == -1) {
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
|
|
continue;
|
|
else
|
|
return n;
|
|
}
|
|
written += n;
|
|
}
|
|
return written;
|
|
}
|
|
|
|
static void
|
|
connect_to_socket()
|
|
{
|
|
struct sockaddr_un addr;
|
|
|
|
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
|
|
// Initialize struct to 0
|
|
memset(&addr, 0, sizeof(struct sockaddr_un));
|
|
|
|
addr.sun_family = AF_UNIX;
|
|
strcpy(addr.sun_path, DEFAULT_SOCKET_PATH);
|
|
|
|
connect(sock, (const struct sockaddr *)&addr, sizeof(struct sockaddr_un));
|
|
|
|
sock_fd = sock;
|
|
}
|
|
|
|
static int
|
|
send_message(IPCMessageType msg_type, uint32_t msg_size, uint8_t *msg)
|
|
{
|
|
dwm_ipc_header_t header = {
|
|
.magic = IPC_MAGIC_ARR, .size = msg_size, .type = msg_type};
|
|
|
|
size_t header_size = sizeof(dwm_ipc_header_t);
|
|
size_t total_size = header_size + msg_size;
|
|
|
|
uint8_t buffer[total_size];
|
|
|
|
// Copy header to buffer
|
|
memcpy(buffer, &header, header_size);
|
|
// Copy message to buffer
|
|
memcpy(buffer + header_size, msg, header.size);
|
|
|
|
write_socket(buffer, total_size);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
is_float(const char *s)
|
|
{
|
|
size_t len = strlen(s);
|
|
int is_dot_used = 0;
|
|
int is_minus_used = 0;
|
|
|
|
// Floats can only have one decimal point in between or digits
|
|
// Optionally, floats can also be below zero (negative)
|
|
for (int i = 0; i < len; i++) {
|
|
if (isdigit(s[i]))
|
|
continue;
|
|
else if (!is_dot_used && s[i] == '.' && i != 0 && i != len - 1) {
|
|
is_dot_used = 1;
|
|
continue;
|
|
} else if (!is_minus_used && s[i] == '-' && i == 0) {
|
|
is_minus_used = 1;
|
|
continue;
|
|
} else
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
is_unsigned_int(const char *s)
|
|
{
|
|
size_t len = strlen(s);
|
|
|
|
// Unsigned int can only have digits
|
|
for (int i = 0; i < len; i++) {
|
|
if (isdigit(s[i]))
|
|
continue;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
is_signed_int(const char *s)
|
|
{
|
|
size_t len = strlen(s);
|
|
|
|
// Signed int can only have digits and a negative sign at the start
|
|
for (int i = 0; i < len; i++) {
|
|
if (isdigit(s[i]))
|
|
continue;
|
|
else if (i == 0 && s[i] == '-') {
|
|
continue;
|
|
} else
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
flush_socket_reply()
|
|
{
|
|
IPCMessageType reply_type;
|
|
uint32_t reply_size;
|
|
char *reply;
|
|
|
|
read_socket(&reply_type, &reply_size, &reply);
|
|
|
|
free(reply);
|
|
}
|
|
|
|
static void
|
|
print_socket_reply()
|
|
{
|
|
IPCMessageType reply_type;
|
|
uint32_t reply_size;
|
|
char *reply;
|
|
|
|
read_socket(&reply_type, &reply_size, &reply);
|
|
|
|
printf("%.*s\n", reply_size, reply);
|
|
fflush(stdout);
|
|
free(reply);
|
|
}
|
|
|
|
static int
|
|
run_command(const char *name, char *args[], int argc)
|
|
{
|
|
const unsigned char *msg;
|
|
size_t msg_size;
|
|
|
|
yajl_gen gen = yajl_gen_alloc(NULL);
|
|
|
|
// Message format:
|
|
// {
|
|
// "command": "<name>",
|
|
// "args": [ ... ]
|
|
// }
|
|
// clang-format off
|
|
YMAP(
|
|
YSTR("command"); YSTR(name);
|
|
YSTR("args"); YARR(
|
|
for (int i = 0; i < argc; i++) {
|
|
if (is_signed_int(args[i])) {
|
|
long long num = atoll(args[i]);
|
|
YINT(num);
|
|
} else if (is_float(args[i])) {
|
|
float num = atof(args[i]);
|
|
YDOUBLE(num);
|
|
} else {
|
|
YSTR(args[i]);
|
|
}
|
|
}
|
|
)
|
|
)
|
|
// clang-format on
|
|
|
|
yajl_gen_get_buf(gen, &msg, &msg_size);
|
|
|
|
send_message(IPC_TYPE_RUN_COMMAND, msg_size, (uint8_t *)msg);
|
|
|
|
if (!ignore_reply)
|
|
print_socket_reply();
|
|
else
|
|
flush_socket_reply();
|
|
|
|
yajl_gen_free(gen);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
get_monitors()
|
|
{
|
|
send_message(IPC_TYPE_GET_MONITORS, 1, (uint8_t *)"");
|
|
print_socket_reply();
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
get_tags()
|
|
{
|
|
send_message(IPC_TYPE_GET_TAGS, 1, (uint8_t *)"");
|
|
print_socket_reply();
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
get_layouts()
|
|
{
|
|
send_message(IPC_TYPE_GET_LAYOUTS, 1, (uint8_t *)"");
|
|
print_socket_reply();
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
get_dwm_client(Window win)
|
|
{
|
|
const unsigned char *msg;
|
|
size_t msg_size;
|
|
|
|
yajl_gen gen = yajl_gen_alloc(NULL);
|
|
|
|
// Message format:
|
|
// {
|
|
// "client_window_id": "<win>"
|
|
// }
|
|
// clang-format off
|
|
YMAP(
|
|
YSTR("client_window_id"); YINT(win);
|
|
)
|
|
// clang-format on
|
|
|
|
yajl_gen_get_buf(gen, &msg, &msg_size);
|
|
|
|
send_message(IPC_TYPE_GET_DWM_CLIENT, msg_size, (uint8_t *)msg);
|
|
|
|
print_socket_reply();
|
|
|
|
yajl_gen_free(gen);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
subscribe(const char *event)
|
|
{
|
|
const unsigned char *msg;
|
|
size_t msg_size;
|
|
|
|
yajl_gen gen = yajl_gen_alloc(NULL);
|
|
|
|
// Message format:
|
|
// {
|
|
// "event": "<event>",
|
|
// "action": "subscribe"
|
|
// }
|
|
// clang-format off
|
|
YMAP(
|
|
YSTR("event"); YSTR(event);
|
|
YSTR("action"); YSTR("subscribe");
|
|
)
|
|
// clang-format on
|
|
|
|
yajl_gen_get_buf(gen, &msg, &msg_size);
|
|
|
|
send_message(IPC_TYPE_SUBSCRIBE, msg_size, (uint8_t *)msg);
|
|
|
|
if (!ignore_reply)
|
|
print_socket_reply();
|
|
else
|
|
flush_socket_reply();
|
|
|
|
yajl_gen_free(gen);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
usage_error(const char *prog_name, const char *format, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, format);
|
|
|
|
fprintf(stderr, "Error: ");
|
|
vfprintf(stderr, format, args);
|
|
fprintf(stderr, "\nusage: %s <command> [...]\n", prog_name);
|
|
fprintf(stderr, "Try '%s help'\n", prog_name);
|
|
|
|
va_end(args);
|
|
exit(1);
|
|
}
|
|
|
|
static void
|
|
print_usage(const char *name)
|
|
{
|
|
printf("usage: %s [options] <command> [...]\n", name);
|
|
puts("");
|
|
puts("Commands:");
|
|
puts(" run_command <name> [args...] Run an IPC command");
|
|
puts("");
|
|
puts(" get_monitors Get monitor properties");
|
|
puts("");
|
|
puts(" get_tags Get list of tags");
|
|
puts("");
|
|
puts(" get_layouts Get list of layouts");
|
|
puts("");
|
|
puts(" get_dwm_client <window_id> Get dwm client proprties");
|
|
puts("");
|
|
puts(" subscribe [events...] Subscribe to specified events");
|
|
puts(" Options: " IPC_EVENT_TAG_CHANGE ",");
|
|
puts(" " IPC_EVENT_LAYOUT_CHANGE ",");
|
|
puts(" " IPC_EVENT_CLIENT_FOCUS_CHANGE ",");
|
|
puts(" " IPC_EVENT_MONITOR_FOCUS_CHANGE ",");
|
|
puts(" " IPC_EVENT_FOCUSED_TITLE_CHANGE ",");
|
|
puts(" " IPC_EVENT_FOCUSED_STATE_CHANGE);
|
|
puts("");
|
|
puts(" help Display this message");
|
|
puts("");
|
|
puts("Options:");
|
|
puts(" --ignore-reply Don't print reply messages from");
|
|
puts(" run_command and subscribe.");
|
|
puts("");
|
|
}
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
const char *prog_name = argv[0];
|
|
|
|
connect_to_socket();
|
|
if (sock_fd == -1) {
|
|
fprintf(stderr, "Failed to connect to socket\n");
|
|
return 1;
|
|
}
|
|
|
|
int i = 1;
|
|
if (i < argc && strcmp(argv[i], "--ignore-reply") == 0) {
|
|
ignore_reply = 1;
|
|
i++;
|
|
}
|
|
|
|
if (i >= argc) usage_error(prog_name, "Expected an argument, got none");
|
|
|
|
if (strcmp(argv[i], "help") == 0)
|
|
print_usage(prog_name);
|
|
else if (strcmp(argv[i], "run_command") == 0) {
|
|
if (++i >= argc) usage_error(prog_name, "No command specified");
|
|
// Command name
|
|
char *command = argv[i];
|
|
// Command arguments are everything after command name
|
|
char **command_args = argv + ++i;
|
|
// Number of command arguments
|
|
int command_argc = argc - i;
|
|
run_command(command, command_args, command_argc);
|
|
} else if (strcmp(argv[i], "get_monitors") == 0) {
|
|
get_monitors();
|
|
} else if (strcmp(argv[i], "get_tags") == 0) {
|
|
get_tags();
|
|
} else if (strcmp(argv[i], "get_layouts") == 0) {
|
|
get_layouts();
|
|
} else if (strcmp(argv[i], "get_dwm_client") == 0) {
|
|
if (++i < argc) {
|
|
if (is_unsigned_int(argv[i])) {
|
|
Window win = atol(argv[i]);
|
|
get_dwm_client(win);
|
|
} else
|
|
usage_error(prog_name, "Expected unsigned integer argument");
|
|
} else
|
|
usage_error(prog_name, "Expected the window id");
|
|
} else if (strcmp(argv[i], "subscribe") == 0) {
|
|
if (++i < argc) {
|
|
for (int j = i; j < argc; j++) subscribe(argv[j]);
|
|
} else
|
|
usage_error(prog_name, "Expected event name");
|
|
// Keep listening for events forever
|
|
while (1) {
|
|
print_socket_reply();
|
|
}
|
|
} else
|
|
usage_error(prog_name, "Invalid argument '%s'", argv[i]);
|
|
|
|
return 0;
|
|
}
|