From 81757c235ff8e112b4baabdd1ff23409426e9c98 Mon Sep 17 00:00:00 2001 From: Philip Wittamore Date: Sun, 8 Jun 2025 22:00:43 +0200 Subject: update --- src/block.c | 147 ++++++++++++++++++++++++++++++++++++++++++++ src/cli.c | 33 ++++++++++ src/main.c | 168 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/signal-handler.c | 124 +++++++++++++++++++++++++++++++++++++ src/status.c | 78 ++++++++++++++++++++++++ src/timer.c | 72 ++++++++++++++++++++++ src/util.c | 49 +++++++++++++++ src/watcher.c | 69 +++++++++++++++++++++ src/x11.c | 44 ++++++++++++++ 9 files changed, 784 insertions(+) create mode 100644 src/block.c create mode 100644 src/cli.c create mode 100644 src/main.c create mode 100644 src/signal-handler.c create mode 100644 src/status.c create mode 100644 src/timer.c create mode 100644 src/util.c create mode 100644 src/watcher.c create mode 100644 src/x11.c (limited to 'src') 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#include + +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 +#include +#include + +#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 +#include +#include +#include +#include +#include + +#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 +#include +#include + +#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 +#include +#include +#include + +#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 +#include +#include +#include + +#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 +#include +#include +#include + +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; +} -- cgit v1.2.3