commit 4fc826c2d403ff17d96ffca90f7fd96be3e7fc35 from: mtmn date: Sat Jul 11 15:02:53 2026 UTC add wlr-sway-title commit - f625c3f4b413f4308be2f654ce502e3e06a0727f commit + 4fc826c2d403ff17d96ffca90f7fd96be3e7fc35 blob - 5d80e59e421d084f2d1500aafc346b271a312427 blob + 1572e329fa24a84dbeb3a808663fc79c745bb3ca --- README.md +++ README.md @@ -19,6 +19,7 @@ Various tools I have been using throughout the years. | [speediness](speediness/) | network speed test | | [tempfox](tempfox/) | Firefox user.js preference overrides | | [wlr-river-title](wlr-river-title/) | River/Wayland focused view title printer | +| [wlr-sway-title](wlr-sway-title/) | Sway/Wayland focused view title printer | | [wrd](wrd/) | full-text search and reader for an offline web archive | | [plants](plants/) | bluetooth battery monitoring for linux | blob - /dev/null blob + fa745fcd808bb68617018cc69c1f10d868c797ea (mode 644) --- /dev/null +++ wlr-sway-title/BUILD @@ -0,0 +1,25 @@ +load("@rules_cc//cc:defs.bzl", "cc_binary") +load("//bazel:local-deploy.bzl", "local_deploy") + +package(default_visibility = ["//visibility:public"]) + +COMMON_COPTS = [ + "-Wall", + "-Wextra", + "-O2", + "-fanalyzer", + "-Wshadow", +] + +cc_binary( + name = "wlr-sway-title", + srcs = ["wlr-sway-title.c"], + copts = COMMON_COPTS, +) + +local_deploy( + name = "deploy", + srcs = [ + ":wlr-sway-title", + ], +) \ No newline at end of file blob - /dev/null blob + 7117821834ef5b0692672f7d3a8f38a1727325d9 (mode 644) --- /dev/null +++ wlr-sway-title/README.md @@ -0,0 +1,19 @@ +# wlr-sway-title + +Prints the title of the currently focused view in a [sway](https://github.com/swaywm/sway)-compatible Wayland compositor (sway, swayfx, volare, …) via the i3 IPC protocol and exits. Useful for feeding into status bars or scripts like `monofetch`. + +## Building + +```sh +make +# or +cc wlr-sway-title.c -o wlr-sway-title +``` + +## Usage + +```sh +wlr-sway-title +``` + +Prints one line to stdout and exits. Reads the IPC socket from `SWAYSOCK`. Exits with status 1 if `SWAYSOCK` is unset, the socket cannot be connected to, or the IPC reply is malformed. \ No newline at end of file blob - /dev/null blob + b6d9a4421664864d534397c417ecc54ed0ae8f1f (mode 644) --- /dev/null +++ wlr-sway-title/wlr-sway-title.c @@ -0,0 +1,446 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define I3_IPC_MAGIC "i3-ipc" +#define I3_IPC_MAGIC_LEN 6 +#define I3_IPC_MESSAGE_TYPE_GET_TREE 4 + +static int send_request(int sock, uint32_t type, const char *payload) { + size_t payload_len = payload ? strlen(payload) : 0; + char header[I3_IPC_MAGIC_LEN + sizeof(uint32_t) * 2]; + memcpy(header, I3_IPC_MAGIC, I3_IPC_MAGIC_LEN); + uint32_t len_field = (uint32_t)payload_len; + uint32_t type_field = type; + memcpy(header + I3_IPC_MAGIC_LEN, &len_field, sizeof(len_field)); + memcpy(header + I3_IPC_MAGIC_LEN + sizeof(uint32_t), &type_field, + sizeof(type_field)); + + size_t total = sizeof(header) + payload_len; + char *buf = malloc(total); + if (!buf) + return -1; + memcpy(buf, header, sizeof(header)); + if (payload_len) + memcpy(buf + sizeof(header), payload, payload_len); + + size_t sent = 0; + while (sent < total) { + ssize_t n = send(sock, buf + sent, total - sent, 0); + if (n < 0) { + free(buf); + return -1; + } + sent += (size_t)n; + } + free(buf); + return 0; +} + +static int recv_exact(int sock, char *buf, size_t need) { + size_t got = 0; + while (got < need) { + ssize_t n = recv(sock, buf + got, need - got, 0); + if (n < 0) + return -1; + if (n == 0) + return -1; + got += (size_t)n; + } + return 0; +} + +/* + * Reads one full IPC message from the socket. The server keeps the connection + * open after replying, so we must parse the header to know the payload length + * rather than waiting for EOF. Returns a malloc'd buffer containing the whole + * message (magic + length + type + payload, NUL-terminated) and points + * *out_payload at the payload start. + */ +static char *recv_message(int sock, char **out_payload, + size_t *out_payload_len) { + char header[I3_IPC_MAGIC_LEN + sizeof(uint32_t) * 2]; + if (recv_exact(sock, header, sizeof(header)) < 0) + return NULL; + if (memcmp(header, I3_IPC_MAGIC, I3_IPC_MAGIC_LEN) != 0) + return NULL; + + uint32_t payload_len; + memcpy(&payload_len, header + I3_IPC_MAGIC_LEN, sizeof(uint32_t)); + + size_t total = sizeof(header) + payload_len; + char *buf = malloc(total + 1); + if (!buf) + return NULL; + memcpy(buf, header, sizeof(header)); + if (payload_len > 0) { + if (recv_exact(sock, buf + sizeof(header), payload_len) < 0) { + free(buf); + return NULL; + } + } + buf[total] = '\0'; + *out_payload = buf + sizeof(header); + if (out_payload_len) + *out_payload_len = payload_len; + return buf; +} + +/* + * Minimal recursive JSON search: find the first object with a "focused":true + * member and return a malloc'd copy of its "name" string value. + * + * This is a tiny purpose-built parser; it is not a general JSON library. + * It walks the tree tracking object nesting and recognises the two keys + * we care about. + */ +struct search_ctx { + int focused_found; + char *name; +}; + +static void skip_ws(const char **p) { + while (**p && isspace((unsigned char)**p)) + (*p)++; +} + +static int match_key(const char **p, const char *key) { + skip_ws(p); + const char *start = *p; + if (**p != '"') + return 0; + (*p)++; + const char *k = key; + while (**p && *k) { + if (**p != *k) { + *p = start; + return 0; + } + (*p)++; + k++; + } + if (**p != '"') { + *p = start; + return 0; + } + (*p)++; + return 1; +} + +static char *parse_string(const char **p) { + skip_ws(p); + if (**p != '"') + return NULL; + (*p)++; + size_t cap = 64; + size_t len = 0; + char *out = malloc(cap); + if (!out) + return NULL; + while (**p && **p != '"') { + char c = **p; + if (c == '\\') { + (*p)++; + c = **p; + switch (c) { + case 'n': + c = '\n'; + break; + case 't': + c = '\t'; + break; + case 'r': + c = '\r'; + break; + case '"': + c = '"'; + break; + case '\\': + c = '\\'; + break; + case '/': + c = '/'; + break; + case 'b': + c = '\b'; + break; + case 'f': + c = '\f'; + break; + case 'u': { + /* skip 4 hex digits, substitute with '?' for simplicity */ + (*p)++; + for (int i = 0; i < 4 && isxdigit((unsigned char)**p); i++) + (*p)++; + c = '?'; + break; + } + default: + break; + } + } + if (len + 1 >= cap) { + cap *= 2; + char *nb = realloc(out, cap); + if (!nb) { + free(out); + return NULL; + } + out = nb; + } + out[len++] = c; + (*p)++; + } + if (**p == '"') + (*p)++; + out[len] = '\0'; + return out; +} + +static void walk_node(const char **p, struct search_ctx *ctx); + +static void skip_value(const char **p); + +static void skip_string(const char **p) { + if (**p != '"') + return; + (*p)++; + while (**p && **p != '"') { + if (**p == '\\') { + (*p)++; + if (!**p) + return; + } + (*p)++; + } + if (**p) + (*p)++; +} + +static void skip_object(const char **p) { + if (**p != '{') + return; + (*p)++; + for (;;) { + skip_ws(p); + if (**p == '}') { + (*p)++; + return; + } + if (**p == ',') { + (*p)++; + continue; + } + skip_string(p); + skip_ws(p); + if (**p == ':') + (*p)++; + skip_value(p); + } +} + +static void skip_array(const char **p) { + if (**p != '[') + return; + (*p)++; + for (;;) { + skip_ws(p); + if (**p == ']') { + (*p)++; + return; + } + if (**p == ',') { + (*p)++; + continue; + } + skip_value(p); + } +} + +static void skip_value(const char **p) { + skip_ws(p); + if (**p == '"') { + skip_string(p); + return; + } + if (**p == '{') { + skip_object(p); + return; + } + if (**p == '[') { + skip_array(p); + return; + } + while (**p && !isspace((unsigned char)**p) && **p != ',' && **p != '}' && + **p != ']') + (*p)++; +} + +static void walk_node(const char **p, struct search_ctx *ctx) { + skip_ws(p); + if (**p != '{') { + skip_value(p); + return; + } + (*p)++; + + int this_focused = 0; + char *this_name = NULL; + + for (;;) { + skip_ws(p); + if (**p == '}') { + (*p)++; + break; + } + if (**p == ',') { + (*p)++; + continue; + } + + if (match_key(p, "focused")) { + skip_ws(p); + if (**p == ':') + (*p)++; + skip_ws(p); + if (strncmp(*p, "true", 4) == 0) { + this_focused = 1; + *p += 4; + } else if (strncmp(*p, "false", 5) == 0) { + *p += 5; + } else { + skip_value(p); + } + continue; + } + + if (match_key(p, "name")) { + skip_ws(p); + if (**p == ':') + (*p)++; + char *s = parse_string(p); + if (s) { + free(this_name); + this_name = s; + } + continue; + } + + if (match_key(p, "nodes") || match_key(p, "floating_nodes")) { + skip_ws(p); + if (**p == ':') + (*p)++; + skip_ws(p); + if (**p != '[') { + skip_value(p); + continue; + } + (*p)++; + for (;;) { + skip_ws(p); + if (**p == ']') { + (*p)++; + break; + } + if (**p == ',') { + (*p)++; + continue; + } + walk_node(p, ctx); + if (ctx->name) { + free(this_name); + this_name = NULL; + this_focused = 0; + goto done; + } + } + continue; + } + + skip_ws(p); + if (**p == '"') { + skip_string(p); + skip_ws(p); + if (**p == ':') + (*p)++; + skip_value(p); + } else { + skip_value(p); + } + } + +done: + if (this_focused) { + ctx->focused_found = 1; + if (!ctx->name && this_name) { + ctx->name = this_name; + this_name = NULL; + } + } + free(this_name); +} + +static char *find_focused_name(char *json) { + const char *p = json; + struct search_ctx ctx = {0}; + walk_node(&p, &ctx); + if (ctx.focused_found && ctx.name) + return ctx.name; + free(ctx.name); + return NULL; +} + +int main(void) { + const char *sock_path = getenv("SWAYSOCK"); + if (!sock_path || !*sock_path) { + fprintf(stderr, "SWAYSOCK is not set\n"); + return 1; + } + + int sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock < 0) { + perror("socket"); + return 1; + } + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1); + + if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("connect"); + close(sock); + return 1; + } + + if (send_request(sock, I3_IPC_MESSAGE_TYPE_GET_TREE, NULL) < 0) { + fprintf(stderr, "failed to send IPC request\n"); + close(sock); + return 1; + } + + char *payload = NULL; + size_t payload_len = 0; + char *data = recv_message(sock, &payload, &payload_len); + close(sock); + if (!data) { + fprintf(stderr, "failed to read IPC reply\n"); + return 1; + } + + char *title = find_focused_name(payload); + if (title) { + printf("%s\n", title); + free(title); + } + + free(data); + return 0; +} \ No newline at end of file