Commit Diff


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 <ctype.h>
+#include <errno.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#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