From 40bc40b1a0cc1044fca8c3b2cfbdc8a487d1c6fd Mon Sep 17 00:00:00 2001 From: Kris Yotam Date: Tue, 27 Jan 2026 03:47:21 -0600 Subject: [PATCH] Switch to dwmblocks-async for better performance Async version prevents slow scripts from blocking status bar. Co-Authored-By: Claude Opus 4.5 --- .clang-format | 8 + .clang-tidy | 30 ++++ .clangd | 5 + .github/FUNDING.yml | 4 + .gitignore | 3 + LICENSE | 339 +++++++++++++++++++++++++++++++++++++++ Makefile | 57 +++++++ README.md | 164 +++++++++++++++++++ config.h | 40 +++++ include/block.h | 29 ++++ include/cli.h | 12 ++ include/main.h | 16 ++ include/signal-handler.h | 33 ++++ include/status.h | 31 ++++ include/timer.h | 21 +++ include/util.h | 28 ++++ include/watcher.h | 28 ++++ include/x11.h | 13 ++ preview.png | Bin 0 -> 54091 bytes src/block.c | 147 +++++++++++++++++ src/cli.c | 33 ++++ src/main.c | 168 +++++++++++++++++++ src/signal-handler.c | 126 +++++++++++++++ src/status.c | 78 +++++++++ src/timer.c | 72 +++++++++ src/util.c | 49 ++++++ src/watcher.c | 69 ++++++++ src/x11.c | 44 +++++ 28 files changed, 1647 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .clangd create mode 100644 .github/FUNDING.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 config.h create mode 100644 include/block.h create mode 100644 include/cli.h create mode 100644 include/main.h create mode 100644 include/signal-handler.h create mode 100644 include/status.h create mode 100644 include/timer.h create mode 100644 include/util.h create mode 100644 include/watcher.h create mode 100644 include/x11.h create mode 100644 preview.png 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 diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000000000000000000000000000000000000..299675fe2bdc256bc96999e9c589fef7ffbea201 --- /dev/null +++ b/.clang-format @@ -0,0 +1,8 @@ +BasedOnStyle: Google +IndentWidth: 4 +InsertBraces: true +ColumnLimit: 79 +AlignConsecutiveMacros: Consecutive +AllowShortFunctionsOnASingleLine: None +AllowShortLoopsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: Never diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000000000000000000000000000000000000..17ce268282baa391d65c4b16c9eca1f27b910eae --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,30 @@ +Checks: | + -*, + abseil-*, + bugprone-*, + clang-analyzer-*, + misc-*, + modernize-*, + performance-*, + portability-*, + readability-*, + llvm-*, + -bugprone-easily-swappable-parameters, + -readability-avoid-const-params-in-decls, + -readability-identifier-length + +CheckOptions: + - key: readability-inconsistent-declaration-parameter-name.Strict + value: true + - key: readability-identifier-naming.StructCase + value: lower_case + - key: readability-identifier-naming.FunctionCase + value: lower_case + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.EnumConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.MacroDefinitionCase + value: UPPER_CASE + - key: readability-function-cognitive-complexity.Threshold + value: 15 diff --git a/.clangd b/.clangd new file mode 100644 index 0000000000000000000000000000000000000000..d5ba52443a24e5dc9b0a6944064539218347f0af --- /dev/null +++ b/.clangd @@ -0,0 +1,5 @@ +Diagnostics: + UnusedIncludes: Strict + MissingIncludes: Strict + Includes: + IgnoreHeader: bits/getopt_core.h diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000000000000000000000000000000000..c7fd5901b24699e77c7a0153699e18d4372380b8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +patreon: UtkarshVerma +ko_fi: UtkarshVerma +liberapay: UtkarshVerma +custom: https://paypal.me/UtkarshVermaI diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..902f3c724a472754287a96bd687833552e33cc17 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +.cache/ +compile_commands.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..d159169d1050894d3ea3b98e1c965c4058208fe1 --- /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. + + + Copyright (C) + + 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. + + , 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 0000000000000000000000000000000000000000..635673ecab715a95233e5d236899945e88efcc30 --- /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 0000000000000000000000000000000000000000..1b389f6fe52b03e4e61bdcbbe440eac626b9d631 --- /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 0000000000000000000000000000000000000000..58f8330005ba0c499b88f25ab77d7786bbbb2648 --- /dev/null +++ b/config.h @@ -0,0 +1,40 @@ +#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). +// Blocks from krisyotam's setup - async version +#define BLOCKS(X) \ + X("", "cat /tmp/recordingicon 2>/dev/null", 0, 9) \ + X("", "sb-playerctl", 0, 11) \ + X("", "sb-pacpackages", 0, 8) \ + X("", "sb-news", 0, 6) \ + X("", "sb-torrent", 20, 7) \ + X("", "sb-temps", 30, 20) \ + X("", "sb-disk", 60, 21) \ + X("", "sb-aurora", 3600, 19) \ + X("", "sb-doppler", 0, 13) \ + X("", "sb-forecast", 18000, 5) \ + X("", "sb-mailbox", 180, 12) \ + X("", "sb-nettraf", 1, 16) \ + X("", "sb-volume", 0, 10) \ + X("", "sb-battery", 5, 3) \ + X("", "sb-clock", 60, 1) \ + X("", "sb-internet", 5, 4) \ + X("", "sb-help-icon", 0, 15) + +#endif // CONFIG_H diff --git a/include/block.h b/include/block.h new file mode 100644 index 0000000000000000000000000000000000000000..c4f8d5422e14bb2d30fbbf1d0b183bc69bfcd530 --- /dev/null +++ b/include/block.h @@ -0,0 +1,29 @@ +#ifndef BLOCK_H +#define BLOCK_H + +#include +#include +#include + +#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 0000000000000000000000000000000000000000..2f93f62d2d9e32c633c3d91c46bf95e5b250f0f0 --- /dev/null +++ b/include/cli.h @@ -0,0 +1,12 @@ +#ifndef CLI_H +#define CLI_H + +#include + +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 0000000000000000000000000000000000000000..b37a6b1b2a619fc6da032dede381db8825c044c2 --- /dev/null +++ b/include/main.h @@ -0,0 +1,16 @@ +#ifndef MAIN_H +#define MAIN_H + +#include + +#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 0000000000000000000000000000000000000000..da2d4715a560446f8a46a5779d8ddf3a6db6330b --- /dev/null +++ b/include/signal-handler.h @@ -0,0 +1,33 @@ +#ifndef SIGNAL_HANDLER_H +#define SIGNAL_HANDLER_H + +#include + +#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 0000000000000000000000000000000000000000..48fb3d87ebce831ce073194dc7d84f2c1a9a5a64 --- /dev/null +++ b/include/status.h @@ -0,0 +1,31 @@ +#ifndef STATUS_H +#define STATUS_H + +#include + +#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 0000000000000000000000000000000000000000..1ec7f7567716f4079896ecae1d234a720850ad0a --- /dev/null +++ b/include/timer.h @@ -0,0 +1,21 @@ +#ifndef TIMER_H +#define TIMER_H + +#include +#include + +#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 0000000000000000000000000000000000000000..a3bdcceb5c947cec1a4b919736149e067fca0888 --- /dev/null +++ b/include/util.h @@ -0,0 +1,28 @@ +#ifndef UTIL_H +#define UTIL_H + +#include + +#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 0000000000000000000000000000000000000000..ff31809e689cfe2d9fea74f03f0f0cd592772de2 --- /dev/null +++ b/include/watcher.h @@ -0,0 +1,28 @@ +#ifndef WATCHER_H +#define WATCHER_H + +#include +#include + +#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 0000000000000000000000000000000000000000..6faaced040d18a876bd73a11a3c81228178ab0dc --- /dev/null +++ b/include/x11.h @@ -0,0 +1,13 @@ +#ifndef X11_H +#define X11_H + +#include + +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 0000000000000000000000000000000000000000..5c0027495a22428f96da28541a827ae826d15d79 GIT binary patch literal 54091 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 +#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 0000000000000000000000000000000000000000..8b94cc8c11c39433b08fd1fc6d35b05670d6916b --- /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 0000000000000000000000000000000000000000..747b075f3b4f5d5087ba2b31e2cc8ac36526a9a7 --- /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 0000000000000000000000000000000000000000..7a1a9666d2a036f4585b274fad3599714a0bc0c6 --- /dev/null +++ b/src/signal-handler.c @@ -0,0 +1,126 @@ +#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; + default: + break; + } + + 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 0000000000000000000000000000000000000000..cf0911a74e8b0100c871ea87a9d8e7017ac06347 --- /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 0000000000000000000000000000000000000000..2ee555b762f98c33ea90645840cc2320c39c722f --- /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 0000000000000000000000000000000000000000..10485db735d4580302a4e2fba38dba394c58343b --- /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 0000000000000000000000000000000000000000..71b6c52b726c55714257fc668432f3fd2051c6ca --- /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 0000000000000000000000000000000000000000..7a310e92dfe8905e2ce4fa4bb5e58102eb7fafe0 --- /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; +}