From fb9add0778fe4898e1a0675a135544eda37d1480 Mon Sep 17 00:00:00 2001 From: HuntedByTheIRS Date: Mon, 29 Jun 2026 01:33:11 -0400 Subject: [PATCH] Add a swaymsg-like IPC, and add some more docs. --- CITATION.cff | 23 + CODE_OF_CONDUCT.md | 121 +++++ Makefile | 4 +- SECURITY.md | 17 + include/ipc_socket.h | 1235 ++++++++++++++++++++++++++++++++++++++++++ src/peachwm.c | 12 +- 6 files changed, 1410 insertions(+), 2 deletions(-) create mode 100644 CITATION.cff create mode 100644 CODE_OF_CONDUCT.md create mode 100644 SECURITY.md create mode 100644 include/ipc_socket.h diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..2b0d823 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,23 @@ +cff-version: 1.2.0 +message: >- + If you use PeachWM in your research or project, please cite it + using the metadata below. +title: PeachWM +version: 0.2 +date-released: 2026 +authors: + - family-names: HuntedByTheIRS + email: huntedbyth3irs@gmail.com +repository-code: >- + https://github.com/HuntedByTheIRS/peachwm +license: GPL-3.0-or-later +preferred-citation: + type: software + authors: + - family-names: HuntedByTheIRS + email: huntedbyth3irs@gmail.com + title: PeachWM + version: 0.2 + year: 2026 + url: >- + https://github.com/HuntedByTheIRS/peachwm diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..7c3c318 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,121 @@ +# Code of Conduct + +## Our Pledge + +We as members, contributors, and maintainers pledge to make participation +in our community a harassment-free experience for everyone, regardless of +age, body size, visible or invisible disability, ethnicity, sex +characteristics, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, +welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our + mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political + attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Project maintainers are responsible for clarifying and enforcing our +standards of acceptable behavior and will take appropriate and fair +corrective action in response to any behavior that they deem +inappropriate, threatening, offensive, or harmful. + +Project maintainers have the right and responsibility to remove, edit, +or reject comments, commits, code, wiki edits, issues, and other +contributions that are not aligned to this Code of Conduct, and will +communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also +applies when an individual is officially representing the community in +public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may +be reported to the project maintainer at **huntedbyth3irs@gmail.com**. +All complaints will be reviewed and investigated promptly and fairly. + +All project maintainers are obligated to respect the privacy and security +of the reporter of any incident. + +## Enforcement Guidelines + +Project maintainers will follow these Community Impact Guidelines in +determining the consequences for any action they deem in violation of +this Code of Conduct: + +### 1. Correction + +**Community Impact:** Use of inappropriate language or other behavior +deemed unprofessional or unwelcome. + +**Consequence:** A private, written warning from project maintainers, +providing clarity around the nature of the violation and an explanation +of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact:** A violation through a single incident or series of +actions. + +**Consequence:** A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, for a specified period of +time. This includes avoiding interactions in community spaces as well +as external channels like social media. Violating these terms may lead +to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact:** A serious violation of community standards, +including sustained inappropriate behavior. + +**Consequence:** A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No +public or private interaction with the people involved, including +unsolicited interaction with those enforcing the Code of Conduct, is +allowed during this period. Violating these terms may lead to a +permanent ban. + +### 4. Permanent Ban + +**Community Impact:** Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of +individuals. + +**Consequence:** A permanent ban from any sort of public interaction +within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +. + +[homepage]: https://www.contributor-covenant.org diff --git a/Makefile b/Makefile index 7054480..da77a8e 100644 --- a/Makefile +++ b/Makefile @@ -66,6 +66,7 @@ debug: CFLAGS += -Werror -Weverything \ -Wno-unreachable-code-break -Wno-missing-format-attribute \ -Wno-format-nonliteral -Wno-float-conversion \ -Wno-tentative-definition-compat -Wno-missing-variable-declarations \ + -Wno-gnu-zero-variadic-macro-arguments \ -g3 -fno-omit-frame-pointer -fsanitize=address,undefined,leak \ -fsanitize-trap=all debug: LDLIBS += -fsanitize=address,undefined,leak @@ -78,7 +79,8 @@ peachwm: src/peachwm.o src/util.o parser/parser.o \ $(CC) $^ $(LDFLAGS) $(LDLIBS) -o $@ src/peachwm.o: src/peachwm.c include/client.h include/ipc.h \ - include/ext-workspace.h src/wlr_ext_workspace_v1.h parser/parser.h \ + include/ipc_socket.h include/ext-workspace.h \ + src/wlr_ext_workspace_v1.h parser/parser.h \ protocols/peachwm-ipc-unstable-v2-protocol.h \ protocols/ext-workspace-v1-protocol.h \ protocols/cursor-shape-v1-protocol.h \ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4e0c1bb --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,17 @@ +# Security Policy + +PeachWM is a Wayland compositor — a window manager. A major security +vulnerability should never really happen, as it's literally just a +window manager. That said, if you find a flaw, please handle it +responsibly. + +## Reporting a Vulnerability + +Do **not** open a public issue disclosing the vulnerability. + +Instead: + +1. Send an email to **huntedbyth3irs@gmail.com** describing the issue. +2. Open a public issue noting that you have sent an email about a security + concern, but **do not** include any technical details about the + vulnerability in the issue body. diff --git a/include/ipc_socket.h b/include/ipc_socket.h new file mode 100644 index 0000000..4b87a02 --- /dev/null +++ b/include/ipc_socket.h @@ -0,0 +1,1235 @@ +/* + * See LICENSE file for copyright and license details. + * PeachWM IPC Socket - swaymsg-compatible Unix domain socket IPC + * + * This is included directly in peachwm.c (similar to ipc.h / client.h). + * It provides a swaymsg-compatible JSON-based IPC over a Unix domain socket, + * allowing external tools like waybar to query peachwm state. + * + * Protocol (sway/i3-compatible): + * Header: "i3-ipc" magic (6 bytes) + * uint32_t payload_length (little-endian) + * uint32_t message_type (little-endian) + * Payload: JSON string (UTF-8) + * + * Message types: + * 0 = COMMAND 1 = GET_WORKSPACES 2 = SUBSCRIBE + * 3 = GET_OUTPUTS 4 = GET_TREE 5 = GET_VERSION + * + * Events (type has 0x80000000 bit set): + * 0x80000000 = workspace 0x80000001 = output + * 0x80000002 = mode 0x80000003 = window + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ------------------------------------------------------------------ */ +/* Protocol constants */ +/* ------------------------------------------------------------------ */ + +#define IPC_MAGIC "i3-ipc" +#define IPC_MAGIC_LEN 6 +#define IPC_HEADER_SIZE 14 /* magic(6) + len(4) + type(4) */ + +enum ipc_message_type { + IPC_COMMAND = 0, + IPC_GET_WORKSPACES = 1, + IPC_SUBSCRIBE = 2, + IPC_GET_OUTPUTS = 3, + IPC_GET_TREE = 4, + IPC_GET_VERSION = 5, +}; + +/* Event types use the high bit (0x80000000) which exceeds INT_MAX. + * Use uint32_t constants instead of enum members to stay C11-clean. */ +#define IPC_EVENT_WORKSPACE ((uint32_t)0x80000000u) +#define IPC_EVENT_OUTPUT ((uint32_t)0x80000001u) +#define IPC_EVENT_MODE ((uint32_t)0x80000002u) +#define IPC_EVENT_WINDOW ((uint32_t)0x80000003u) + +enum ipc_sub_flag { + IPC_SUB_WORKSPACE = 1 << 0, + IPC_SUB_WINDOW = 1 << 1, + IPC_SUB_OUTPUT = 1 << 2, + IPC_SUB_MODE = 1 << 3, +}; + +/* ------------------------------------------------------------------ */ +/* Per-client state */ +/* ------------------------------------------------------------------ */ + +struct ipc_client { + struct wl_list link; + int fd; + uint32_t subscribed; + struct wl_event_source *src; + /* partial-read state */ + uint8_t hdr_buf[IPC_HEADER_SIZE]; + size_t hdr_len; + char *payload; + size_t payload_size; /* total expected payload length */ + size_t payload_len; /* amount read so far */ + size_t payload_alloc; + uint32_t msg_type; + int header_done; +}; + +/* ------------------------------------------------------------------ */ +/* Globals */ +/* ------------------------------------------------------------------ */ + +static int ipc_listen_fd = -1; +static struct wl_event_source *ipc_listen_src; +static struct wl_list ipc_clients; +static struct wl_event_loop *ipc_event_loop; +static char ipc_socket_path[256]; + +/* ------------------------------------------------------------------ */ +/* Simple JSON builder */ +/* ------------------------------------------------------------------ */ + +struct json_writer { + char *buf; + size_t len; + size_t cap; + int depth; + int need_comma[32]; +}; + +static void +json_init(struct json_writer *w) +{ + w->cap = 4096; + w->buf = ecalloc(1, w->cap); + w->len = 0; + w->depth = -1; + memset(w->need_comma, 0, sizeof(w->need_comma)); +} + +static void +json_finish(struct json_writer *w) +{ + free(w->buf); +} + +static void +json_grow(struct json_writer *w, size_t needed) +{ + if (w->len + needed <= w->cap) + return; + while (w->cap < w->len + needed) + w->cap *= 2; + w->buf = realloc(w->buf, w->cap); + if (!w->buf) + die("json_grow: realloc:"); +} + +static void +json_raw(struct json_writer *w, const char *s, size_t n) +{ + json_grow(w, n); + memcpy(w->buf + w->len, s, n); + w->len += n; +} + +static void +json_puts(struct json_writer *w, const char *s) +{ + json_raw(w, s, strlen(s)); +} + +__attribute__((__format__(__printf__, 2, 3))) +static void +json_printf(struct json_writer *w, const char *fmt, ...) +{ + va_list ap; + int n; + json_grow(w, 128); + va_start(ap, fmt); + n = vsnprintf(w->buf + w->len, w->cap - w->len, fmt, ap); + va_end(ap); + if ((size_t)n >= w->cap - w->len) { + json_grow(w, (size_t)n + 1); + va_start(ap, fmt); + n = vsnprintf(w->buf + w->len, w->cap - w->len, fmt, ap); + va_end(ap); + } + w->len += (size_t)n; +} + +static void +json_comma(struct json_writer *w) +{ + if (w->need_comma[w->depth + 1]) + json_puts(w, ","); + w->need_comma[w->depth + 1] = 1; +} + +static void +json_object_start(struct json_writer *w) +{ + json_comma(w); + json_puts(w, "{"); + ++w->depth; + w->need_comma[w->depth + 1] = 0; +} + +static void +json_object_end(struct json_writer *w) +{ + json_puts(w, "}"); + --w->depth; +} + +static void +json_array_start(struct json_writer *w) +{ + json_comma(w); + json_puts(w, "["); + ++w->depth; + w->need_comma[w->depth + 1] = 0; +} + +static void +json_array_end(struct json_writer *w) +{ + json_puts(w, "]"); + --w->depth; +} + +static void +json_key(struct json_writer *w, const char *key) +{ + json_comma(w); + json_printf(w, "\"%s\":", key); +} + +static void +json_string(struct json_writer *w, const char *s) +{ + json_comma(w); + json_puts(w, "\""); + for (; *s; s++) { + switch (*s) { + case '"': json_puts(w, "\\\""); break; + case '\\': json_puts(w, "\\\\"); break; + case '\n': json_puts(w, "\\n"); break; + case '\r': json_puts(w, "\\r"); break; + case '\t': json_puts(w, "\\t"); break; + default: + if ((unsigned char)*s < 0x20) + json_printf(w, "\\u%04x", + (unsigned char)*s); + else + json_raw(w, s, 1); + break; + } + } + json_puts(w, "\""); +} + +static void +json_integer(struct json_writer *w, int64_t val) +{ + json_comma(w); + json_printf(w, "%" PRId64, val); +} + +static void +json_bool(struct json_writer *w, int val) +{ + json_comma(w); + json_puts(w, val ? "true" : "false"); +} + +static void +json_float(struct json_writer *w, double val) +{ + json_comma(w); + json_printf(w, "%.2f", val); +} + +#define json_key_string(w, k, v) do { \ + json_key(w, k); json_string(w, v); } while(0) +#define json_key_int(w, k, v) do { \ + json_key(w, k); json_integer(w, v); } while(0) +#define json_key_bool(w, k, v) do { \ + json_key(w, k); json_bool(w, v); } while(0) +#define json_key_float(w, k, v) do { \ + json_key(w, k); json_float(w, v); } while(0) + +/* ------------------------------------------------------------------ */ +/* Helper: tag index (0-based) -> workspace name */ +/* ------------------------------------------------------------------ */ + +static const char * +ipc_tag_name(int idx) +{ + static const char *names[] = { + "1", "2", "3", "4", "5", "6", "7", "8", "9", + }; + if (idx >= 0 && idx < (int)LENGTH(names)) + return names[idx]; + return "?"; +} + +/* ------------------------------------------------------------------ */ +/* Send a raw message (header + payload) to a client fd */ +/* ------------------------------------------------------------------ */ + +static int +ipc_send_raw(int fd, uint32_t type, const char *payload, size_t len) +{ + uint8_t hdr[IPC_HEADER_SIZE]; + uint32_t nlen = (uint32_t)len; + + memcpy(hdr, IPC_MAGIC, IPC_MAGIC_LEN); + memcpy(hdr + IPC_MAGIC_LEN, &nlen, sizeof(nlen)); + memcpy(hdr + IPC_MAGIC_LEN + 4, &type, sizeof(type)); + + struct iovec iov[2]; + iov[0].iov_base = hdr; + iov[0].iov_len = IPC_HEADER_SIZE; + iov[1].iov_base = (void *)payload; + iov[1].iov_len = len; + + struct msghdr msg = {0}; + msg.msg_iov = iov; + msg.msg_iovlen = 2; + + ssize_t ret; + do { + ret = sendmsg(fd, &msg, MSG_NOSIGNAL); + } while (ret < 0 && errno == EINTR); + + if (ret < 0 && errno != EAGAIN && errno != EWOULDBLOCK) + return -1; + return 0; +} + +static int +ipc_send_json(int fd, uint32_t type, struct json_writer *w) +{ + return ipc_send_raw(fd, type, w->buf, w->len); +} + +/* ------------------------------------------------------------------ */ +/* Broadcast an event to all subscribed clients */ +/* ------------------------------------------------------------------ */ + +static void +ipc_broadcast(uint32_t event_type, struct json_writer *w) +{ + struct ipc_client *c; + uint32_t flag; + + switch (event_type) { + case IPC_EVENT_WORKSPACE: flag = IPC_SUB_WORKSPACE; break; + case IPC_EVENT_OUTPUT: flag = IPC_SUB_OUTPUT; break; + case IPC_EVENT_WINDOW: flag = IPC_SUB_WINDOW; break; + case IPC_EVENT_MODE: flag = IPC_SUB_MODE; break; + default: flag = ~0u; break; + } + + wl_list_for_each(c, &ipc_clients, link) { + if (c->subscribed & flag) + ipc_send_json(c->fd, event_type, w); + } +} + +/* ------------------------------------------------------------------ */ +/* JSON response builders */ +/* ------------------------------------------------------------------ */ + +static void +ipc_build_workspace(struct json_writer *w, Monitor *m, uint32_t tag_bit, + int tag_idx) +{ + Client *c; + int is_urgent = 0; + int visible = !!(m->tagset[m->seltags] & tag_bit); + int num = 0; + + /* compute tag number (1-9) */ + { + uint32_t t = tag_bit; + while (t) { num++; t >>= 1; } + } + + wl_list_for_each(c, &clients, link) { + if (c->mon != m) + continue; + if (c->tags & tag_bit) { + if (c->isurgent) + is_urgent = 1; + } + } + + json_object_start(w); + json_key_int(w, "id", (int64_t)tag_bit + + ((int64_t)(uintptr_t)m << 32)); + json_key_int(w, "num", num); + json_key_string(w, "name", ipc_tag_name(tag_idx)); + json_key_bool(w, "visible", visible); + json_key_bool(w, "focused", visible && m == selmon); + json_key_bool(w, "urgent", !!is_urgent); + + json_key(w, "rect"); + json_object_start(w); + json_key_int(w, "x", m->m.x); + json_key_int(w, "y", m->m.y); + json_key_int(w, "width", m->m.width); + json_key_int(w, "height", m->m.height); + json_object_end(w); + + json_key_string(w, "output", m->wlr_output->name); + json_key_string(w, "layout", m->ltsymbol); + json_key_string(w, "representation", ""); + json_key_string(w, "type", "workspace"); + + json_key(w, "focus"); + json_array_start(w); + c = focustop(m); + if (c && (c->tags & tag_bit)) + json_integer(w, (int64_t)(uintptr_t)c); + json_array_end(w); + + json_key(w, "floating_nodes"); + json_array_start(w); + wl_list_for_each(c, &clients, link) { + if (c->mon == m && (c->tags & tag_bit) && c->isfloating && + !c->isfullscreen) + json_integer(w, (int64_t)(uintptr_t)c); + } + json_array_end(w); + + json_key(w, "nodes"); + json_array_start(w); + wl_list_for_each(c, &clients, link) { + if (c->mon == m && (c->tags & tag_bit) && !c->isfloating && + !c->isfullscreen) + json_integer(w, (int64_t)(uintptr_t)c); + } + json_array_end(w); + json_object_end(w); +} + +static void +ipc_build_output(struct json_writer *w, Monitor *m) +{ + struct wlr_output *o = m->wlr_output; + struct wlr_output_mode *mode = NULL; + int ti; + + if (o->current_mode) + mode = o->current_mode; + else + mode = wlr_output_preferred_mode(o); + + /* Determine current workspace name = first visible tag name */ + const char *ws_name = ""; + for (ti = 0; ti < TAGCOUNT; ti++) { + if (m->tagset[m->seltags] & (1u << ti)) { + ws_name = ipc_tag_name(ti); + break; + } + } + + json_object_start(w); + json_key_string(w, "name", o->name ? o->name : ""); + json_key_string(w, "make", o->make ? o->make : ""); + json_key_string(w, "model", o->model ? o->model : ""); + json_key_string(w, "serial", o->serial ? o->serial : ""); + json_key_bool(w, "active", o->enabled); + json_key_bool(w, "primary", + m == wl_container_of(mons.next, m, link)); + json_key_float(w, "scale", o->scale); + json_key_string(w, "subpixel_hinting", "rgb"); + json_key_string(w, "transform", + o->transform == WL_OUTPUT_TRANSFORM_NORMAL ? + "normal" : "90"); + json_key_string(w, "current_workspace", ws_name); + + json_key(w, "modes"); + json_array_start(w); + if (mode) { + json_object_start(w); + json_key_int(w, "width", mode->width); + json_key_int(w, "height", mode->height); + json_key_int(w, "refresh", mode->refresh); + json_object_end(w); + } + json_array_end(w); + + if (mode) { + json_key(w, "current_mode"); + json_object_start(w); + json_key_int(w, "width", mode->width); + json_key_int(w, "height", mode->height); + json_key_int(w, "refresh", mode->refresh); + json_object_end(w); + } + + json_key(w, "rect"); + json_object_start(w); + json_key_int(w, "x", m->m.x); + json_key_int(w, "y", m->m.y); + json_key_int(w, "width", m->m.width); + json_key_int(w, "height", m->m.height); + json_object_end(w); + json_object_end(w); +} + +static void +ipc_build_client(struct json_writer *w, Client *c) +{ + int floating = c->isfloating || + !c->mon->lt[c->mon->sellt]->arrange; + + json_object_start(w); + json_key_int(w, "id", (int64_t)(uintptr_t)c); + json_key_string(w, "type", "con"); + json_key_string(w, "orientation", "none"); + json_key_int(w, "percent", -1); + json_key_bool(w, "urgent", !!c->isurgent); + json_key_bool(w, "focused", c == focustop(selmon)); + json_key_string(w, "layout", "none"); + json_key_string(w, "app_id", client_get_appid(c)); + json_key_string(w, "name", client_get_title(c)); + json_key_string(w, "title", client_get_title(c)); + json_key_bool(w, "fullscreen_mode", !!c->isfullscreen); + json_key_bool(w, "floating", !!floating); + + json_key(w, "window_properties"); + json_object_start(w); + json_key_string(w, "class", client_get_appid(c)); + json_key_string(w, "instance", client_get_appid(c)); + json_key_string(w, "title", client_get_title(c)); + json_key_bool(w, "transient", 0); + json_object_end(w); + + json_key(w, "marks"); + json_array_start(w); + json_array_end(w); + + json_key(w, "rect"); + json_object_start(w); + json_key_int(w, "x", c->geom.x); + json_key_int(w, "y", c->geom.y); + json_key_int(w, "width", c->geom.width); + json_key_int(w, "height", c->geom.height); + json_object_end(w); + + json_key(w, "window_rect"); + json_object_start(w); + json_key_int(w, "x", (int)c->bw); + json_key_int(w, "y", (int)c->bw); + json_key_int(w, "width", + c->geom.width - 2 * (int)c->bw); + json_key_int(w, "height", + c->geom.height - 2 * (int)c->bw); + json_object_end(w); + + json_key(w, "deco_rect"); + json_object_start(w); + json_key_int(w, "x", 0); + json_key_int(w, "y", 0); + json_key_int(w, "width", 0); + json_key_int(w, "height", 0); + json_object_end(w); + + json_key(w, "geometry"); + json_object_start(w); + json_key_int(w, "x", c->geom.x); + json_key_int(w, "y", c->geom.y); + json_key_int(w, "width", c->geom.width); + json_key_int(w, "height", c->geom.height); + json_object_end(w); + + json_key(w, "border"); + json_object_start(w); + json_key_int(w, "top", (int)c->bw); + json_key_int(w, "bottom", (int)c->bw); + json_key_int(w, "right", (int)c->bw); + json_key_int(w, "left", (int)c->bw); + json_object_end(w); + + json_key_int(w, "current_border_width", (int64_t)c->bw); + + json_key_int(w, "workspace", current_tag_idx(c->mon) + 1); + + json_key(w, "nodes"); + json_array_start(w); + json_array_end(w); + + json_key(w, "floating_nodes"); + json_array_start(w); + json_array_end(w); + json_object_end(w); +} + +/* ------------------------------------------------------------------ */ +/* Command handlers */ +/* ------------------------------------------------------------------ */ + +static void +ipc_handle_get_workspaces(struct ipc_client *client) +{ + struct json_writer w; + Monitor *m; + int i; + + json_init(&w); + json_array_start(&w); + wl_list_for_each(m, &mons, link) { + for (i = 0; i < TAGCOUNT; i++) { + uint32_t tag_bit = 1u << i; + if (m->tagset[m->seltags] & tag_bit) + ipc_build_workspace(&w, m, tag_bit, i); + } + } + json_array_end(&w); + ipc_send_json(client->fd, IPC_GET_WORKSPACES, &w); + json_finish(&w); +} + +static void +ipc_handle_get_outputs(struct ipc_client *client) +{ + struct json_writer w; + Monitor *m; + + json_init(&w); + json_array_start(&w); + wl_list_for_each(m, &mons, link) { + ipc_build_output(&w, m); + } + json_array_end(&w); + ipc_send_json(client->fd, IPC_GET_OUTPUTS, &w); + json_finish(&w); +} + +static void +ipc_handle_get_version(struct ipc_client *client) +{ + struct json_writer w; + + json_init(&w); + json_object_start(&w); + json_key_string(&w, "variant", "peachwm"); + json_key_string(&w, "human_readable", + "peachwm " VERSION); + json_key_int(&w, "major", 0); + json_key_int(&w, "minor", 2); + json_key_int(&w, "patch", 0); + json_key_string(&w, "loaded_config_file_name", ""); + json_object_end(&w); + ipc_send_json(client->fd, IPC_GET_VERSION, &w); + json_finish(&w); +} + +static void +ipc_handle_get_tree(struct ipc_client *client) +{ + struct json_writer w; + Monitor *m; + Client *c; + int i; + + json_init(&w); + + /* root node */ + json_object_start(&w); + json_key_string(&w, "type", "root"); + json_key_string(&w, "name", "root"); + + json_key(&w, "nodes"); + json_array_start(&w); + + wl_list_for_each(m, &mons, link) { + /* output node */ + json_object_start(&w); + json_key_string(&w, "type", "output"); + json_key_string(&w, "name", m->wlr_output->name); + json_key_bool(&w, "active", m->wlr_output->enabled); + + json_key(&w, "rect"); + json_object_start(&w); + json_key_int(&w, "x", m->m.x); + json_key_int(&w, "y", m->m.y); + json_key_int(&w, "width", m->m.width); + json_key_int(&w, "height", m->m.height); + json_object_end(&w); + + json_key(&w, "nodes"); + json_array_start(&w); + + for (i = 0; i < TAGCOUNT; i++) { + uint32_t tag_bit = 1u << i; + if (!(m->tagset[m->seltags] & tag_bit)) + continue; + + /* workspace node */ + json_object_start(&w); + json_key_string(&w, "type", "workspace"); + json_key_string(&w, "name", + ipc_tag_name(i)); + json_key_string(&w, "layout", m->ltsymbol); + + json_key(&w, "rect"); + json_object_start(&w); + json_key_int(&w, "x", m->m.x); + json_key_int(&w, "y", m->m.y); + json_key_int(&w, "width", m->m.width); + json_key_int(&w, "height", m->m.height); + json_object_end(&w); + + /* tiled nodes */ + json_key(&w, "nodes"); + json_array_start(&w); + wl_list_for_each(c, &clients, link) { + if (c->mon == m && + (c->tags & tag_bit) && + !c->isfloating && + !c->isfullscreen) + ipc_build_client(&w, c); + } + json_array_end(&w); + + /* floating nodes */ + json_key(&w, "floating_nodes"); + json_array_start(&w); + wl_list_for_each(c, &clients, link) { + if (c->mon == m && + (c->tags & tag_bit) && + (c->isfloating || + c->isfullscreen)) + ipc_build_client(&w, c); + } + json_array_end(&w); + + json_object_end(&w); /* workspace */ + } + json_array_end(&w); /* output nodes */ + + json_key(&w, "floating_nodes"); + json_array_start(&w); + json_array_end(&w); + + json_object_end(&w); /* output */ + } + json_array_end(&w); /* root nodes */ + + json_key(&w, "floating_nodes"); + json_array_start(&w); + json_array_end(&w); + + json_object_end(&w); /* root */ + + ipc_send_json(client->fd, IPC_GET_TREE, &w); + json_finish(&w); +} + +static void +ipc_handle_command(struct ipc_client *client, const char *payload, + size_t len) +{ + struct json_writer w; + int success = 0; + char *cmd = NULL; + + if (len > 0) { + cmd = ecalloc(1, len + 1); + memcpy(cmd, payload, len); + cmd[len] = '\0'; + } + + if (cmd) { + if (!strncmp(cmd, "focus workspace", 15)) { + const char *ws = cmd + 15; + while (*ws == ' ') ws++; + if (*ws) { + int num = atoi(ws); + if (num >= 1 && num <= TAGCOUNT) { + view(&(Arg){.ui = + 1u << (num - 1)}); + success = 1; + } + } + } else if (!strcmp(cmd, "focus left")) { + focusdir(&(Arg){.i = WLR_DIRECTION_LEFT}); + success = 1; + } else if (!strcmp(cmd, "focus right")) { + focusdir(&(Arg){.i = WLR_DIRECTION_RIGHT}); + success = 1; + } else if (!strcmp(cmd, "focus up")) { + focusdir(&(Arg){.i = WLR_DIRECTION_UP}); + success = 1; + } else if (!strcmp(cmd, "focus down")) { + focusdir(&(Arg){.i = WLR_DIRECTION_DOWN}); + success = 1; + } else if (!strncmp(cmd, "move workspace", 14)) { + const char *ws = cmd + 14; + while (*ws == ' ') ws++; + if (*ws) { + int num = atoi(ws); + if (num >= 1 && num <= TAGCOUNT) { + tag(&(Arg){.ui = + 1u << (num - 1)}); + success = 1; + } + } + } else if (!strcmp(cmd, "floating toggle") || + !strcmp(cmd, "floating enable") || + !strcmp(cmd, "floating disable")) { + togglefloating(NULL); + success = 1; + } else if (!strcmp(cmd, "fullscreen toggle") || + !strcmp(cmd, "fullscreen enable") || + !strcmp(cmd, "fullscreen disable")) { + togglefullscreen(NULL); + success = 1; + } else if (!strcmp(cmd, "kill") || + !strcmp(cmd, "close")) { + killclient(NULL); + success = 1; + } else if (!strncmp(cmd, "workspace", 9)) { + const char *ws = cmd + 9; + while (*ws == ' ') ws++; + if (*ws) { + int num = atoi(ws); + if (num >= 1 && num <= TAGCOUNT) { + view(&(Arg){.ui = + 1u << (num - 1)}); + success = 1; + } + } + } else if (!strcmp(cmd, "exit") || + !strcmp(cmd, "quit")) { + quit(NULL); + success = 1; + } else if (!strcmp(cmd, "reload")) { + success = 1; + } + } + + free(cmd); + + json_init(&w); + json_array_start(&w); + json_object_start(&w); + json_key_bool(&w, "success", success); + if (!success) + json_key_string(&w, "error", + "Unknown or unsupported command"); + json_object_end(&w); + json_array_end(&w); + ipc_send_json(client->fd, IPC_COMMAND, &w); + json_finish(&w); +} + +static void +ipc_handle_subscribe(struct ipc_client *client, const char *payload, + size_t len) +{ + struct json_writer w; + (void)len; + + client->subscribed = 0; + + if (strstr(payload, "\"workspace\"")) + client->subscribed |= IPC_SUB_WORKSPACE; + if (strstr(payload, "\"window\"")) + client->subscribed |= IPC_SUB_WINDOW; + if (strstr(payload, "\"output\"")) + client->subscribed |= IPC_SUB_OUTPUT; + if (strstr(payload, "\"mode\"")) + client->subscribed |= IPC_SUB_MODE; + + json_init(&w); + json_array_start(&w); + json_object_start(&w); + json_key_bool(&w, "success", 1); + json_object_end(&w); + json_array_end(&w); + ipc_send_json(client->fd, IPC_SUBSCRIBE, &w); + json_finish(&w); +} + +/* ------------------------------------------------------------------ */ +/* Client I/O */ +/* ------------------------------------------------------------------ */ + +static void +ipc_client_close(struct ipc_client *c) +{ + wl_list_remove(&c->link); + if (c->src) { + wl_event_source_remove(c->src); + c->src = NULL; + } + close(c->fd); + free(c->payload); + free(c); +} + +static int +ipc_client_handle_readable(int fd, uint32_t mask, void *data) +{ + struct ipc_client *c = data; + ssize_t n; + size_t want; + (void)fd; + + if (mask & (WL_EVENT_ERROR | WL_EVENT_HANGUP)) { + ipc_client_close(c); + return 0; + } + + if (!(mask & WL_EVENT_READABLE)) + return 0; + + if (!c->header_done) { + want = IPC_HEADER_SIZE - c->hdr_len; + n = read(c->fd, c->hdr_buf + c->hdr_len, want); + if (n <= 0) { + if (n == 0 || + (errno != EAGAIN && errno != EWOULDBLOCK)) { + ipc_client_close(c); + return 0; + } + return 0; + } + c->hdr_len += (size_t)n; + if (c->hdr_len < IPC_HEADER_SIZE) + return 0; + + /* Validate magic */ + if (memcmp(c->hdr_buf, IPC_MAGIC, IPC_MAGIC_LEN) != 0) { + ipc_client_close(c); + return 0; + } + + /* Parse header */ + memcpy(&c->payload_size, + c->hdr_buf + IPC_MAGIC_LEN, sizeof(uint32_t)); + memcpy(&c->msg_type, + c->hdr_buf + IPC_MAGIC_LEN + 4, sizeof(uint32_t)); + + if (c->payload_size > 0) { + if (c->payload_size > c->payload_alloc) { + free(c->payload); + c->payload_alloc = c->payload_size + 1; + c->payload = ecalloc(1, c->payload_alloc); + } + c->payload_len = 0; + } + c->header_done = 1; + } + + /* Read payload */ + if (c->header_done && c->payload_size > 0) { + want = c->payload_size - c->payload_len; + if (want > 0) { + n = read(c->fd, + c->payload + c->payload_len, want); + if (n <= 0) { + if (n == 0 || + (errno != EAGAIN && + errno != EWOULDBLOCK)) { + ipc_client_close(c); + return 0; + } + return 0; + } + c->payload_len += (size_t)n; + } + if (c->payload_len < c->payload_size) + return 0; + } + + /* Full message received – dispatch */ + switch (c->msg_type) { + case IPC_COMMAND: + ipc_handle_command(c, c->payload, c->payload_size); + break; + case IPC_GET_WORKSPACES: + ipc_handle_get_workspaces(c); + break; + case IPC_GET_OUTPUTS: + ipc_handle_get_outputs(c); + break; + case IPC_GET_VERSION: + ipc_handle_get_version(c); + break; + case IPC_GET_TREE: + ipc_handle_get_tree(c); + break; + case IPC_SUBSCRIBE: + ipc_handle_subscribe(c, c->payload, c->payload_size); + break; + default: + ipc_send_raw(c->fd, c->msg_type, "{}", 2); + break; + } + + /* Reset for next message */ + c->hdr_len = 0; + c->header_done = 0; + c->payload_len = 0; + c->payload_size = 0; + + return 0; +} + +/* ------------------------------------------------------------------ */ +/* Accept new connections */ +/* ------------------------------------------------------------------ */ + +static int +ipc_handle_accept(int fd, uint32_t mask, void *data) +{ + struct sockaddr_un un; + socklen_t len = sizeof(un); + int cfd; + struct ipc_client *c; + struct wl_event_source *src; + int flags; + + (void)data; + + if (!(mask & WL_EVENT_READABLE)) + return 0; + + cfd = accept(fd, (struct sockaddr *)&un, &len); + if (cfd < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) + wlr_log(WLR_ERROR, "ipc: accept: %s", + strerror(errno)); + return 0; + } + + /* Set non-blocking and close-on-exec */ + flags = fcntl(cfd, F_GETFL); + if (flags < 0 || fcntl(cfd, F_SETFL, flags | O_NONBLOCK) < 0) { + close(cfd); + return 0; + } + + c = ecalloc(1, sizeof(*c)); + c->fd = cfd; + c->hdr_len = 0; + c->header_done = 0; + c->payload_size = 0; + c->payload_len = 0; + c->payload_alloc = 0; + c->payload = NULL; + c->subscribed = 0; + + src = wl_event_loop_add_fd(ipc_event_loop, cfd, + WL_EVENT_READABLE, + ipc_client_handle_readable, c); + if (!src) { + close(cfd); + free(c); + return 0; + } + c->src = src; + + wl_list_insert(&ipc_clients, &c->link); + + wlr_log(WLR_DEBUG, "ipc: client connected (fd %d)", cfd); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* Public API */ +/* ------------------------------------------------------------------ */ + +static void +ipc_socket_init(void) +{ + const char *runtime_dir; + struct sockaddr_un un; + int fd, flags; + char sock_path[256]; + struct passwd *pw; + + wl_list_init(&ipc_clients); + ipc_event_loop = event_loop; + + runtime_dir = getenv("XDG_RUNTIME_DIR"); + if (runtime_dir && runtime_dir[0]) { + snprintf(sock_path, sizeof(sock_path), + "%s/peachwm.sock", runtime_dir); + } else { + pw = getpwuid(getuid()); + if (pw) + snprintf(sock_path, sizeof(sock_path), + "/tmp/peachwm-%s.sock", pw->pw_name); + else + snprintf(sock_path, sizeof(sock_path), + "/tmp/peachwm-%d.sock", getuid()); + } + + unlink(sock_path); + + fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (fd < 0) { + wlr_log(WLR_ERROR, "ipc: socket: %s", strerror(errno)); + return; + } + + memset(&un, 0, sizeof(un)); + un.sun_family = AF_UNIX; + strncpy(un.sun_path, sock_path, sizeof(un.sun_path) - 1); + + if (bind(fd, (struct sockaddr *)&un, sizeof(un)) < 0) { + wlr_log(WLR_ERROR, "ipc: bind(%s): %s", + sock_path, strerror(errno)); + close(fd); + return; + } + + chmod(sock_path, 0700); + + if (listen(fd, 8) < 0) { + wlr_log(WLR_ERROR, "ipc: listen: %s", strerror(errno)); + close(fd); + unlink(sock_path); + return; + } + + flags = fcntl(fd, F_GETFL); + if (flags < 0 || + fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) { + wlr_log(WLR_ERROR, "ipc: fcntl(O_NONBLOCK): %s", + strerror(errno)); + close(fd); + unlink(sock_path); + return; + } + + ipc_listen_fd = fd; + strncpy(ipc_socket_path, sock_path, + sizeof(ipc_socket_path) - 1); + ipc_socket_path[sizeof(ipc_socket_path) - 1] = '\0'; + + ipc_listen_src = wl_event_loop_add_fd( + ipc_event_loop, fd, WL_EVENT_READABLE, + ipc_handle_accept, NULL); + if (!ipc_listen_src) { + wlr_log(WLR_ERROR, + "ipc: wl_event_loop_add_fd failed"); + close(fd); + unlink(sock_path); + ipc_listen_fd = -1; + ipc_socket_path[0] = '\0'; + return; + } + + wlr_log(WLR_DEBUG, "ipc: listening on %s", sock_path); +} + +static void +ipc_socket_finish(void) +{ + struct ipc_client *c, *tmp; + + wl_list_for_each_safe(c, tmp, &ipc_clients, link) + ipc_client_close(c); + + if (ipc_listen_src) { + wl_event_source_remove(ipc_listen_src); + ipc_listen_src = NULL; + } + if (ipc_listen_fd >= 0) { + close(ipc_listen_fd); + if (ipc_socket_path[0]) + unlink(ipc_socket_path); + ipc_listen_fd = -1; + ipc_socket_path[0] = '\0'; + } +} + +/* ------------------------------------------------------------------ */ +/* Event helpers called from peachwm.c */ +/* ------------------------------------------------------------------ */ + +static void +ipc_socket_send_workspace_event(const char *change) +{ + struct json_writer w; + + if (wl_list_empty(&ipc_clients)) + return; + + json_init(&w); + json_object_start(&w); + json_key_string(&w, "change", change ? change : "focus"); + + if (selmon) { + int ti = current_tag_idx(selmon); + uint32_t tag_bit = 1u << ti; + if (selmon->tagset[selmon->seltags] & tag_bit) { + json_key(&w, "current"); + ipc_build_workspace(&w, selmon, tag_bit, ti); + } + } + + json_object_end(&w); + ipc_broadcast((uint32_t)IPC_EVENT_WORKSPACE, &w); + json_finish(&w); +} + +static void +ipc_socket_send_window_event(const char *change) +{ + struct json_writer w; + + if (wl_list_empty(&ipc_clients)) + return; + + json_init(&w); + json_object_start(&w); + json_key_string(&w, "change", change ? change : "focus"); + + if (selmon) { + Client *c = focustop(selmon); + if (c) { + json_key(&w, "container"); + ipc_build_client(&w, c); + } + } + + json_object_end(&w); + ipc_broadcast((uint32_t)IPC_EVENT_WINDOW, &w); + json_finish(&w); +} + +static void +ipc_socket_send_output_event(void) +{ + struct json_writer w; + + if (wl_list_empty(&ipc_clients)) + return; + + json_init(&w); + json_object_start(&w); + json_key_string(&w, "change", "unspecified"); + json_object_end(&w); + ipc_broadcast((uint32_t)IPC_EVENT_OUTPUT, &w); + json_finish(&w); +} diff --git a/src/peachwm.c b/src/peachwm.c index ec6ae42..cf553cb 100644 --- a/src/peachwm.c +++ b/src/peachwm.c @@ -529,6 +529,7 @@ static const char *custom_cfg_path; #include "client.h" #include "ext-workspace.h" #include "ipc.h" +#include "ipc_socket.h" /* function implementations */ @@ -824,6 +825,7 @@ void checkidleinhibitor(struct wlr_surface *exclude) { void cleanup(void) { config_watch_stop(cfg_watch_src); cfg_watch_src = NULL; + ipc_socket_finish(); cleanuplisteners(); #ifdef XWAYLAND @@ -888,6 +890,7 @@ void cleanupmon(struct wl_listener *listener, void *data) { closemon(m); ext_workspace_cleanupmon(m); + ipc_socket_send_output_event(); for (int i = 0; i < TAGCOUNT; i++) { dwindle_free_tree(m->dwindle_root[i]); m->dwindle_root[i] = NULL; @@ -1260,6 +1263,7 @@ void createmon(struct wl_listener *listener, void *data) { wl_list_insert(&mons, &m->link); ext_workspace_createmon(m); printstatus(); + ipc_socket_send_output_event(); /* The xdg-protocol specifies: * @@ -2206,6 +2210,7 @@ void mapnotify(struct wl_listener *listener, void *data) { applyrules(c); } printstatus(); + ipc_socket_send_window_event("new"); unset_fullscreen: m = c->mon ? c->mon : xytomon(c->geom.x, c->geom.y); @@ -2522,6 +2527,7 @@ void printstatus(void) { } fflush(stdout); ipc_printstatus(); + ipc_socket_send_workspace_event("focus"); } void powermgrsetmode(struct wl_listener *listener, void *data) { @@ -2941,6 +2947,7 @@ void setup(void) { dpy = wl_display_create(); event_loop = wl_display_get_event_loop(dpy); ipc_init(); + ipc_socket_init(); workspaces_init(); /* Start the inotify config watcher */ @@ -3815,6 +3822,7 @@ void unmapnotify(struct wl_listener *listener, void *data) { wlr_scene_node_destroy(&c->scene->node); printstatus(); + ipc_socket_send_window_event("close"); motionnotify(0, NULL, 0, 0, 0, 0); } @@ -3918,8 +3926,10 @@ void updatemons(struct wl_listener *listener, void *data) { void updatetitle(struct wl_listener *listener, void *data) { Client *c = wl_container_of(listener, c, set_title); - if (c == focustop(c->mon)) + if (c == focustop(c->mon)) { printstatus(); + ipc_socket_send_window_event("title"); + } } void urgent(struct wl_listener *listener, void *data) {