aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Makefile25
-rw-r--r--README.md16
-rw-r--r--UNLICENSE24
-rw-r--r--mstatus.1109
-rw-r--r--mstatus.c289
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 */
+}