aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LICENSE339
-rw-r--r--Makefile57
-rw-r--r--README.md164
-rw-r--r--config.h27
-rw-r--r--include/block.h29
-rw-r--r--include/cli.h12
-rw-r--r--include/main.h16
-rw-r--r--include/signal-handler.h33
-rw-r--r--include/status.h31
-rw-r--r--include/timer.h21
-rw-r--r--include/util.h28
-rw-r--r--include/watcher.h28
-rw-r--r--include/x11.h13
-rw-r--r--preview.pngbin0 -> 54091 bytes
-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
23 files changed, 1582 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..635673e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,57 @@
+.POSIX:
+
+BIN := dwmblocks
+BUILD_DIR := build
+SRC_DIR := src
+INC_DIR := include
+
+DEBUG := 0
+VERBOSE := 0
+LIBS := xcb-atom
+
+PREFIX := /usr/local
+CFLAGS := -Ofast -I. -I$(INC_DIR) -std=c99
+CFLAGS += -DBINARY=\"$(BIN)\" -D_POSIX_C_SOURCE=200809L
+CFLAGS += -Wall -Wpedantic -Wextra -Wswitch-enum
+CFLAGS += $(shell pkg-config --cflags $(LIBS))
+LDLIBS := $(shell pkg-config --libs $(LIBS))
+
+SRCS := $(wildcard $(SRC_DIR)/*.c)
+OBJS := $(subst $(SRC_DIR)/,$(BUILD_DIR)/,$(SRCS:.c=.o))
+
+INSTALL_DIR := $(DESTDIR)$(PREFIX)/bin
+
+# Prettify output
+PRINTF := @printf "%-8s %s\n"
+ifeq ($(VERBOSE), 0)
+ Q := @
+endif
+
+ifeq ($(DEBUG), 1)
+ CFLAGS += -g
+endif
+
+all: $(BUILD_DIR)/$(BIN)
+
+$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c config.h
+ $Qmkdir -p $(@D)
+ $(PRINTF) "CC" $@
+ $Q$(COMPILE.c) -o $@ $<
+
+$(BUILD_DIR)/$(BIN): $(OBJS)
+ $(PRINTF) "LD" $@
+ $Q$(LINK.o) $^ $(LDLIBS) -o $@
+
+clean:
+ $(PRINTF) "CLEAN" $(BUILD_DIR)
+ $Q$(RM) $(BUILD_DIR)/*
+
+install: $(BUILD_DIR)/$(BIN)
+ $(PRINTF) "INSTALL" $(INSTALL_DIR)/$(BIN)
+ $Qinstall -D -m 755 $< $(INSTALL_DIR)/$(BIN)
+
+uninstall:
+ $(PRINTF) "RM" $(INSTALL_DIR)/$(BIN)
+ $Q$(RM) $(INSTALL_DIR)/$(BIN)
+
+.PHONY: all clean install uninstall
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1b389f6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,164 @@
+# dwmblocks-async
+
+A [`dwm`](https://dwm.suckless.org) status bar that has a modular, async
+design, so it is always responsive. Imagine `i3blocks`, but for `dwm`.
+
+![A lean config of dwmblocks-async.](preview.png)
+
+## Features
+
+- [Modular](#modifying-the-blocks)
+- Lightweight
+- [Suckless](https://suckless.org/philosophy)
+- Blocks:
+ - [Clickable](#clickable-blocks)
+ - Loaded asynchronously
+ - [Updates can be externally triggered](#signalling-changes)
+- Compatible with `i3blocks` scripts
+
+> Additionally, this build of `dwmblocks` is more optimized and fixes the
+> flickering of the status bar when scrolling.
+
+## Why `dwmblocks`?
+
+In `dwm`, you have to set the status bar through an infinite loop, like so:
+
+```sh
+while :; do
+ xsetroot -name "$(date)"
+ sleep 30
+done
+```
+
+This is inefficient when running multiple commands that need to be updated at
+different frequencies. For example, to display an unread mail count and a clock
+in the status bar:
+
+```sh
+while :; do
+ xsetroot -name "$(mailCount) $(date)"
+ sleep 60
+done
+```
+
+Both are executed at the same rate, which is wasteful. Ideally, the mail
+counter would be updated every thirty minutes, since there's a limit to the
+number of requests I can make using Gmail's APIs (as a free user).
+
+`dwmblocks` allows you to divide the status bar into multiple blocks, each of
+which can be updated at its own interval. This effectively addresses the
+previous issue, because the commands in a block are only executed once within
+that time frame.
+
+## Why `dwmblocks-async`?
+
+The magic of `dwmblocks-async` is in the `async` part. Since vanilla
+`dwmblocks` executes the commands of each block sequentially, it leads to
+annoying freezes. In cases where one block takes several seconds to execute,
+like in the mail and date blocks example from above, the delay is clearly
+visible. Fire up a new instance of `dwmblocks` and you'll see!
+
+With `dwmblocks-async`, the computer executes each block asynchronously
+(simultaneously).
+
+## Installation
+
+Clone this repository, modify `config.h` appropriately, then compile the
+program:
+
+```sh
+git clone https://github.com/UtkarshVerma/dwmblocks-async.git
+cd dwmblocks-async
+vi config.h
+sudo make install
+```
+
+## Usage
+
+To set `dwmblocks-async` as your status bar, you need to run it as a background
+process on startup. One way is to add the following to `~/.xinitrc`:
+
+```sh
+# The binary of `dwmblocks-async` is named `dwmblocks`
+dwmblocks &
+```
+
+### Modifying the blocks
+
+You can define your status bar blocks in `config.h`:
+
+```c
+#define BLOCKS(X) \
+ ...
+ X(" ", "wpctl get-volume @DEFAULT_AUDIO_SINK@ | cut -d' ' -f2", 0, 5) \
+ X("󰥔 ", "date '+%H:%M:%S'", 1, 1) \
+ ...
+```
+
+Each block has the following properties:
+
+| Property | Description |
+| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Icon | An icon you wish to prepend to your block output. |
+| Command | The command you wish to execute in your block. |
+| Update interval | Time in seconds, after which you want the block to update. If `0`, the block will never be updated. |
+| Update signal | Signal to be used for triggering the block. Must be a positive integer. If `0`, a signal won't be set up for the block and it will be unclickable. |
+
+Apart from defining the blocks, features can be toggled through `config.h`:
+
+```c
+// String used to delimit block outputs in the status.
+#define DELIMITER " "
+
+// Maximum number of Unicode characters that a block can output.
+#define MAX_BLOCK_OUTPUT_LENGTH 45
+
+// Control whether blocks are clickable.
+#define CLICKABLE_BLOCKS 1
+
+// Control whether a leading delimiter should be prepended to the status.
+#define LEADING_DELIMITER 0
+
+// Control whether a trailing delimiter should be appended to the status.
+#define TRAILING_DELIMITER 0
+```
+
+### Signalling changes
+
+Most status bars constantly rerun all scripts every few seconds. This is an
+option here, but a superior choice is to give your block a signal through which
+you can indicate it to update on relevant event, rather than have it rerun
+idly.
+
+For example, the volume block has the update signal `5` by default. I run
+`kill -39 $(pidof dwmblocks)` alongside my volume shortcuts in `dwm` to only
+update it when relevant. Just add `34` to your signal number! You could also
+run `pkill -RTMIN+5 dwmblocks`, but it's slower.
+
+To refresh all the blocks, run `kill -10 $(pidof dwmblocks)` or
+`pkill -SIGUSR1 dwmblocks`.
+
+> All blocks must have different signal numbers!
+
+### Clickable blocks
+
+Like `i3blocks`, this build allows you to build in additional actions into your
+scripts in response to click events. You can check out
+[my status bar scripts](https://github.com/UtkarshVerma/dotfiles/tree/main/.local/bin/statusbar)
+as references for using the `$BLOCK_BUTTON` variable.
+
+To use this feature, define the `CLICKABLE_BLOCKS` feature macro in your
+`config.h`:
+
+```c
+#define CLICKABLE_BLOCKS 1
+```
+
+Apart from that, you need `dwm` to be patched with
+[statuscmd](https://dwm.suckless.org/patches/statuscmd/).
+
+## Credits
+
+This work would not have been possible without
+[Luke's build of dwmblocks](https://github.com/LukeSmithxyz/dwmblocks) and
+[Daniel Bylinka's statuscmd patch](https://dwm.suckless.org/patches/statuscmd/).
diff --git a/config.h b/config.h
new file mode 100644
index 0000000..f370c04
--- /dev/null
+++ b/config.h
@@ -0,0 +1,27 @@
+#ifndef CONFIG_H
+#define CONFIG_H
+
+// String used to delimit block outputs in the status.
+#define DELIMITER ""
+
+// Maximum number of Unicode characters that a block can output.
+#define MAX_BLOCK_OUTPUT_LENGTH 50
+
+// Control whether blocks are clickable.
+#define CLICKABLE_BLOCKS 1
+
+// Control whether a leading delimiter should be prepended to the status.
+#define LEADING_DELIMITER 0
+
+// Control whether a trailing delimiter should be appended to the status.
+#define TRAILING_DELIMITER 0
+
+// Define blocks for the status feed as X(icon, cmd, interval, signal).
+#define BLOCKS(X) \
+ X("", "pw-music", 2, 5) \
+ X("", "pw-fan", 5, 4) \
+ X("", "pw-cputemp", 5, 3) \
+ X("", "pw-battery", 5, 2) \
+ X("", "pw-date", 1, 1)
+
+#endif // CONFIG_H
diff --git a/include/block.h b/include/block.h
new file mode 100644
index 0000000..c4f8d54
--- /dev/null
+++ b/include/block.h
@@ -0,0 +1,29 @@
+#ifndef BLOCK_H
+#define BLOCK_H
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <sys/types.h>
+
+#include "config.h"
+#include "util.h"
+
+typedef struct {
+ const char *const icon;
+ const char *const command;
+ const unsigned int interval;
+ const int signal;
+
+ int pipe[PIPE_FD_COUNT];
+ char output[MAX_BLOCK_OUTPUT_LENGTH * UTF8_MAX_BYTE_COUNT + 1];
+ pid_t fork_pid;
+} block;
+
+block block_new(const char *const icon, const char *const command,
+ const unsigned int interval, const int signal);
+int block_init(block *const block);
+int block_deinit(block *const block);
+int block_execute(block *const block, const uint8_t button);
+int block_update(block *const block);
+
+#endif // BLOCK_H
diff --git a/include/cli.h b/include/cli.h
new file mode 100644
index 0000000..2f93f62
--- /dev/null
+++ b/include/cli.h
@@ -0,0 +1,12 @@
+#ifndef CLI_H
+#define CLI_H
+
+#include <stdbool.h>
+
+typedef struct {
+ bool is_debug_mode;
+} cli_arguments;
+
+cli_arguments cli_parse_arguments(const char* const argv[], const int argc);
+
+#endif // CLI_H
diff --git a/include/main.h b/include/main.h
new file mode 100644
index 0000000..b37a6b1
--- /dev/null
+++ b/include/main.h
@@ -0,0 +1,16 @@
+#ifndef MAIN_H
+#define MAIN_H
+
+#include <signal.h>
+
+#include "config.h"
+#include "util.h"
+
+#define REFRESH_SIGNAL SIGUSR1
+
+// Utilise C's adjacent string concatenation to count the number of blocks.
+#define X(...) "."
+enum { BLOCK_COUNT = LEN(BLOCKS(X)) - 1 };
+#undef X
+
+#endif // MAIN_H
diff --git a/include/signal-handler.h b/include/signal-handler.h
new file mode 100644
index 0000000..da2d471
--- /dev/null
+++ b/include/signal-handler.h
@@ -0,0 +1,33 @@
+#ifndef SIGNAL_HANDLER_H
+#define SIGNAL_HANDLER_H
+
+#include <signal.h>
+
+#include "block.h"
+#include "timer.h"
+
+typedef sigset_t signal_set;
+typedef int (*signal_refresh_callback)(block* const blocks,
+ const unsigned short block_count);
+typedef int (*signal_timer_callback)(block* const blocks,
+ const unsigned short block_code,
+ timer* const timer);
+
+typedef struct {
+ int fd;
+ const signal_refresh_callback refresh_callback;
+ const signal_timer_callback timer_callback;
+
+ block* const blocks;
+ const unsigned short block_count;
+} signal_handler;
+
+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);
+int signal_handler_init(signal_handler* const handler);
+int signal_handler_deinit(signal_handler* const handler);
+int signal_handler_process(signal_handler* const handler, timer* const timer);
+
+#endif // SIGNAL_HANDLER_H
diff --git a/include/status.h b/include/status.h
new file mode 100644
index 0000000..48fb3d8
--- /dev/null
+++ b/include/status.h
@@ -0,0 +1,31 @@
+#ifndef STATUS_H
+#define STATUS_H
+
+#include <stdbool.h>
+
+#include "block.h"
+#include "config.h"
+#include "main.h"
+#include "util.h"
+#include "x11.h"
+
+typedef struct {
+#define STATUS_LENGTH \
+ ((BLOCK_COUNT * (MEMBER_LENGTH(block, output) - 1) + CLICKABLE_BLOCKS) + \
+ (BLOCK_COUNT - 1 + LEADING_DELIMITER + TRAILING_DELIMITER) * \
+ (LEN(DELIMITER) - 1) + \
+ 1)
+ char current[STATUS_LENGTH];
+ char previous[STATUS_LENGTH];
+#undef STATUS_LENGTH
+
+ const block* const blocks;
+ const unsigned short block_count;
+} status;
+
+status status_new(const block* const blocks, const unsigned short block_count);
+bool status_update(status* const status);
+int status_write(const status* const status, const bool is_debug_mode,
+ x11_connection* const connection);
+
+#endif // STATUS_H
diff --git a/include/timer.h b/include/timer.h
new file mode 100644
index 0000000..1ec7f75
--- /dev/null
+++ b/include/timer.h
@@ -0,0 +1,21 @@
+#ifndef TIMER_H
+#define TIMER_H
+
+#include <signal.h>
+#include <stdbool.h>
+
+#include "block.h"
+
+#define TIMER_SIGNAL SIGALRM
+
+typedef struct {
+ unsigned int time;
+ const unsigned int tick;
+ const unsigned int reset_value;
+} timer;
+
+timer timer_new(const block *const blocks, const unsigned short block_count);
+int timer_arm(timer *const timer);
+bool timer_must_run_block(const timer *const timer, const block *const block);
+
+#endif // TIMER_H
diff --git a/include/util.h b/include/util.h
new file mode 100644
index 0000000..a3bdcce
--- /dev/null
+++ b/include/util.h
@@ -0,0 +1,28 @@
+#ifndef UTIL_H
+#define UTIL_H
+
+#include <stddef.h>
+
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
+#define BIT(n) (1 << (n))
+
+// NOLINTBEGIN(bugprone-macro-parentheses)
+#define MEMBER_SIZE(type, member) sizeof(((type*)NULL)->member)
+#define MEMBER_LENGTH(type, member) \
+ (MEMBER_SIZE(type, member) / MEMBER_SIZE(type, member[0]))
+// NOLINTEND(bugprone-macro-parentheses)
+
+#define UTF8_MAX_BYTE_COUNT 4
+
+enum pipe_fd_index {
+ READ_END,
+ WRITE_END,
+ PIPE_FD_COUNT,
+};
+
+unsigned int gcd(unsigned int a, unsigned int b);
+size_t truncate_utf8_string(char* const buffer, const size_t size,
+ const size_t char_limit);
+
+#endif // UTIL_H
diff --git a/include/watcher.h b/include/watcher.h
new file mode 100644
index 0000000..ff31809
--- /dev/null
+++ b/include/watcher.h
@@ -0,0 +1,28 @@
+#ifndef WATCHER_H
+#define WATCHER_H
+
+#include <poll.h>
+#include <stdbool.h>
+
+#include "block.h"
+#include "main.h"
+
+enum watcher_fd_index {
+ SIGNAL_FD = BLOCK_COUNT,
+ WATCHER_FD_COUNT,
+};
+
+typedef struct pollfd watcher_fd;
+
+typedef struct {
+ watcher_fd fds[WATCHER_FD_COUNT];
+ unsigned short active_blocks[BLOCK_COUNT];
+ unsigned short active_block_count;
+ bool got_signal;
+} watcher;
+
+int watcher_init(watcher *const watcher, const block *const blocks,
+ const unsigned short block_count, const int signal_fd);
+int watcher_poll(watcher *const watcher, const int timeout_ms);
+
+#endif // WATCHER_H
diff --git a/include/x11.h b/include/x11.h
new file mode 100644
index 0000000..6faaced
--- /dev/null
+++ b/include/x11.h
@@ -0,0 +1,13 @@
+#ifndef X11_H
+#define X11_H
+
+#include <xcb/xcb.h>
+
+typedef xcb_connection_t x11_connection;
+
+x11_connection* x11_connection_open(void);
+void x11_connection_close(x11_connection* const connection);
+int x11_set_root_name(x11_connection* const connection,
+ const char* const name);
+
+#endif // X11_H
diff --git a/preview.png b/preview.png
new file mode 100644
index 0000000..5c00274
--- /dev/null
+++ b/preview.png
Binary files differ
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;
+}