~rootmos/action

579d7b68ce22c30486c5c8fd16276590dd4b2549 — Gustav Behm 23 days ago 2addd23 + a55b846 master
Merge branch 'c-impl'
14 files changed, 1148 insertions(+), 3 deletions(-)

M README.md
A c-impl/.gitignore
A c-impl/Makefile
A c-impl/action.c
A c-impl/r.h
R .gitignore => py-impl/.gitignore
R Makefile => py-impl/Makefile
R Pipfile => py-impl/Pipfile
R Pipfile.lock => py-impl/Pipfile.lock
R pyproject.toml => py-impl/pyproject.toml
R setup.cfg => py-impl/setup.cfg
R src/action/__init__.py => py-impl/src/action/__init__.py
R src/action/cli.py => py-impl/src/action/cli.py
R src/action/util.py => py-impl/src/action/util.py
M README.md => README.md +3 -3
@@ 1,10 1,10 @@
# action
Trigger action `X` from thing `Y`.

For example, bind the F11 key to `action trigger f11` in [your favorite window manager](https://xmonad.org/) and
run:
For example, bind the F11 key to `action -t f11` (`-t` for trigger) in [your favorite window manager](https://xmonad.org/) and
run (`-b` for bind):

```while action bind f11; do screenshot; done```
```while action -b f11; do screenshot; done```

in [your favorite terminal emulator](https://st.suckless.org/) when you're working with something you want to screenshot.
Note that the `f11` string is arbitrary, i.e. go crazy, what about `volume_up`, `fix_wifi`, `call_mom`?

A c-impl/.gitignore => c-impl/.gitignore +1 -0
@@ 0,0 1,1 @@
*.exe

A c-impl/Makefile => c-impl/Makefile +25 -0
@@ 0,0 1,25 @@
CC = gcc

CFLAGS = -Wall -Werror -O1
LDFLAGS =
LOG_LEVEL ?= WARN
EXTRA_CFLAGS ?= -DLOG_LEVEL=LOG_$(LOG_LEVEL)
EXTRA_LDFLAGS ?=

export PREFIX ?= $(HOME)/.local

EXE = action.exe

.PHONY: install
install: build
	install --strip -D $(EXE) $(PREFIX)/bin/$(EXE:%.exe=%)

.PHONY: build
build: $(EXE)

%.exe: %.c r.h
	$(CC) $(CFLAGS) $(EXTRA_CFLAGS) -o $@ $< $(LDFLAGS) $(EXTRA_LDFLAGS)

.PHONY: clean
clean:
	rm -f *.exe

A c-impl/action.c => c-impl/action.c +313 -0
@@ 0,0 1,313 @@
#define _GNU_SOURCE

#include <poll.h>
#include <signal.h>
#include <sys/signalfd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <string.h>

#define LIBR_IMPLEMENTATION
#include "r.h"

#define ACTION_MAX (NAME_MAX)

struct options {
    char action[ACTION_MAX+1];

    enum {
        NOOP = 0,
        BIND,
        PRINT_PATH,
        TRIGGER,
    } cmd;
};

static void print_usage(int fd, const char* prog)
{
    dprintf(fd, "usage: %s [OPTION]... ACTION\n", prog);
    dprintf(fd, "\n");
    dprintf(fd, "commands:\n");
    dprintf(fd, "  -b       bind action\n");
    dprintf(fd, "  -t       trigger action\n");
    dprintf(fd, "  -p       print socket path\n");
    dprintf(fd, "  -h       print this message\n");
}

static void parse_options(struct options* o, int argc, char* argv[])
{
    memset(o, 0, sizeof(*o));

    int res;
    while((res = getopt(argc, argv, "btph")) != -1) {
        switch(res) {
        case 't': o->cmd = TRIGGER; break;
        case 'b': o->cmd = BIND; break;
        case 'p': o->cmd = PRINT_PATH; break;
        case 'h':
        default:
            print_usage(res == 'h' ? 1 : 2, argv[0]);
            exit(res == 'h' ? 0 : 2);
        }
    }

    if(optind + 1 == argc) {
        if(strnlen(argv[optind], ACTION_MAX) > ACTION_MAX) {
            dprintf(2, "error: action too long\n");
            exit(2);
        }
        strncpy(o->action, argv[optind], ACTION_MAX);
        debug("action: %s", o->action);
    } else {
        if(optind >= argc) {
            dprintf(2, "error: action not specified\n");
        } else {
            dprintf(2, "error: too many actions specified\n");
        }
        print_usage(2, argv[0]);
        exit(2);
    }

    if(o->cmd == NOOP) {
        dprintf(2, "error: command not specified\n");
        print_usage(2, argv[0]);
        exit(2);
    }
}

struct msg {
    char action[ACTION_MAX];
    struct timespec timestamp;
};

static const char* iso8601_timespec(const struct timespec* ts)
{
    static char buf[21];
    size_t r = strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", gmtime(&ts->tv_sec));
    if(r <= 0) abort();
    return buf;
}

static void prepare_sockaddr(struct sockaddr_un* addr, const char* socket_path)
{
    memset(addr, 0, sizeof(*addr));

    addr->sun_family = AF_UNIX;

    if(strlen(socket_path) >= sizeof(addr->sun_path)) {
        failwith("socket path too long");
    }

    strncpy(addr->sun_path, socket_path, sizeof(addr->sun_path) - 1);
}

static int cmd_trigger(const struct options* o, const char* socket_path)
{
    debug("triggering: %s", o->action);

    int fd = socket(AF_UNIX, SOCK_DGRAM, 0);

    struct sockaddr_un addr;
    prepare_sockaddr(&addr, socket_path);

    struct msg msg = {0};
    strncpy(msg.action, o->action, sizeof(msg.action));
    int r = clock_gettime(CLOCK_REALTIME, &msg.timestamp);
    CHECK(r, "clock_gettime(CLOCK_REALTIME)");

    ssize_t s = sendto(fd,
            &msg, sizeof(msg),
            0,
            (const struct sockaddr*)&addr, sizeof(addr));
    CHECK(s, "sendto(%s)", socket_path);

    if(s != sizeof(msg)) {
        failwith("unexpected partial sendto");
    }

    info("triggered: %s (%s)", o->action, iso8601_timespec(&msg.timestamp));

    r = close(fd); CHECK(r, "close");
    return 0;
}

struct state {
    int running;
    int exit_code;
    int sfd;
};

static int signalfd_init(struct state* st)
{
    sigset_t m;
    sigemptyset(&m);
    sigaddset(&m, SIGINT);
    sigaddset(&m, SIGTERM);

    int fd = st->sfd = signalfd(-1, &m, 0);
    CHECK(fd, "signalfd");

    int r = sigprocmask(SIG_BLOCK, &m, NULL);
    CHECK(r, "sigprocmask");

    set_blocking(fd, 0);

    return fd;
}

static void signalfd_deinit(struct state* st)
{
    int r = close(st->sfd); CHECK(r, "close");
    st->sfd = -1;
}

static void signalfd_handle_event(struct state* st)
{
    while(1) {
        struct signalfd_siginfo si;

        ssize_t s = read(st->sfd, &si, sizeof(si));
        if(s == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
            break;
        }
        CHECK(s, "read");

        if(s != sizeof(si)) {
            failwith("unexpected partial read");
        }

        if(si.ssi_signo == SIGINT) {
            debug("SIGINT");
            st->running = 0;
            st->exit_code = 1;
        } else if(si.ssi_signo == SIGTERM) {
            debug("SIGTERM");
            st->running = 0;
            st->exit_code = 1;
        } else {
            warning("unhandled signal: %u", si.ssi_signo);
        }
    }
}

static void prepare_socket_path(const char* socket_path)
{
    struct stat st;
    int r = stat(socket_path, &st);
    if(r == 0) {
        if((st.st_mode & S_IFMT) == S_IFSOCK) {
            debug("socket already exists, trying to unlink: %s", socket_path);
            r = unlink(socket_path);
            CHECK(r, "unlink(%s)", socket_path);
        } else {
            failwith("socket path already exists (and is not a socket): %s", socket_path);
        }
    } else if(r == -1 && errno != ENOENT) {
        CHECK(r, "stat(%s)", socket_path);
    } else {
        xdg_makedirs(dirname(strdupa(socket_path)));
    }
}

static int cmd_bind(const struct options* o, const char* socket_path)
{
    int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
    CHECK(fd, "socket(AF_UNIX, SOCK_DGRAM, 0)");

    struct sockaddr_un addr;
    prepare_sockaddr(&addr, socket_path);

    prepare_socket_path(socket_path);

    int r = bind(fd, (const struct sockaddr*)&addr, sizeof(addr));
    CHECK(r, "bind(%s)", socket_path);

    set_blocking(fd, 0);

    struct state st = {
        .running = 1,
        .exit_code = 255,
    };

    struct pollfd fds[] = {
        { .fd = signalfd_init(&st), .events = POLLIN },
        { .fd = fd, .events = POLLIN },
    };

    while(st.running) {
        int r = poll(fds, LENGTH(fds), -1);
        CHECK(r, "poll");

        if(fds[0].revents & POLLIN) {
            signalfd_handle_event(&st);
            fds[0].revents &= ~POLLIN;
        }

        if(fds[1].revents & POLLIN) {
            struct msg msg;
            ssize_t s = recvfrom(fd, &msg, sizeof(msg), 0, NULL, NULL);
            CHECK(s, "recvfrom(%s)", socket_path);

            if(s != sizeof(msg)) {
                failwith("unexpected partial recvfrom");
            }

            if(strncmp(msg.action, o->action, sizeof(msg.action)) == 0) {
                info("triggered: %s (%s)", o->action, iso8601_timespec(&msg.timestamp));
                st.running = 0;
                st.exit_code = 0;
            }

            fds[1].revents &= ~POLLIN;
        }

        for(size_t i = 0; i < LENGTH(fds); i++) {
            if(fds[i].revents != 0) {
                failwith("unhandled poll events: "
                         "fds[%zu] = { .fd = %d, .revents = %hd }",
                         i, fds[i].fd, fds[i].revents);
            }
        }
    }

    debug("graceful shutdown");
    signalfd_deinit(&st);

    r = close(fd); CHECK(r, "close");
    r = unlink(socket_path);
    CHECK(r, "unlink(%s)", socket_path);

    return st.exit_code;
}

int main(int argc, char* argv[])
{
    struct options o;
    parse_options(&o, argc, argv);

    struct xdg* xdg = xdg_new("action");

    char socket_path[PATH_MAX];
    xdg_resolve(xdg, XDG_RUNTIME, LIT(socket_path), o.action, NULL);
    debug("socket path: %s", socket_path);

    int ec = 255;

    if(o.cmd == PRINT_PATH) {
        // ensure the path is a valid socket address
        struct sockaddr_un addr;
        prepare_sockaddr(&addr, socket_path);

        printf("%s\n", socket_path);
        ec = 0;
    } else if(o.cmd == TRIGGER) {
        ec = cmd_trigger(&o, socket_path);
    } else if(o.cmd == BIND) {
        ec = cmd_bind(&o, socket_path);
    }

    xdg_free(xdg);

    return ec;
}

A c-impl/r.h => c-impl/r.h +806 -0
@@ 0,0 1,806 @@
// libr 0.5.0 (ec3c888754c9966a470ff5a8d1baf1cfaa3045d2) (https://github.com/rootmos/libr.git) (2024-03-29T04:47:48+01:00)
// modules: fail logging now util nonblock xdg path

#ifndef LIBR_HEADER
#define LIBR_HEADER

#define LIBR(x) x
#define PRIVATE __attribute__((visibility("hidden")))
#define PUBLIC __attribute__((visibility("default")))
#define API PRIVATE


// libr: fail.h

#define CHECK(res, format, ...) CHECK_NOT(res, -1, format, ##__VA_ARGS__)

#define CHECK_NOT(res, err, format, ...) \
    CHECK_IF(res == err, format, ##__VA_ARGS__)

#define CHECK_IF(cond, format, ...) do { \
    if(cond) { \
        LIBR(failwith0)(__extension__ __FUNCTION__, __extension__ __FILE__, \
            __extension__ __LINE__, 1, \
            format "\n", ##__VA_ARGS__); \
    } \
} while(0)

#define CHECK_MALLOC(x) CHECK_NOT(x, NULL, "memory allocation failed")
#define CHECK_MMAP(x) CHECK_NOT(x, MAP_FAILED, "memory mapping failed")

#define failwith(format, ...) \
    LIBR(failwith0)(__extension__ __FUNCTION__, __extension__ __FILE__, \
        __extension__ __LINE__, 0, format "\n", ##__VA_ARGS__)

#define not_implemented() LIBR(failwith0)("not implemented")

void LIBR(failwith0)(
    const char* const caller,
    const char* const file,
    const unsigned int line,
    const int include_errno,
    const char* const fmt, ...)
__attribute__ ((noreturn, format (printf, 5, 6)));

// libr: logging.h

#include <stdarg.h>

#define LOG_QUIET 0
#define LOG_ERROR 1
#define LOG_WARNING 2
#define LOG_INFO 3
#define LOG_DEBUG 4
#define LOG_TRACE 5

#ifndef LOG_LEVEL
#define LOG_LEVEL LOG_INFO
#endif

extern int LIBR(logger_fd);

#define __r_log(level, format, ...) do { \
    LIBR(logger)(level, __extension__ __FUNCTION__, __extension__ __FILE__, \
          __extension__ __LINE__, format "\n", ##__VA_ARGS__); \
} while(0)

#ifdef __cplusplus
void LIBR(dummy)(...);
#else
void LIBR(dummy)();
#endif

#if LOG_LEVEL >= LOG_ERROR
#define error(format, ...) __r_log(LOG_ERROR, format, ##__VA_ARGS__)
#else
#define error(format, ...) do { if(0) LIBR(dummy)(__VA_ARGS__); } while(0)
#endif

#if LOG_LEVEL >= LOG_WARNING
#define warning(format, ...) __r_log(LOG_WARNING, format, ##__VA_ARGS__)
#else
#define warning(format, ...) do { if(0) LIBR(dummy)(__VA_ARGS__); } while(0)
#endif

#if LOG_LEVEL >= LOG_INFO
#define info(format, ...) __r_log(LOG_INFO, format, ##__VA_ARGS__)
#else
#define info(format, ...) do { if(0) LIBR(dummy)(__VA_ARGS__); } while(0)
#endif

#if LOG_LEVEL >= LOG_DEBUG
#define debug(format, ...) __r_log(LOG_DEBUG, format, ##__VA_ARGS__)
#else
#define debug(format, ...) do { if(0) LIBR(dummy)(__VA_ARGS__); } while(0)
#endif

#if LOG_LEVEL >= LOG_TRACE
#define trace(format, ...) __r_log(LOG_TRACE, format, ##__VA_ARGS__)
#else
#define trace(format, ...) do { if(0) LIBR(dummy)(__VA_ARGS__); } while(0)
#endif

void LIBR(logger)(
    int level,
    const char* const caller,
    const char* const file,
    const unsigned int line,
    const char* const fmt, ...)
__attribute__ ((format (printf, 5, 6)));

void LIBR(vlogger)(
    int level,
    const char* const caller,
    const char* const file,
    const unsigned int line,
    const char* const fmt,
    va_list vl
);

// libr: now.h

// returns current time formated as compact ISO8601: 20190123T182628Z
const char* LIBR(now_iso8601_compact)(void);

// libr: util.h

#ifndef LENGTH
#define LENGTH(xs) (sizeof(xs)/sizeof((xs)[0]))
#endif

#ifndef LIT
#define LIT(x) x,sizeof(x)
#endif

#ifndef STR
#define STR(x) x,strlen(x)
#endif

#ifndef MAX
#define MAX(a,b) ((a) > (b) ? (a) : (b))
#endif

#ifndef MIN
#define MIN(a,b) ((a) < (b) ? (a) : (b))
#endif

// libr: nonblock.h

void LIBR(set_blocking)(int fd, int blocking);

// libr: xdg.h

#include <stddef.h>
#include <stdarg.h>

// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html

struct xdg;

enum xdg_kind {
    XDG_HOME = 0,
    XDG_DATA,
    XDG_CONFIG,
    XDG_STATE,
    XDG_CACHE,
    XDG_RUNTIME,
    XDG_KINDS,
};

struct xdg* LIBR(xdg_new)(const char* app);
void LIBR(xdg_free)(struct xdg* xdg);

const char* LIBR(xdg_dir)(struct xdg* xdg, enum xdg_kind k);
const char** LIBR(xdg_dirs)(struct xdg* xdg, enum xdg_kind k);

const char* LIBR(xdg_home)(struct xdg* xdg);
const char* LIBR(xdg_data_home)(struct xdg* xdg);
const char* LIBR(xdg_config_home)(struct xdg* xdg);
const char* LIBR(xdg_state_home)(struct xdg* xdg);
const char* LIBR(xdg_cache_home)(struct xdg* xdg);
const char* LIBR(xdg_runtime)(struct xdg* xdg);

const char** LIBR(xdg_data_dirs)(struct xdg* xdg);
const char** LIBR(xdg_config_dirs)(struct xdg* xdg);

const char* LIBR(xdg_resolve)(struct xdg* xdg, enum xdg_kind k, char* buf, size_t L, ...);
const char* LIBR(xdg_resolvev)(struct xdg* xdg, enum xdg_kind k, char* buf, size_t L, va_list ps);
const char* LIBR(xdg_resolves)(struct xdg* xdg, enum xdg_kind k, ...);
const char* LIBR(xdg_resolvevs)(struct xdg* xdg, enum xdg_kind k, va_list ps);

const char* LIBR(xdg_preparev)(struct xdg* xdg, enum xdg_kind k, char* buf, size_t L, va_list ps);
const char* LIBR(xdg_prepare)(struct xdg* xdg, enum xdg_kind k, char* buf, size_t L, ...);
const char* LIBR(xdg_preparevs)(struct xdg* xdg, enum xdg_kind k, va_list ps);
const char* LIBR(xdg_prepares)(struct xdg* xdg, enum xdg_kind k, ...);

// libr: path.h

#include <stdarg.h>
#include <stddef.h>
#include <sys/types.h>

size_t LIBR(path_join)(char* buf, size_t L, const char* p0, ...);
size_t LIBR(path_joinv)(char* buf, size_t L, const char* p0, va_list ps);

// using static buffer
#ifdef failwith
const char* LIBR(path_joins)(const char* p0, ...);
const char* LIBR(path_joinvs)(const char* p0, va_list ps);
#endif

int LIBR(makedirs)(const char* path, mode_t mode);
#endif // LIBR_HEADER

#ifdef LIBR_IMPLEMENTATION

// libr: fail.c

#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

API void LIBR(failwith0)(
    const char* const caller,
    const char* const file,
    const unsigned int line,
    const int include_errno,
    const char* const fmt, ...)
{
    va_list vl;
    va_start(vl, fmt);

    if(include_errno) {
        LIBR(logger)(LOG_ERROR, caller, file, line, "(%s) ", strerror(errno));
        if(vdprintf(LIBR(logger_fd), fmt, vl) < 0) {
            abort();
        }
    } else {
        LIBR(vlogger)(LOG_ERROR, caller, file, line, fmt, vl);
    }
    va_end(vl);

    abort();
}

// libr: logging.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

#ifdef __cplusplus
API void LIBR(dummy)(...)
#else
API void LIBR(dummy)()
#endif
{
    abort();
}

int LIBR(logger_fd) API = 2;

API void LIBR(vlogger)(
    int level,
    const char* const caller,
    const char* const file,
    const unsigned int line,
    const char* const fmt, va_list vl)
{
    int r = dprintf(LIBR(logger_fd), "%s:%d:%s:%s:%u ",
        LIBR(now_iso8601_compact)(), getpid(), caller, file, line);
    if(r < 0) {
        abort();
    }

    r = vdprintf(LIBR(logger_fd), fmt, vl);
    if(r < 0) {
        abort();
    }
}

API void LIBR(logger)(
    int level,
    const char* const caller,
    const char* const file,
    const unsigned int line,
    const char* const fmt, ...)
{
    va_list vl;
    va_start(vl, fmt);
    LIBR(vlogger)(level, caller, file, line, fmt, vl);
    va_end(vl);
}

// libr: now.c

#include <time.h>
#include <stdlib.h>

PRIVATE const char* LIBR(now_iso8601_compact)(void)
{
    static char buf[17];
    const time_t t = time(NULL);
    size_t r = strftime(buf, sizeof(buf), "%Y%m%dT%H%M%SZ", gmtime(&t));
    if(r <= 0) abort();
    return buf;
}

// libr: nonblock.c

#include <fcntl.h>

API void LIBR(set_blocking)(int fd, int blocking)
{
    int fl = fcntl(fd, F_GETFL, 0);
    if(blocking) {
        fl &= ~O_NONBLOCK;
    } else {
        fl |= O_NONBLOCK;
    }

    int r = fcntl(fd, F_SETFL, fl);
    CHECK(r, "fcntl(%d, F_SETFL, %d)", fd, fl);
}

// libr: xdg.c

#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <libgen.h>

struct xdg {
    char app[NAME_MAX];

    char* dir[XDG_KINDS];
    char** dirs[XDG_KINDS];
};

API struct xdg* LIBR(xdg_new)(const char* app)
{
    struct xdg* xdg = calloc(1, sizeof(*xdg));
    CHECK_MALLOC(xdg);

    if(app) {
        const size_t N = sizeof(xdg->app);
        size_t n = strnlen(app, N);
        if(n >= N) {
            failwith("application name truncated at %zu characters", n);
        }

        if(n > 0) {
            memcpy(xdg->app, app, n);
        }
    }

    return xdg;
}

API void LIBR(xdg_free)(struct xdg* xdg)
{
    if(xdg == NULL) return;

    for(int i = 0; i < XDG_KINDS; i++) {
        free(xdg->dir[i]);
        free(xdg->dirs[i]);
    }

    free(xdg);
}

static size_t xdg_check_path(const char* p)
{
    if(!p) return 0;
    size_t r = strnlen(p, PATH_MAX);
    if(r >= PATH_MAX) return 0;
    if(r == 0) return 0;
    if(p[0] != '/') return 0;
    return r;
}

static size_t xdg_append_app(const struct xdg* xdg, char* buf, size_t L, const char* p)
{
    if(xdg->app[0]) {
        size_t l = LIBR(path_join)(buf, L, p, xdg->app, NULL);
        if(l >= L) {
            failwith("buffer overflow");
        }

        return l;
    } else {
        return strlen(buf);
    }
}

static size_t LIBR(xdg_fallback_runtime_dir)(char* buf, size_t L)
{
    char template[NAME_MAX];
    size_t n = snprintf(LIT(template), "/tmp/xdg-runtime-fallback-%d-XXXXXX", geteuid());
    if(n >= sizeof(template)) {
        failwith("buffer overflow");
    }

    // mkdtemp(3): "The directory is then created with permissions 0700."
    char* tmp = mkdtemp(template);
    CHECK_NOT(tmp, NULL, "mkdtemp(%s)", template);
    warning("using fallback directory for an unset XDG_RUNTIME_DIR: %s", tmp);
    return snprintf(buf, L, "%s", tmp);
}

API const char* LIBR(xdg_dir)(struct xdg* xdg, enum xdg_kind k)
{
    if(xdg->dir[k]) return xdg->dir[k];

    if(k == XDG_HOME) {
        char* home = getenv("HOME");
        if(!home) {
            failwith("unable to resolve HOME variable");
        }

        xdg->dir[k] = strdup(home);
        CHECK_MALLOC(xdg->dir[k]);
        return xdg->dir[k];
    }

    char buf[PATH_MAX];
    size_t l;

    const char* v = NULL;
    switch(k) {
        case XDG_DATA: v = "XDG_DATA_HOME"; break;
        case XDG_CONFIG: v = "XDG_CONFIG_HOME"; break;
        case XDG_STATE: v = "XDG_STATE_HOME"; break;
        case XDG_CACHE: v = "XDG_CACHE_HOME"; break;
        case XDG_RUNTIME: v = "XDG_RUNTIME_DIR"; break;
        default: failwith("unexpected kind: %d", k);
    }

    const char* e = getenv(v);
    if(xdg_check_path(e)) {
        l = LIBR(path_join)(LIT(buf), e, NULL);
    } else {
        switch(k) {
            case XDG_DATA:
                l = LIBR(path_join)(LIT(buf), LIBR(xdg_home)(xdg), ".local", "share", NULL);
                break;
            case XDG_CONFIG:
                l = LIBR(path_join)(LIT(buf), LIBR(xdg_home)(xdg), ".config", NULL);
                break;
            case XDG_STATE:
                l = LIBR(path_join)(LIT(buf), LIBR(xdg_home)(xdg), ".local", "state", NULL);
                break;
            case XDG_CACHE:
                l = LIBR(path_join)(LIT(buf), LIBR(xdg_home)(xdg), ".cache", NULL);
                break;
            case XDG_RUNTIME:
                l = LIBR(xdg_fallback_runtime_dir)(LIT(buf));
                break;
            default:
                failwith("unexpected kind: %d", k);
        }
    }
    if(l >= sizeof(buf)) {
        failwith("buffer overflow");
    }

    xdg_append_app(xdg, LIT(buf), buf);

    xdg->dir[k] = strdup(buf);
    CHECK_MALLOC(xdg->dir[k]);

    return xdg->dir[k];
}

API const char* LIBR(xdg_home)(struct xdg* xdg)
{
    return LIBR(xdg_dir)(xdg, XDG_HOME);
}

API const char* LIBR(xdg_data_home)(struct xdg* xdg)
{
    return LIBR(xdg_dir)(xdg, XDG_DATA);
}

API const char* LIBR(xdg_config_home)(struct xdg* xdg)
{
    return LIBR(xdg_dir)(xdg, XDG_CONFIG);
}

API const char* LIBR(xdg_state_home)(struct xdg* xdg)
{
    return LIBR(xdg_dir)(xdg, XDG_STATE);
}

API const char* LIBR(xdg_cache_home)(struct xdg* xdg)
{
    return LIBR(xdg_dir)(xdg, XDG_CACHE);
}

API const char* LIBR(xdg_runtime)(struct xdg* xdg)
{
    return LIBR(xdg_dir)(xdg, XDG_RUNTIME);
}

API const char** LIBR(xdg_dirs)(struct xdg* xdg, enum xdg_kind k)
{
    if(xdg->dirs[k]) return (const char**)xdg->dirs[k];

    const char* e = NULL;
    if(k == XDG_DATA || k == XDG_CONFIG) {
        const char* v = NULL;
        if(k == XDG_DATA) {
            v = "XDG_DATA_DIRS";
        } else if(k == XDG_CONFIG) {
            v = "XDG_CONFIG_DIRS";
        }

        e = getenv(v);
        if(e == NULL || *e == 0) {
            if(k == XDG_DATA) {
                e = "/usr/local/share:/usr/share";
            } else if(k == XDG_CONFIG) {
                e = "/etc/xdg:/etc";
            }
        }
    }

    struct {
        char* path;
        size_t len;
        void* next;
    }* dirs;

    {
        dirs = alloca(sizeof(*dirs));
        const char* h = LIBR(xdg_dir)(xdg, k);
        dirs->len = strlen(h);
        dirs->path = alloca(dirs->len+1);
        memcpy(dirs->path, h, dirs->len+1);
        dirs->next = NULL;
    }
    typeof(*dirs)** tail = (void*)&dirs->next;

    size_t n = 1;
    if(e) {
        const size_t L = strlen(e);
        char buf[L+1];
        memcpy(buf, e, L+1);
        size_t a = 0, b = 0;
        do {
            for(; buf[b] != ':' && buf[b] != 0; b++);
            buf[b] = 0;
            char* p = &buf[a];
            if(xdg_check_path(p)) {
                char q[PATH_MAX];
                size_t l = LIBR(xdg_append_app)(xdg, LIT(q), p);
                if(l >= sizeof(q)) {
                    failwith("buffer overflow");
                }

                typeof(*dirs)* t = alloca(sizeof(*dirs));
                t->len = l;
                t->path = alloca(l+1);
                memcpy(t->path, q, l+1);
                t->next = NULL;

                *tail = t;
                tail = (void*)&t->next;
                n += 1;
            }
            b += 1;
            a = b;
        } while(b < L);
    }

    size_t N = 0;
    for(typeof(*dirs)* p = dirs; p != NULL; p = p->next) {
        N += sizeof(char*);
        N += p->len+1;
    }
    N += sizeof(char*); // add space for the NULL guard

    void* buf = malloc(N);
    CHECK_MALLOC(buf);
    char** index = buf;

    index[n] = NULL;
    char* strings = (char*)&index[n+1];

    size_t i = 0;
    for(typeof(*dirs)* p = dirs; p != NULL; i++, p = p->next) {
        index[i] = strings;
        memcpy(strings, p->path, p->len + 1);
        strings += p->len + 1;
    }

    xdg->dirs[k] = buf;
    return buf;
}

API const char** LIBR(xdg_data_dirs)(struct xdg* xdg)
{
    return LIBR(xdg_dirs)(xdg, XDG_DATA);
}

API const char** LIBR(xdg_config_dirs)(struct xdg* xdg)
{
    return LIBR(xdg_dirs)(xdg, XDG_CONFIG);
}

API const char* LIBR(xdg_resolvev)(struct xdg* xdg, enum xdg_kind k, char* buf, size_t L, va_list ps)
{
    const char** dirs = LIBR(xdg_dirs)(xdg, k);
    for(size_t i = 0; dirs[i]; i++) {
        va_list qs;
        va_copy(qs, ps);
        size_t l = LIBR(path_joinv)(buf, L, dirs[i], qs);
        if(l >= L) {
            failwith("buffer overflow");
        }
        va_end(qs);

        struct stat st;
        int r = stat(buf, &st);
        if(r == -1 && (errno == EACCES || errno == ENOENT || errno == ENOTDIR)) {
            continue;
        }
        CHECK(r, "stat(%s)", buf);

        return buf;
    }

    return NULL;
}

API const char* LIBR(xdg_resolve)(struct xdg* xdg, enum xdg_kind k, char* buf, size_t L, ...)
{
    va_list ps; va_start(ps, L);
    const char* p = LIBR(xdg_resolvev)(xdg, k, buf, L, ps);
    return va_end(ps), p;
}

API const char* LIBR(xdg_resolvevs)(struct xdg* xdg, enum xdg_kind k, va_list ps)
{
    static char buf[PATH_MAX];
    return LIBR(xdg_resolvev)(xdg, k, LIT(buf), ps);
}

API const char* LIBR(xdg_resolves)(struct xdg* xdg, enum xdg_kind k, ...)
{
    va_list ps; va_start(ps, k);
    const char* p = LIBR(xdg_resolvevs)(xdg, k, ps);
    return va_end(ps), p;
}

API void LIBR(xdg_makedirs)(const char* path)
{
    int r = LIBR(makedirs)(path, 0700);
    CHECK(r, "makedirs(%s, 0700)", path);
}

API const char* LIBR(xdg_preparev)(struct xdg* xdg, enum xdg_kind k, char* buf, size_t L, va_list ps)
{
    size_t l = LIBR(path_joinv)(buf, L, LIBR(xdg_dir)(xdg, k), ps);
    if(l >= L) {
        failwith("buffer overflow");
    }

    struct stat st;
    int r = stat(buf, &st);
    if(r == -1 && errno == ENOENT) {
        char d[l+1];
        memcpy(d, buf, l+1);
        LIBR(xdg_makedirs)(dirname(d));
        return buf;
    }
    CHECK(r, "stat(%s)", buf);

    return buf;
}

API const char* LIBR(xdg_prepare)(struct xdg* xdg, enum xdg_kind k, char* buf, size_t L, ...)
{
    va_list ps; va_start(ps, L);
    const char* p = LIBR(xdg_preparev)(xdg, k, buf, L, ps);
    return va_end(ps), p;
}

API const char* LIBR(xdg_preparevs)(struct xdg* xdg, enum xdg_kind k, va_list ps)
{
    static char buf[PATH_MAX];
    return LIBR(xdg_preparev)(xdg, k, LIT(buf), ps);
}

API const char* LIBR(xdg_prepares)(struct xdg* xdg, enum xdg_kind k, ...)
{
    va_list ps; va_start(ps, k);
    const char* p = LIBR(xdg_preparevs)(xdg, k, ps);
    return va_end(ps), p;
}

// libr: path.c

#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <limits.h>

API size_t LIBR(path_joinv)(char* buf, size_t L, const char* p0, va_list ps)
{
    size_t n = strlen(p0);
    if(n < L) {
        memmove(buf, p0, n);
    }

    for(;;) {
        const char* p = va_arg(ps, const char*);
        if(!p) break;

        if(n < L) {
            buf[n] = '/';
        }
        n += 1;

        size_t l = strlen(p);
        if(n + l < L) {
            memcpy(&buf[n], p, l);
        }
        n += l;
    }

    if(n < L) {
        buf[n] = 0;
    }

    return n;
}

API size_t LIBR(path_join)(char* buf, size_t L, const char* p0, ...)
{
    va_list ps; va_start(ps, p0);
    size_t n = LIBR(path_joinv)(buf, L, p0, ps);
    return va_end(ps), n;
}

#ifdef failwith
API const char* LIBR(path_joinvs)(const char* p0, va_list ps)
{
    static char buf[PATH_MAX];
    size_t l = LIBR(path_joinv)(buf, sizeof(buf), p0, ps);
    if(l >= sizeof(buf)) {
        failwith("buffer overflow");
    }

    return buf;
}

API const char* LIBR(path_joins)(const char* p0, ...)
{
    va_list ps; va_start(ps, p0);
    const char* p = LIBR(path_joinvs)(p0, ps);
    return va_end(ps), p;
}
#endif

API int LIBR(makedirs)(const char* path, mode_t mode)
{
    const size_t L = strlen(path);
    char buf[L+1];
    memcpy(buf, path, L+1);

    for(size_t l = (buf[0] == '/' ? 1 : 0); l <= L; l++) {
        if(buf[l] == '/') {
            buf[l] = 0;
        }

        if(buf[l] == 0) {
            int r = mkdir(buf, mode);
            if(r == -1) {
                if(errno != EEXIST) {
                    return -1;
                }
            }

            if(l < L) {
                buf[l] = '/';
            }
        }
    }

    struct stat st;
    int r = stat(buf, &st);
    if(r != 0) return -1;

    if(!(st.st_mode & S_IFDIR)) {
        errno = EEXIST;
        return -1;
    }

    return 0;
}
#endif // LIBR_IMPLEMENTATION

R .gitignore => py-impl/.gitignore +0 -0
R Makefile => py-impl/Makefile +0 -0
R Pipfile => py-impl/Pipfile +0 -0
R Pipfile.lock => py-impl/Pipfile.lock +0 -0
R pyproject.toml => py-impl/pyproject.toml +0 -0
R setup.cfg => py-impl/setup.cfg +0 -0
R src/action/__init__.py => py-impl/src/action/__init__.py +0 -0
R src/action/cli.py => py-impl/src/action/cli.py +0 -0
R src/action/util.py => py-impl/src/action/util.py +0 -0