diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile | 25 | ||||
-rw-r--r-- | README.md | 16 | ||||
-rw-r--r-- | UNLICENSE | 24 | ||||
-rw-r--r-- | mstatus.1 | 109 | ||||
-rw-r--r-- | mstatus.c | 289 |
6 files changed, 465 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18f6a90 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.clang-format +mstatus diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b4ba764 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +.POSIX: + +target = mstatus + +CC = cc +CFLAGS = -O3 -std=c11 -pedantic -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes +LDFLAGS = -lX11 + +PREFIX = /usr/local + +all: ${target} +${target}: mstatus.c + ${CC} ${CFLAGS} ${LDFLAGS} -o $@ $< + +.PHONY: clean install uninstall +clean: + rm -f ${target} ${objects} + +install: + mkdir -p ${DESTDIR}${PREFIX}/bin ${DESTDIR}${PREFIX}/share/man/man1 + cp -f ${target} ${DESTDIR}${PREFIX}/bin + cp -f ${target}.1 ${DESTDIR}${PREFIX}/share/man/man1 + +uninstall: + rm -f ${DESTDIR}${PREFIX}/bin/${target} ${DESTDIR}${PREFIX}/share/man/man1/${target}.1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..161293a --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +mstatus +======= +`mstatus` is a minimal status bar for the DWM window manager. The status bar is comprised of a +series of blocks which can be added, modified, and deleted by sending simple commands to a file. + +For usage instructions see the `mstatus(1)` manual page. + + +Installation +============ +Assuming you have cloned the repo, you can install the status bar and manual page in two commands. + +```sh +$ make +$ make install # Will require root permissions +``` diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to <http://unlicense.org/> diff --git a/mstatus.1 b/mstatus.1 new file mode 100644 index 0000000..475ca24 --- /dev/null +++ b/mstatus.1 @@ -0,0 +1,109 @@ +.Dd $Mdocdate: 2 August 2021 $ +.Dt MSTATUS 1 URM +.Os DWM +.Sh NAME +.Nm mstatus +.Nd a modular status bar for DWM +.Sh SYNOPSIS +.Nm +.Op Fl r +.Op Fl s Ar seperator +.Sh DESCRIPTION +.Nm +is a modular status bar for the +.Xr dwm 1 +window manager. +The +.Nm +status bar is comprised of a series of blocks which can be programmatically added, removed, and +updated. +These blocks appear side by side on the status bar seperated by a the string +.Dq " | " , +although this can be configured with the +.Fl s +flag. +Commands are sent to and from the status bar via a named pipe located at +.Pa $XDG_RUNTIME_DIR/mstatus.pipe +or +.Pa /run/user/$(id -u)/mstatus.pipe +if the +.Ev $XDG_RUNTIME_DIR +environment variable is not set. +.Ss SYNTAX +The syntax used to send commands is extremely simple. +Every command sent follows the form +.Dq (\-?)([0-9]*)(.*) +or in other words, an optional +.Sq \- +followed by an optional unsigned integer, followed by an optional string. +Starting at the beginning, a leading +.Sq \- +tells +.Nm +that you would like to remove a block from the status bar. +If the command does not begin with a +.Sq \- +then +.Nm +will attempt to create/update a block instead. +.Pp +Next, you can optionally provide a number which represents the block you want to act upon. +As an example, the command +.Dq \-10 +signals that you would like to remove block 10 from the status bar. +The command +.Dq 4 +on the other hand signals that you would like to create/update block 4. +If no block number is specified, then the specified action will be executed on block 1. +It is important to note that the command +.Dq 0 +will be ignored as there is no block 0, however the command +.Dq \-0 +is special in that it deletes all the blocks from the status bar. +.Pp +Finally, after you have provided the optional +.Sq \- +flag and have selected the block to act upon, you can provide any string which will be displayed in +the selected block. +If the +.Sq \- +flag was specified then this string will be simply ignored. +.Sh OPTIONS +.Bl -tag -width Ds +.It Fl r +Add a single space of padding to the right of the status bar. +.It Fl s Ar seperator +Set the block seperator to the string specified by +.Ar seperator +as opposed to the default of +.Dq " | " . +.El +.Sh EXAMPLES +Display the current time in block 1, and the current date in block 2: +.Pp +.Dl "$ date \(aq+%H:%M\(aq >$XDG_RUNTIME_DIR/mstatus.pipe # Note the implicit \(aq1\(aq" +.Dl "$ date \(aq+2%d/%m/%Y\(aq >$XDG_RUNTIME_DIR/mstatus.pipe # Note the leading \(aq2\(aq" +.Pp +Delete the 5th block: +.Pp +.Dl $ echo \(aq-5\(aq >$XDG_RUNTIME_DIR/mstatus.pipe +.Pp +Replace the entire status bar with +.Dq Hello world! : +.Pp +.Dl $ printf \(aq-0\enHello world!\(aq >$XDG_RUNTIME_DIR/mstatus.pipe +.Sh EXIT STATUS +.Ex -std +.Sh NOTES +.Nm +always allocates enough memory to be able to hold as many blocks as the number of the rightmost +block. +This means that if you created a block in slots 1 and 2, memory will be allocated for 2 blocks, +however if you create a block in slots 1, 2, and 300, then memory will be allocated for 300 blocks. +It is for this reason that you should avoid creating blocks in very high slots without reason. +Luckily if deleting the block in slot 300 from the above example, the memory for slots 3 to 300 will +all be freed. +.Sh BUGS +As of the initial 1.0 version you cannot have a block which begins with a digit. +.Sh SEE ALSO +.Xr dwm 1 diff --git a/mstatus.c b/mstatus.c new file mode 100644 index 0000000..2599db0 --- /dev/null +++ b/mstatus.c @@ -0,0 +1,289 @@ +#define _GNU_SOURCE +#include <sys/types.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <errno.h> +#include <linux/limits.h> +#include <paths.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <stdnoreturn.h> +#include <signal.h> +#include <string.h> +#include <syslog.h> +#include <unistd.h> + +#include <X11/Xlib.h> + +#define CTOI(x) ((x) ^ 48) + +struct Block { + int pos; + char *text; + bool remove; +}; + +bool rflag; +const char *argv0; +struct { + char *str; + size_t len; +} seperator = {.str = " | ", .len = 3}; + +static noreturn void +usage(void) +{ + fprintf(stderr, "Usage: %s [-r] [-s seperator]\n", argv0); + exit(EXIT_FAILURE); +} + +static noreturn void +die(const char *s) +{ + char *err = strerror(errno); + syslog(LOG_ERR, "%s: %s", s, err); + fprintf(stderr, "%s: %s: %s\n", argv0, s, err); + exit(EXIT_FAILURE); +} + +static void +xfork(void) +{ + pid_t pid = fork(); + if (pid == -1) + die("fork"); + if (pid != 0) + exit(EXIT_SUCCESS); +} + +static void * +xrealloc(void *ptr, size_t size) +{ + void *ret; + if (!(ret = realloc(ptr, size))) + die("realloc"); + return ret; +} + +static void +xfree(char **ptr) +{ + free(*ptr); + *ptr = NULL; +} + +static void +write_status(struct Block b) +{ + static struct { + char **blocks; + int count; + size_t length; + } sb; + + if (b.remove) { + if (b.pos > sb.count) + return; + + /* b.remove && !b.pos is a special case to remove everything from the bar */ + if (!b.pos) { + for (int i = 0; i < sb.count; i++) + free(sb.blocks[i]); + sb.count = 0; + sb.blocks = xrealloc(sb.blocks, sizeof(char *)); + goto update_bar; + } + + /* If the block is not NULL we free it */ + if (sb.blocks[--b.pos]) { + sb.length -= strlen(sb.blocks[b.pos]); + xfree(&sb.blocks[b.pos]); + } + + /* If the block is the last one, we resize the bar to remove trailing NULL blocks */ + if (b.pos + 1 == sb.count) { + for (; !sb.blocks[b.pos] && b.pos; b.pos--) + sb.count--; + sb.blocks = xrealloc(sb.blocks, sizeof(char *) * ++b.pos); + } + goto update_bar; + } + + /* If the position exceeds the space allocated, allocate more blocks */ + if (b.pos > sb.count) { + sb.blocks = xrealloc(sb.blocks, sizeof(char *) * b.pos); + /* Make sure to set all the newly allocated blocks to NULL */ + for (int i = sb.count; i < b.pos; i++) + sb.blocks[i] = NULL; + sb.count = b.pos; + } + + /* If the block is NULL we dont need to bother with strlen and free */ + if (sb.blocks[--b.pos]) { + sb.length -= strlen(sb.blocks[b.pos]); + xfree(&sb.blocks[b.pos]); + } + if (!(sb.blocks[b.pos] = strdup(b.text))) + die("strdup"); + sb.length += strlen(b.text); + + /* + * The buffer to store the text that will be displayed in. It needs space for the text, the + * seperators between the different blocks, the NUL byte at the end, and the right padding + * space. + */ +update_bar:; + char buf[sb.length + (sb.count - 1) * seperator.len + 2]; + memset(buf, '\0', sb.length + 1); + + /* Double for loops so that the seperator isnt printed to the left of the first block */ + int i; + for (i = 0; i < sb.count; i++) { + if (sb.blocks[i]) { + strcpy(buf, sb.blocks[i]); + break; + } + } + for (i++; i < sb.count; i++) + if (sb.blocks[i]) + sprintf(buf, "%s%s%s", buf, seperator.str, sb.blocks[i]); + if (rflag) + strcat(buf, " "); + + /* Xlib magic to set the DWM status */ + Display *dpy = XOpenDisplay(NULL); + int screen = DefaultScreen(dpy); + Window root = RootWindow(dpy, screen); + (void) XStoreName(dpy, root, buf); + (void) XCloseDisplay(dpy); +} + +static bool +process(char *line, ssize_t len, struct Block *b) +{ + /* For some reason output with newlines can cause performance issues */ + if (line[--len] == '\n') + line[len] = '\0'; + + if (*line == '-') { + b->remove = true; + line++; + } + else + b->remove = false; + + if (!isdigit(*line)) + b->pos = 1; /* Default position */ + else { + b->pos = 0; + while (isdigit(*line)) + b->pos = b->pos * 10 + CTOI(*line++); + if (!b->pos && !b->remove) + return false; + } + + b->text = line; + return true; +} + +static void +create_fifo(char *fifo_path) +{ + char *runtime_dir = getenv("XDG_RUNTIME_DIR"); + if (runtime_dir) { + size_t end = strlen(runtime_dir) - 1; + if (runtime_dir[end] == '/') + runtime_dir[end] = '\0'; + sprintf(fifo_path, "%s/%s.pipe", runtime_dir, argv0); + } + else + sprintf(fifo_path, _PATH_VARRUN "user/%d/%s.pipe", getuid(), argv0); + + umask(0); + +create_fifo: + if (mkfifo(fifo_path, DEFFILEMODE) == -1) { + if (errno == EEXIST) { + if (unlink(fifo_path) == -1) + die("unlink"); + goto create_fifo; + } + else + die("mkfifo"); + } + + syslog(LOG_INFO, "Created input FIFO '%s'", fifo_path); +} + +static void +daemonize(void) +{ + xfork(); + if (setsid() == -1) + die("setsid"); + + (void) signal(SIGCHLD, SIG_IGN); + xfork(); + + (void) chdir("/"); + (void) close(STDIN_FILENO); + (void) close(STDOUT_FILENO); + (void) close(STDERR_FILENO); + + stdin = fopen(_PATH_DEVNULL, "r"); + stdout = fopen(_PATH_DEVNULL, "w+"); + stderr = fopen(_PATH_DEVNULL, "w+"); + + syslog(LOG_INFO, "Daemonized '%s'", argv0); +} + +int +main(int argc, char **argv) +{ + (void) argc; + argv0 = argv[0]; + + int opt; + while ((opt = getopt(argc, argv, ":rs:")) != -1) { + switch (opt) { + case 'r': + rflag = true; + break; + case 's': + seperator.str = optarg; + seperator.len = strlen(optarg); + break; + default: + usage(); + } + } + + openlog(argv0, LOG_PID | LOG_CONS, LOG_DAEMON); + char fifo_path[PATH_MAX]; + create_fifo(fifo_path); + daemonize(); + + char *line = NULL; + size_t len = 0; + while (true) { + FILE *fp; + if (!(fp = fopen(fifo_path, "r"))) + die("fopen"); + + ssize_t nr; + while ((nr = getline(&line, &len, fp)) != -1) { + syslog(LOG_INFO, "Recieved command '%s'", line); + struct Block b; + if (!process(line, nr, &b)) + continue; + write_status(b); + } + if (ferror(fp)) + die("getline"); + + (void) fclose(fp); + } + /* NOTREACHED */ +} |