aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPhilip Wittamore <philip@wittamore.com>2025-06-08 22:00:43 +0200
committerPhilip Wittamore <philip@wittamore.com>2025-06-08 22:00:43 +0200
commit81757c235ff8e112b4baabdd1ff23409426e9c98 (patch)
treeef213566ac3c17bf3d7795b0597f254791bd219e /src
downloaddwmblocks-async-81757c235ff8e112b4baabdd1ff23409426e9c98.tar.gz
dwmblocks-async-81757c235ff8e112b4baabdd1ff23409426e9c98.tar.bz2
dwmblocks-async-81757c235ff8e112b4baabdd1ff23409426e9c98.zip
update
Diffstat (limited to 'src')
-rw-r--r--src/block.c147
-rw-r--r--src/cli.c33
-rw-r--r--src/main.c168
-rw-r--r--src/signal-handler.c124
-rw-r--r--src/status.c78
-rw-r--r--src/timer.c72
-rw-r--r--src/util.c49
-rw-r--r--src/watcher.c69
-rw-r--r--src/x11.c44
9 files changed, 784 insertions, 0 deletions
diff --git a/src/block.c b/src/block.c
new file mode 100644
index 0000000..a6c919d
--- /dev/null
+++ b/src/block.c
@@ -0,0 +1,147 @@
+#include "block.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "config.h"
+#include "util.h"
+
+block block_new(const char *const icon, const char *const command,
+ const unsigned int interval, const int signal) {
+ block block = {
+ .icon = icon,
+ .command = command,
+ .interval = interval,
+ .signal = signal,
+
+ .output = {[0] = '\0'},
+ .fork_pid = -1,
+ };
+
+ return block;
+}
+
+int block_init(block *const block) {
+ if (pipe(block->pipe) != 0) {
+ (void)fprintf(stderr,
+ "error: could not create a pipe for \"%s\" block\n",
+ block->command);
+ return 1;
+ }
+
+ return 0;
+}
+
+int block_deinit(block *const block) {
+ int status = close(block->pipe[READ_END]);
+ status |= close(block->pipe[WRITE_END]);
+ if (status != 0) {
+ (void)fprintf(stderr, "error: could not close \"%s\" block's pipe\n",
+ block->command);
+ return 1;
+ }
+
+ return 0;
+}
+
+int block_execute(block *const block, const uint8_t button) {
+ // Ensure only one child process exists per block at an instance.
+ if (block->fork_pid != -1) {
+ return 0;
+ }
+
+ block->fork_pid = fork();
+ if (block->fork_pid == -1) {
+ (void)fprintf(
+ stderr, "error: could not create a subprocess for \"%s\" block\n",
+ block->command);
+ return 1;
+ }
+
+ if (block->fork_pid == 0) {
+ const int write_fd = block->pipe[WRITE_END];
+ int status = close(block->pipe[READ_END]);
+
+ if (button != 0) {
+ char button_str[4];
+ (void)snprintf(button_str, LEN(button_str), "%hhu", button);
+ status |= setenv("BLOCK_BUTTON", button_str, 1);
+ }
+
+ const char null = '\0';
+ if (status != 0) {
+ (void)write(write_fd, &null, sizeof(null));
+ exit(EXIT_FAILURE);
+ }
+
+ FILE *const file = popen(block->command, "r");
+ if (file == NULL) {
+ (void)write(write_fd, &null, sizeof(null));
+ exit(EXIT_FAILURE);
+ }
+
+ // Ensure null-termination since fgets() will leave buffer untouched on
+ // no output.
+ char buffer[LEN(block->output)] = {[0] = null};
+ (void)fgets(buffer, LEN(buffer), file);
+
+ // Remove trailing newlines.
+ const size_t length = strcspn(buffer, "\n");
+ buffer[length] = null;
+
+ // Exit if command execution failed or if file could not be closed.
+ if (pclose(file) != 0) {
+ (void)write(write_fd, &null, sizeof(null));
+ exit(EXIT_FAILURE);
+ }
+
+ const size_t output_size =
+ truncate_utf8_string(buffer, LEN(buffer), MAX_BLOCK_OUTPUT_LENGTH);
+ (void)write(write_fd, buffer, output_size);
+
+ exit(EXIT_SUCCESS);
+ }
+
+ return 0;
+}
+
+int block_update(block *const block) {
+ char buffer[LEN(block->output)];
+
+ const ssize_t bytes_read =
+ read(block->pipe[READ_END], buffer, LEN(buffer));
+ if (bytes_read == -1) {
+ (void)fprintf(stderr,
+ "error: could not fetch output of \"%s\" block\n",
+ block->command);
+ return 2;
+ }
+
+ // Collect exit-status of the subprocess to avoid zombification.
+ int fork_status = 0;
+ if (waitpid(block->fork_pid, &fork_status, 0) == -1) {
+ (void)fprintf(stderr,
+ "error: could not obtain exit status for \"%s\" block\n",
+ block->command);
+ return 2;
+ }
+ block->fork_pid = -1;
+
+ if (fork_status != 0) {
+ (void)fprintf(stderr,
+ "error: \"%s\" block exited with non-zero status\n",
+ block->command);
+ return 1;
+ }
+
+ (void)strncpy(block->output, buffer, LEN(buffer));
+
+ return 0;
+}
diff --git a/src/cli.c b/src/cli.c
new file mode 100644
index 0000000..b1849ec
--- /dev/null
+++ b/src/cli.c
@@ -0,0 +1,33 @@
+#include "cli.h"
+
+#include <errno.h>
+#include <getopt.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+cli_arguments cli_parse_arguments(const char *const argv[], const int argc) {
+ errno = 0;
+ cli_arguments args = {
+ .is_debug_mode = false,
+ };
+
+ int opt = -1;
+ opterr = 0; // Suppress getopt's built-in invalid opt message
+ while ((opt = getopt(argc, (char *const *)argv, "dh")) != -1) {
+ switch (opt) {
+ case 'd':
+ args.is_debug_mode = true;
+ break;
+ case '?':
+ (void)fprintf(stderr, "error: unknown option `-%c'\n", optopt);
+ // fall through
+ case 'h':
+ // fall through
+ default:
+ (void)fprintf(stderr, "usage: %s [-d]\n", BINARY);
+ errno = 1;
+ }
+ }
+
+ return args;
+}
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..747b075
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,168 @@
+#include "main.h"
+
+#include <errno.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+#include "block.h"
+#include "cli.h"
+#include "config.h"
+#include "signal-handler.h"
+#include "status.h"
+#include "timer.h"
+#include "util.h"
+#include "watcher.h"
+#include "x11.h"
+
+static int init_blocks(block *const blocks, const unsigned short block_count) {
+ for (unsigned short i = 0; i < block_count; ++i) {
+ block *const block = &blocks[i];
+ if (block_init(block) != 0) {
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+static int deinit_blocks(block *const blocks,
+ const unsigned short block_count) {
+ for (unsigned short i = 0; i < block_count; ++i) {
+ block *const block = &blocks[i];
+ if (block_deinit(block) != 0) {
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+static int execute_blocks(block *const blocks,
+ const unsigned short block_count,
+ const timer *const timer) {
+ for (unsigned short i = 0; i < block_count; ++i) {
+ block *const block = &blocks[i];
+ if (!timer_must_run_block(timer, block)) {
+ continue;
+ }
+
+ if (block_execute(&blocks[i], 0) != 0) {
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+static int trigger_event(block *const blocks, const unsigned short block_count,
+ timer *const timer) {
+ if (execute_blocks(blocks, block_count, timer) != 0) {
+ return 1;
+ }
+
+ if (timer_arm(timer) != 0) {
+ return 1;
+ }
+
+ return 0;
+}
+
+static int refresh_callback(block *const blocks,
+ const unsigned short block_count) {
+ if (execute_blocks(blocks, block_count, NULL) != 0) {
+ return 1;
+ }
+
+ return 0;
+}
+
+static int event_loop(block *const blocks, const unsigned short block_count,
+ const bool is_debug_mode,
+ x11_connection *const connection,
+ signal_handler *const signal_handler) {
+ timer timer = timer_new(blocks, block_count);
+
+ // Kickstart the event loop with an initial execution.
+ if (trigger_event(blocks, block_count, &timer) != 0) {
+ return 1;
+ }
+
+ watcher watcher;
+ if (watcher_init(&watcher, blocks, block_count, signal_handler->fd) != 0) {
+ return 1;
+ }
+
+ status status = status_new(blocks, block_count);
+ bool is_alive = true;
+ while (is_alive) {
+ if (watcher_poll(&watcher, -1) != 0) {
+ return 1;
+ }
+
+ if (watcher.got_signal) {
+ is_alive = signal_handler_process(signal_handler, &timer) == 0;
+ }
+
+ for (unsigned short i = 0; i < watcher.active_block_count; ++i) {
+ (void)block_update(&blocks[watcher.active_blocks[i]]);
+ }
+
+ const bool has_status_changed = status_update(&status);
+ if (has_status_changed &&
+ status_write(&status, is_debug_mode, connection) != 0) {
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+int main(const int argc, const char *const argv[]) {
+ const cli_arguments cli_args = cli_parse_arguments(argv, argc);
+ if (errno != 0) {
+ return 1;
+ }
+
+ x11_connection *const connection = x11_connection_open();
+ if (connection == NULL) {
+ return 1;
+ }
+
+#define BLOCK(icon, command, interval, signal) \
+ block_new(icon, command, interval, signal),
+ block blocks[BLOCK_COUNT] = {BLOCKS(BLOCK)};
+#undef BLOCK
+ const unsigned short block_count = LEN(blocks);
+
+ int status = 0;
+ if (init_blocks(blocks, block_count) != 0) {
+ status = 1;
+ goto x11_close;
+ }
+
+ signal_handler signal_handler = signal_handler_new(
+ blocks, block_count, refresh_callback, trigger_event);
+ if (signal_handler_init(&signal_handler) != 0) {
+ status = 1;
+ goto deinit_blocks;
+ }
+
+ if (event_loop(blocks, block_count, cli_args.is_debug_mode, connection,
+ &signal_handler) != 0) {
+ status = 1;
+ }
+
+ if (signal_handler_deinit(&signal_handler) != 0) {
+ status = 1;
+ }
+
+deinit_blocks:
+ if (deinit_blocks(blocks, block_count) != 0) {
+ status = 1;
+ }
+
+x11_close:
+ x11_connection_close(connection);
+
+ return status;
+}
diff --git a/src/signal-handler.c b/src/signal-handler.c
new file mode 100644
index 0000000..d816dcd
--- /dev/null
+++ b/src/signal-handler.c
@@ -0,0 +1,124 @@
+#include "signal-handler.h"
+
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <sys/signalfd.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "block.h"
+#include "main.h"
+#include "timer.h"
+
+typedef struct signalfd_siginfo signal_info;
+
+signal_handler signal_handler_new(
+ block *const blocks, const unsigned short block_count,
+ const signal_refresh_callback refresh_callback,
+ const signal_timer_callback timer_callback) {
+ signal_handler handler = {
+ .refresh_callback = refresh_callback,
+ .timer_callback = timer_callback,
+
+ .blocks = blocks,
+ .block_count = block_count,
+ };
+
+ return handler;
+}
+
+int signal_handler_init(signal_handler *const handler) {
+ signal_set set;
+ (void)sigemptyset(&set);
+
+ // Handle user-generated signal for refreshing the status.
+ (void)sigaddset(&set, REFRESH_SIGNAL);
+
+ // Handle SIGALRM generated by the timer.
+ (void)sigaddset(&set, TIMER_SIGNAL);
+
+ // Handle termination signals.
+ (void)sigaddset(&set, SIGINT);
+ (void)sigaddset(&set, SIGTERM);
+
+ for (unsigned short i = 0; i < handler->block_count; ++i) {
+ const block *const block = &handler->blocks[i];
+ if (block->signal > 0) {
+ if (sigaddset(&set, SIGRTMIN + block->signal) != 0) {
+ (void)fprintf(
+ stderr,
+ "error: invalid or unsupported signal specified for "
+ "\"%s\" block\n",
+ block->command);
+ return 1;
+ }
+ }
+ }
+
+ // Create a signal file descriptor for epoll to watch.
+ handler->fd = signalfd(-1, &set, 0);
+ if (handler->fd == -1) {
+ (void)fprintf(stderr,
+ "error: could not create file descriptor for signals\n");
+ return 1;
+ }
+
+ // Block all realtime and handled signals.
+ for (int i = SIGRTMIN; i <= SIGRTMAX; ++i) {
+ (void)sigaddset(&set, i);
+ }
+ (void)sigprocmask(SIG_BLOCK, &set, NULL);
+
+ return 0;
+}
+
+int signal_handler_deinit(signal_handler *const handler) {
+ if (close(handler->fd) != 0) {
+ (void)fprintf(stderr,
+ "error: could not close signal file descriptor\n");
+ return 1;
+ }
+
+ return 0;
+}
+
+int signal_handler_process(signal_handler *const handler, timer *const timer) {
+ signal_info info;
+ const ssize_t bytes_read = read(handler->fd, &info, sizeof(info));
+ if (bytes_read == -1) {
+ (void)fprintf(stderr, "error: could not read info of incoming signal");
+ return 1;
+ }
+
+ const int signal = (int)info.ssi_signo;
+ switch (signal) {
+ case TIMER_SIGNAL:
+ if (handler->timer_callback(handler->blocks, handler->block_count,
+ timer) != 0) {
+ return 1;
+ }
+ return 0;
+ case REFRESH_SIGNAL:
+ if (handler->refresh_callback(handler->blocks,
+ handler->block_count) != 0) {
+ return 1;
+ }
+ return 0;
+ case SIGTERM:
+ // fall through
+ case SIGINT:
+ return 1;
+ }
+
+ for (unsigned short i = 0; i < handler->block_count; ++i) {
+ block *const block = &handler->blocks[i];
+ if (block->signal == signal - SIGRTMIN) {
+ const uint8_t button = (uint8_t)info.ssi_int;
+ block_execute(block, button);
+ break;
+ }
+ }
+
+ return 0;
+}
diff --git a/src/status.c b/src/status.c
new file mode 100644
index 0000000..cf0911a
--- /dev/null
+++ b/src/status.c
@@ -0,0 +1,78 @@
+#include "status.h"
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "block.h"
+#include "config.h"
+#include "util.h"
+#include "x11.h"
+
+static bool has_status_changed(const status *const status) {
+ return strcmp(status->current, status->previous) != 0;
+}
+
+status status_new(const block *const blocks,
+ const unsigned short block_count) {
+ status status = {
+ .current = {[0] = '\0'},
+ .previous = {[0] = '\0'},
+
+ .blocks = blocks,
+ .block_count = block_count,
+ };
+
+ return status;
+}
+
+bool status_update(status *const status) {
+ (void)strncpy(status->previous, status->current, LEN(status->current));
+ status->current[0] = '\0';
+
+ for (unsigned short i = 0; i < status->block_count; ++i) {
+ const block *const block = &status->blocks[i];
+
+ if (strlen(block->output) > 0) {
+#if LEADING_DELIMITER
+ (void)strncat(status->current, DELIMITER, LEN(DELIMITER));
+#else
+ if (status->current[0] != '\0') {
+ (void)strncat(status->current, DELIMITER, LEN(DELIMITER));
+ }
+#endif
+
+#if CLICKABLE_BLOCKS
+ if (block->signal > 0) {
+ const char signal[] = {(char)block->signal, '\0'};
+ (void)strncat(status->current, signal, LEN(signal));
+ }
+#endif
+
+ (void)strncat(status->current, block->icon, LEN(block->output));
+ (void)strncat(status->current, block->output, LEN(block->output));
+ }
+ }
+
+#if TRAILING_DELIMITER
+ if (status->current[0] != '\0') {
+ (void)strncat(status->current, DELIMITER, LEN(DELIMITER));
+ }
+#endif
+
+ return has_status_changed(status);
+}
+
+int status_write(const status *const status, const bool is_debug_mode,
+ x11_connection *const connection) {
+ if (is_debug_mode) {
+ (void)printf("%s\n", status->current);
+ return 0;
+ }
+
+ if (x11_set_root_name(connection, status->current) != 0) {
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/src/timer.c b/src/timer.c
new file mode 100644
index 0000000..2ee555b
--- /dev/null
+++ b/src/timer.c
@@ -0,0 +1,72 @@
+#include "timer.h"
+
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "block.h"
+#include "util.h"
+
+static unsigned int compute_tick(const block *const blocks,
+ const unsigned short block_count) {
+ unsigned int tick = 0;
+
+ for (unsigned short i = 0; i < block_count; ++i) {
+ const block *const block = &blocks[i];
+ tick = gcd(block->interval, tick);
+ }
+
+ return tick;
+}
+
+static unsigned int compute_reset_value(const block *const blocks,
+ const unsigned short block_count) {
+ unsigned int reset_value = 1;
+
+ for (unsigned short i = 0; i < block_count; ++i) {
+ const block *const block = &blocks[i];
+ reset_value = MAX(block->interval, reset_value);
+ }
+
+ return reset_value;
+}
+
+timer timer_new(const block *const blocks, const unsigned short block_count) {
+ const unsigned int reset_value = compute_reset_value(blocks, block_count);
+
+ timer timer = {
+ .time = reset_value, // Initial value to execute all blocks.
+ .tick = compute_tick(blocks, block_count),
+ .reset_value = reset_value,
+ };
+
+ return timer;
+}
+
+int timer_arm(timer *const timer) {
+ errno = 0;
+ (void)alarm(timer->tick);
+
+ if (errno != 0) {
+ (void)fprintf(stderr, "error: could not arm timer\n");
+ return 1;
+ }
+
+ // Wrap `time` to the interval [1, reset_value].
+ timer->time = (timer->time + timer->tick) % timer->reset_value;
+
+ return 0;
+}
+
+bool timer_must_run_block(const timer *const timer, const block *const block) {
+ if (timer == NULL || timer->time == timer->reset_value) {
+ return true;
+ }
+
+ if (block->interval == 0) {
+ return false;
+ }
+
+ return timer->time % block->interval == 0;
+}
diff --git a/src/util.c b/src/util.c
new file mode 100644
index 0000000..10485db
--- /dev/null
+++ b/src/util.c
@@ -0,0 +1,49 @@
+#include "util.h"
+
+#define UTF8_MULTIBYTE_BIT BIT(7)
+
+unsigned int gcd(unsigned int a, unsigned int b) {
+ while (b > 0) {
+ const unsigned int temp = a % b;
+ a = b;
+ b = temp;
+ }
+
+ return a;
+}
+
+size_t truncate_utf8_string(char* const buffer, const size_t size,
+ const size_t char_limit) {
+ size_t char_count = 0;
+ size_t i = 0;
+ while (char_count < char_limit) {
+ char ch = buffer[i];
+ if (ch == '\0') {
+ break;
+ }
+
+ unsigned short skip = 1;
+
+ // Multibyte unicode character.
+ if ((ch & UTF8_MULTIBYTE_BIT) != 0) {
+ // Skip continuation bytes.
+ ch <<= 1;
+ while ((ch & UTF8_MULTIBYTE_BIT) != 0) {
+ ch <<= 1;
+ ++skip;
+ }
+ }
+
+ // Avoid buffer overflow.
+ if (i + skip >= size) {
+ break;
+ }
+
+ ++char_count;
+ i += skip;
+ }
+
+ buffer[i] = '\0';
+
+ return i + 1;
+}
diff --git a/src/watcher.c b/src/watcher.c
new file mode 100644
index 0000000..71b6c52
--- /dev/null
+++ b/src/watcher.c
@@ -0,0 +1,69 @@
+#include "watcher.h"
+
+#include <errno.h>
+#include <poll.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+#include "block.h"
+#include "util.h"
+
+static bool watcher_fd_is_readable(const watcher_fd* const watcher_fd) {
+ return (watcher_fd->revents & POLLIN) != 0;
+}
+
+int watcher_init(watcher* const watcher, const block* const blocks,
+ const unsigned short block_count, const int signal_fd) {
+ if (signal_fd == -1) {
+ (void)fprintf(
+ stderr,
+ "error: invalid signal file descriptor passed to watcher\n");
+ return 1;
+ }
+
+ watcher_fd* const fd = &watcher->fds[SIGNAL_FD];
+ fd->fd = signal_fd;
+ fd->events = POLLIN;
+
+ for (unsigned short i = 0; i < block_count; ++i) {
+ const int block_fd = blocks[i].pipe[READ_END];
+ if (block_fd == -1) {
+ (void)fprintf(
+ stderr,
+ "error: invalid block file descriptors passed to watcher\n");
+ return 1;
+ }
+
+ watcher_fd* const fd = &watcher->fds[i];
+ fd->fd = block_fd;
+ fd->events = POLLIN;
+ }
+
+ return 0;
+}
+
+int watcher_poll(watcher* watcher, const int timeout_ms) {
+ int event_count = poll(watcher->fds, LEN(watcher->fds), timeout_ms);
+
+ // Don't return non-zero status for signal interruptions.
+ if (event_count == -1 && errno != EINTR) {
+ (void)fprintf(stderr, "error: watcher could not poll blocks\n");
+ return 1;
+ }
+
+ watcher->got_signal = watcher_fd_is_readable(&watcher->fds[SIGNAL_FD]);
+
+ watcher->active_block_count = event_count - (int)watcher->got_signal;
+ unsigned short i = 0;
+ unsigned short j = 0;
+ while (i < event_count && j < LEN(watcher->active_blocks)) {
+ if (watcher_fd_is_readable(&watcher->fds[j])) {
+ watcher->active_blocks[i] = j;
+ ++i;
+ }
+
+ ++j;
+ }
+
+ return 0;
+}
diff --git a/src/x11.c b/src/x11.c
new file mode 100644
index 0000000..7a310e9
--- /dev/null
+++ b/src/x11.c
@@ -0,0 +1,44 @@
+#include "x11.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <xcb/xcb.h>
+#include <xcb/xproto.h>
+
+x11_connection *x11_connection_open(void) {
+ xcb_connection_t *const connection = xcb_connect(NULL, NULL);
+ if (xcb_connection_has_error(connection)) {
+ (void)fprintf(stderr, "error: could not connect to X server\n");
+ return NULL;
+ }
+
+ return connection;
+}
+
+void x11_connection_close(xcb_connection_t *const connection) {
+ xcb_disconnect(connection);
+}
+
+int x11_set_root_name(x11_connection *const connection, const char *name) {
+ xcb_screen_t *const screen =
+ xcb_setup_roots_iterator(xcb_get_setup(connection)).data;
+ const xcb_window_t root_window = screen->root;
+
+ const unsigned short name_format = 8;
+ const xcb_void_cookie_t cookie = xcb_change_property(
+ connection, XCB_PROP_MODE_REPLACE, root_window, XCB_ATOM_WM_NAME,
+ XCB_ATOM_STRING, name_format, strlen(name), name);
+
+ xcb_generic_error_t *error = xcb_request_check(connection, cookie);
+ if (error != NULL) {
+ (void)fprintf(stderr, "error: could not set X root name\n");
+ return 1;
+ }
+
+ if (xcb_flush(connection) <= 0) {
+ (void)fprintf(stderr, "error: could not flush X output buffer\n");
+ return 1;
+ }
+
+ return 0;
+}