commit 16bdee675c0b26b8b6cdb30499263370aeb2fb17 Author: Nikita Ivanov Date: Sun May 22 12:55:04 2022 +0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ed5abb --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.o +*.d +/gen/ +/ctpv +/embed/embed diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c70b5cb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Nikita Ivanov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 OR COPYRIGHT HOLDERS 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1c4c3e4 --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +PREFIX := /usr/local +BINPREFIX := $(PREFIX)/bin + +SRC := $(wildcard *.c) +OBJ := $(SRC:.c=.o) +DEP := $(OBJ:.o=.d) +PRE := $(wildcard prev/*.sh) +GEN := gen/prev/scripts.h gen/prev/helpers.h + +CFLAGS += -Os -MD -Wall -Wextra -Wno-unused-parameter +LDFLAGS += -lmagic + +all: ctpv + +options: + @echo "CC = $(CC)" + @echo "CFLAGS = $(CFLAGS)" + @echo "LDFLAGS = $(LDFLAGS)" + +install: ctpv + install -d $(BINPREFIX) + install $< $(BINPREFIX) + +uninstall: + $(RM) $(BINPREFIX)/ctpv + +clean: + $(RM) ctpv $(OBJ) $(DEP) $(GEN) + $(MAKE) -C embed clean + +make_embed: + $(MAKE) -C embed + +ctpv: $(OBJ) + +ctpv.c: $(GEN) + +gen/prev/scripts.h: embed/embed $(PRE) + @mkdir -p $(@D) + embed/embed -p prev_scr_ $(PRE) > $@ + +gen/prev/helpers.h: embed/embed helpers.sh + @mkdir -p $(@D) + embed/embed -p prev_ helpers.sh > $@ + +embed/embed: make_embed + @: + +-include $(DEP) + +.PHONY: all options install uninstall clean make_embed + +.DELETE_ON_ERROR: diff --git a/ctpv.c b/ctpv.c new file mode 100644 index 0000000..abf9f10 --- /dev/null +++ b/ctpv.c @@ -0,0 +1,117 @@ +#include +#include +#include +#include +#include +#include + +#include "error.h" +#include "utils.h" +#include "previews.h" + +static magic_t magic; + +static struct { + int server; +} ctpv; + +static void cleanup(void) { + cleanup_previews(); + if (magic != NULL) + magic_close(magic); +} + +static int init_magic() { + magic = magic_open(MAGIC_MIME_TYPE); + ERRCHK_RET(!magic, "magic_open() failed"); + + ERRCHK_RET(magic_load(magic, NULL) != 0, "magic_load() failed: %s", + magic_error(magic)); + + return OK; +} + +static const char *get_mimetype(char const *path) { + const char *r = magic_file(magic, path); + if (!r) { + print_errorf("magic_file() failed: %s", magic_error(magic)); + return NULL; + } + + return r; +} + +static const char *get_ext(char const *path) { + const char *r = strrchr(path, '.'); + if (!r) + return NULL; + + return &r[1]; +} + +static int server(void) +{ + /* TODO */ + return OK; +} + +#define GET_PARG(a, i) (a) = argc > (i) ? argv[i] : NULL + +static int client(int argc, char *argv[]) +{ + char *f, *w, *h, *x, *y; + GET_PARG(f, 0); + GET_PARG(w, 1); + GET_PARG(h, 2); + GET_PARG(x, 3); + GET_PARG(y, 4); + + ERRCHK_RET(!f, "file not given"); + ERRCHK_RET(access(f, R_OK) != 0, "failed to access '%s': %s", f, + strerror(errno)); + + ERRCHK_RET_OK(init_magic()); + + init_previews(previews, LEN(previews)); + + const char *mimetype = get_mimetype(f); + ERRCHK_RET(!mimetype); + + Preview *p = find_preview(get_ext(f), mimetype); + if (!p) { + puts("no preview found"); + return OK; + } + + PreviewArgs args = { .f = f, .w = w, .h = h, .x = x, .y = y }; + + ERRCHK_RET_OK(run_preview(p, &args)); + + return OK; +} + +int main(int argc, char *argv[]) +{ + program = argc > 0 ? argv[0] : "ctpv"; + + int c; + while ((c = getopt(argc, argv, "s")) != -1) { + switch (c) { + case 's': + ctpv.server = 1; + break; + default: + return EXIT_FAILURE; + } + } + + int ret; + if (ctpv.server) + ret = server(); + else + ret = client(argc, &argv[optind]); + + cleanup(); + + return ret == OK ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/embed/Makefile b/embed/Makefile new file mode 100644 index 0000000..0b9def8 --- /dev/null +++ b/embed/Makefile @@ -0,0 +1,6 @@ +embed: embed.c + +clean: + $(RM) embed + +.PHONY: clean diff --git a/embed/embed.c b/embed/embed.c new file mode 100644 index 0000000..e46d11f --- /dev/null +++ b/embed/embed.c @@ -0,0 +1,72 @@ +#include +#include +#include +#include +#include +#include + +void getvarname(char *res, char *prefix, char *filename) +{ + char *s = strrchr(filename, '/'); + if (s) + s++; + else + s = filename; + + if (prefix) { + size_t prefix_len = strlen(prefix); + memcpy(res, prefix, prefix_len); + res += prefix_len; + } + + for (int c, i = 0; s[i] != 0; i++) { + c = s[i]; + if (!isalnum(c)) + c = '_'; + + res[i] = c; + } +} + +void embed_file(char *prefix, char *filename) +{ + FILE *f = fopen(filename, "r"); + if (!f) { + fprintf(stderr, "failed to open %s: %s\n", filename, strerror(errno)); + exit(EXIT_FAILURE); + } + + static char varname[FILENAME_MAX]; + getvarname(varname, prefix, filename); + + printf("char %s[] = { ", varname); + + int c; + while ((c = fgetc(f)) != EOF) + printf("0x%x, ", c); + + puts("0 };"); + + fclose(f); +} + +int main(int argc, char *argv[]) +{ + char *prefix = NULL; + + int c; + while ((c = getopt(argc, argv, "p:")) != -1) { + switch (c) { + case 'p': + prefix = optarg; + break; + default: + return EXIT_FAILURE; + } + } + + for (int i = optind; i < argc; i++) + embed_file(prefix, argv[i]); + + return EXIT_SUCCESS; +} diff --git a/error.c b/error.c new file mode 100644 index 0000000..d25a0ae --- /dev/null +++ b/error.c @@ -0,0 +1,16 @@ +#include + +#include "error.h" +#include "utils.h" + +void print_error(char const *error_msg) +{ + fprintf(stderr, "%s: %s\n", program, error_msg); +} + +void print_errorf(char const *format, ...) +{ + char s[1024]; + FORMATTED_STRING(s, format); + print_error(s); +} diff --git a/error.h b/error.h new file mode 100644 index 0000000..e52b1b1 --- /dev/null +++ b/error.h @@ -0,0 +1,33 @@ +#ifndef ERROR_H +#define ERROR_H + +#define _ERRCHK_RET_PR(format, ...) \ + print_error##__VA_OPT__(f)(format __VA_OPT__(, ) __VA_ARGS__) + +/* + * If cond is true, return ERR. Also call print_error or + * print_errorf if error message or format string is given. + */ +#define ERRCHK_RET(cond, ...) \ + do { \ + if (cond) { \ + __VA_OPT__(_ERRCHK_RET_PR(__VA_ARGS__);) \ + return ERR; \ + } \ + } while (0) + +/* + * Shortcut for ERRCHK_RET(expr != OK) + */ +#define ERRCHK_RET_OK(expr, ...) \ + ERRCHK_RET((expr) != OK __VA_OPT__(, ) __VA_ARGS__) + +enum { + OK, + ERR, +}; + +void print_error(char const *error_msg); +void print_errorf(char const *format, ...); + +#endif diff --git a/helpers.sh b/helpers.sh new file mode 100644 index 0000000..ca80e25 --- /dev/null +++ b/helpers.sh @@ -0,0 +1,3 @@ +exists() { + command -v "$1" > /dev/null +} diff --git a/prev/file.sh b/prev/file.sh new file mode 100644 index 0000000..0639965 --- /dev/null +++ b/prev/file.sh @@ -0,0 +1 @@ +echo "$f" diff --git a/preview.c b/preview.c new file mode 100644 index 0000000..b3fed8f --- /dev/null +++ b/preview.c @@ -0,0 +1,176 @@ +#include +#include + +#include "utils.h" +#include "error.h" +#include "preview.h" + +#define PREVP_SIZE sizeof(Preview *) + +static char shell[] = "sh"; + +static Preview **sorted_by_ext, + **sorted_by_mimetype; +static size_t prevs_length; + +static int cmp_prev_ext(const void *p1, const void *p2) +{ + Preview *pr1 = *(Preview **)p1; + Preview *pr2 = *(Preview **)p2; + + if (!pr1->ext) + return 1; + + if (!pr2->ext) + return -1; + + return strcmp(pr1->ext, pr2->ext); +} + +static int cmp_prev_mimetype(const void *p1, const void *p2) +{ + Preview *pr1 = *(Preview **)p1; + Preview *pr2 = *(Preview **)p2; + + if (pr1->type[0] == '\0') + return 1; + else if (pr2->type[0] == '\0') + return -1; + + if (!pr1->subtype && pr2->subtype) { + return 1; + } else if (pr1->subtype && !pr2->subtype) { + return -1; + } else { + int ret = strcmp(pr1->type, pr2->type); + if (ret == 0 && pr1->subtype && pr2->subtype) + return strcmp(pr1->subtype, pr2->subtype); + return ret; + } +} + +void init_previews(Preview *ps, size_t len) +{ + prevs_length = len; + size_t size = len * PREVP_SIZE; + + sorted_by_ext = malloc(size); + sorted_by_mimetype = malloc(size); + if (!sorted_by_ext || !sorted_by_mimetype) { + print_error("malloc() failed"); + abort(); + } + + for (size_t i = 0; i < len; i++) + sorted_by_ext[i] = sorted_by_mimetype[i] = &ps[i]; + + qsort(sorted_by_ext, len, PREVP_SIZE, cmp_prev_ext); + qsort(sorted_by_mimetype, len, PREVP_SIZE, cmp_prev_mimetype); +} + +void cleanup_previews(void) +{ + if (sorted_by_ext) { + free(sorted_by_ext); + sorted_by_ext = NULL; + } + + if (sorted_by_mimetype) { + free(sorted_by_mimetype); + sorted_by_mimetype = NULL; + } + + prevs_length = 0; +} + +static Preview *find_by_ext(char const *ext) +{ + Preview *key = &(Preview){ .ext = (char *)ext }; + + void *p = + bsearch(&key, sorted_by_ext, prevs_length, PREVP_SIZE, cmp_prev_ext); + + return p ? *(Preview **)p : NULL; +} + +static void break_mimetype(char *mimetype, char **type, char **subtype) +{ + *type = mimetype[0] == '\0' ? NULL : mimetype; + *subtype = NULL; + + char *s = strchr(mimetype, '/'); + if (!s) { + print_errorf("invalid mimetype: '%s'", mimetype); + abort(); + } + + *s = '\0'; + *subtype = &s[1]; +} + +#define MIMETYPE_MAX 64 + +static Preview *find_by_mimetype(char const *mimetype) +{ + Preview *p; + char mimetype_c[MIMETYPE_MAX], *t, *s; + + strncpy(mimetype_c, mimetype, MIMETYPE_MAX - 1); + break_mimetype(mimetype_c, &t, &s); + + for (size_t i = 0; i < prevs_length; i++) { + p = sorted_by_mimetype[i]; + + if (strcmp(t, p->type) == 0) { + if (p->subtype) { + if (strcmp(s, p->subtype) == 0) + return p; + } else { + return p; + } + } + } + + return NULL; +} + +Preview *find_preview(char const *ext, char const *mimetype) +{ + if (!sorted_by_ext || !sorted_by_mimetype) { + print_error("init_previews() not called"); + abort(); + } + + Preview *ret = NULL; + if (mimetype) + ret = find_by_mimetype(mimetype); + if (!ret) + ret = find_by_ext(ext); + + return ret; +} + +#define SET_PENV(n, v) \ + do { \ + if (v) \ + ERRCHK_RET(setenv((n), (v), 1) != 0); \ + } while (0) + +int run_preview(Preview *p, PreviewArgs *pa) +{ + SET_PENV("f", pa->f); + SET_PENV("w", pa->w); + SET_PENV("h", pa->h); + SET_PENV("x", pa->x); + SET_PENV("y", pa->y); + + char *args[] = { shell, "-c", p->script, shell, NULL }; + int ret, exitcode; + + ret = spawn(args, NULL, &exitcode); + + if (exitcode > 0) + printf("error: preview exited with code: %d\n", exitcode); + + return ret; +} diff --git a/preview.h b/preview.h new file mode 100644 index 0000000..d183208 --- /dev/null +++ b/preview.h @@ -0,0 +1,19 @@ +#ifndef PREVIEW_H +#define PREVIEW_H + +#include + +typedef struct { + char *ext, *type, *subtype, *script; +} Preview; + +typedef struct { + char *f, *w, *h, *x, *y; +} PreviewArgs; + +void init_previews(Preview *ps, size_t len); +void cleanup_previews(void); +Preview *find_preview(char const *ext, char const *mimetype); +int run_preview(Preview *p, PreviewArgs *pa); + +#endif diff --git a/previews.h b/previews.h new file mode 100644 index 0000000..ad49096 --- /dev/null +++ b/previews.h @@ -0,0 +1,16 @@ +#include + +#include "gen/prev/scripts.h" +#include "preview.h" + +/* + * This file is supposed to be included in ctpv.c + */ + +#define P(e, t, s, f) { e, t, s, f } + +Preview previews[] = { + P(NULL, "text", "plain", prev_scr_file_sh), +}; + +#undef P diff --git a/utils.c b/utils.c new file mode 100644 index 0000000..b6f00a2 --- /dev/null +++ b/utils.c @@ -0,0 +1,90 @@ +#include +#include +#include +#include +#include + +#include "error.h" +#include "utils.h" + +char *program; + +/* + * Call command + * + * If cpid is NULL, wait for the command to finish executing; + * otherwise store pid in cpid + */ +int spawn(char *args[], pid_t *cpid, int *exitcode) +{ + if (exitcode) + *exitcode = -1; + + pid_t pid = fork(); + ERRCHK_RET(pid == -1, "fork() failed"); + + /* Child process */ + if (pid == 0) { + execvp(args[0], args); + print_errorf("exec() failed: %s", strerror(errno)); + exit(EXIT_FAILURE); + } + + if (cpid) { + *cpid = pid; + } else { + int stat; + ERRCHK_RET(waitpid(pid, &stat, 0) == -1, "waitpid() failed: %s", + strerror(errno)); + + if (exitcode && WIFEXITED(stat)) + *exitcode = WEXITSTATUS(stat); + } + + return OK; +} + +CharVec char_v_new(size_t cap) +{ + CharVec v; + v.buf = NULL; + v.len = 0; + v.cap = cap; + + return v; +} + +void char_v_free(CharVec *v) +{ + if (v->buf) { + free(v->buf); + v->buf = NULL; + v->len = 0; + } +} + +void char_v_append(CharVec *v, char c) +{ + if (!v->buf) { + v->buf = malloc(v->cap * sizeof(v->buf[0])); + if (!v->buf) { + print_error("calloc() failed"); + abort(); + } + v->buf[0] = '\0'; + v->len++; + } + + if (v->len + 1 >= v->cap) { + v->cap *= 2; + v->buf = realloc(v->buf, v->cap * sizeof(v->buf[0])); + if (!v->buf) { + print_error("realloc() failed"); + abort(); + } + } + + v->buf[v->len - 1] = c; + v->buf[v->len] = '\0'; + v->len++; +} diff --git a/utils.h b/utils.h new file mode 100644 index 0000000..244d608 --- /dev/null +++ b/utils.h @@ -0,0 +1,32 @@ +#ifndef UTILS_H +#define UTILS_H + +#include +#include + +#define LEN(a) (sizeof(a) / sizeof((a)[0])) +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#define MIN(a, b) ((a) < (b) ? (a) : (b)) + +#define FORMATTED_STRING(arr, format) \ + do { \ + va_list args; \ + va_start(args, (format)); \ + vsnprintf((arr), LEN(arr) - 1, (format), args); \ + va_end(args); \ + } while (0) + +typedef struct { + char *buf; + size_t len, cap; +} CharVec; + +extern char *program; + +int spawn(char *args[], pid_t *cpid, int *exitcode); + +CharVec char_v_new(size_t cap); +void char_v_append(CharVec *v, char c); +void char_v_free(CharVec *v); + +#endif