diff options
| author | hachem <im@hachem.wtf> | 2026-09-18 17:37:08 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-09-18 17:37:08 +0200 |
| commit | 5ecd3773b5a6f40b2c83687180d6f674959f46a3 (patch) | |
| tree | 23599639ba61aa5aa0af4c5440b0f3aab9284ef3 | |
| parent | 2720a126aa5a8412f8d355f8937cf6c37975be78 (diff) | |
feat: add little binary tool
| -rw-r--r-- | mktypst.c | 432 |
1 files changed, 432 insertions, 0 deletions
diff --git a/mktypst.c b/mktypst.c new file mode 100644 index 0000000..077f37b --- /dev/null +++ b/mktypst.c @@ -0,0 +1,432 @@ +/* + * mktypst - dumb typst document generator. + * + * generates a typst document that imports and applies an mktypst template, + * so you can start writing instead of pasting the same preamble every time. + * + * Author: hachem <contact@hachem.cc> + * Version: 0.1.0 + */ + +#include <locale.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#define mktypst_program "mktypst" +#define mktypst_version "0.1.0" +#define mktypst_tagline "dumb typst document generator" +#define mktypst_package "@local/hachem:0.1.0" + +enum Template +{ + template_paper, + template_letter, + template_count +}; + +struct Template_Info +{ + const char* key; + const char* name; +}; + +struct Config +{ + const char* name; + enum Template tmpl; + const char* title; + const char* author; + const char* date; + int use_stdout; + int force; +}; + +static const struct Template_Info templates[template_count] = +{ + [template_paper] = { .key = "paper", .name = "paper" }, + [template_letter] = { .key = "letter", .name = "letter" }, +}; + +static void print_version(void) +{ + (void) printf("%s %s\n", mktypst_program, mktypst_version); +} + +static void print_usage(FILE* stream) +{ + (void) fprintf( + stream, + "%s %s - %s\n" + "\n" + "generates a typst document that imports and applies an mktypst\n" + "template, so you can start writing instead of pasting a preamble.\n" + "\n" + "usage: %s [-t template] [-T title] [-a author] [-d date] [-f] <name>\n" + " %s [-v | -h]\n" + "\n" + "options:\n" + " -t, --template NAME template to apply (default paper)\n" + " -T, --title TITLE document title (default: the name)\n" + " -a, --author NAME author name (default: git config user.name)\n" + " -d, --date DATE document date (default: current month)\n" + " -f, --force overwrite an existing file\n" + " --stdout write to stdout instead of a .typ file\n" + " -v, --version print version and bail\n" + " -h, --help print this and bail\n" + "\n" + "by hachem <contact@hachem.cc>\n", + mktypst_program, mktypst_version, mktypst_tagline, + mktypst_program, mktypst_program); +} + +static void error(const char* message, const char* detail) +{ + if (detail != NULL) + (void) fprintf(stderr, "%s: error: %s: %s\n", mktypst_program, message, detail); + else + (void) fprintf(stderr, "%s: error: %s\n", mktypst_program, message); +} + +/* match a value-taking option. handles both "--opt val" and "--opt=val". + * returns 1 and sets *value on a match, -1 if the value is missing, else 0. */ +static int opt_value(const char* arg, const char* shortname, const char* longname, + int argc, char** argv, int* i, const char** value) +{ + if (strcmp(arg, shortname) == 0 || strcmp(arg, longname) == 0) + { + if (*i + 1 >= argc) + return -1; + + *value = argv[++(*i)]; + return 1; + } + + size_t longlen = strlen(longname); + + if (strncmp(arg, longname, longlen) == 0 && arg[longlen] == '=') + { + *value = arg + longlen + 1; + return 1; + } + + return 0; +} + +static int find_template(const char* key, enum Template* out) +{ + for (enum Template tmpl = 0; tmpl < template_count; tmpl++) + if (strcmp(templates[tmpl].key, key) == 0) + { + *out = tmpl; + return 1; + } + + return 0; +} + +/* the project name with a trailing ".typ" trimmed, used as the default title. */ +static const char* stem_of(const char* name) +{ + static char buf[256]; + + (void) snprintf(buf, sizeof buf, "%s", name); + + size_t len = strlen(buf); + + if (len >= 4 && strcmp(buf + len - 4, ".typ") == 0) + buf[len - 4] = '\0'; + + return buf; +} + +/* best-effort author: git's configured name, else $USER. */ +static const char* author_name(void) +{ + static char buf[256]; + + buf[0] = '\0'; + + FILE* pipe = popen("git config user.name", "r"); + + if (pipe != NULL) + { + if (fgets(buf, (int) sizeof buf, pipe) != NULL) + { + size_t len = strlen(buf); + + if (len > 0 && buf[len - 1] == '\n') + buf[len - 1] = '\0'; + } + + (void) pclose(pipe); + } + + if (buf[0] == '\0') + { + const char* user = getenv("USER"); + + (void) snprintf(buf, sizeof buf, "%s", user != NULL ? user : "Author"); + } + + return buf; +} + +static const char* current_date(void) +{ + static char buf[64]; + + time_t now = time(NULL); + struct tm* local = localtime(&now); + + if (local == NULL || strftime(buf, sizeof buf, "%B %Y", local) == 0) + (void) snprintf(buf, sizeof buf, "undated"); + + return buf; +} + +static void write_paper(FILE* out, const struct Config* config) +{ + (void) fprintf( + out, + "#import \"%s\": paper\n" + "\n" + "#show: paper.with(\n" + " title: \"%s\",\n" + " date: \"%s\",\n" + " authors: (\n" + " (name: \"%s\", location: []),\n" + " ),\n" + ")\n" + "\n" + "= Introduction\n", + mktypst_package, config->title, config->date, config->author); +} + +static void write_letter(FILE* out, const struct Config* config) +{ + (void) fprintf( + out, + "#import \"%s\": letter\n" + "\n" + "#show: letter.with(\n" + " sender: (\n" + " name: [%s],\n" + " address: [Votre adresse \\ Code postal, ville],\n" + " ),\n" + " recipient: (\n" + " name: [Destinataire],\n" + " address: [Son adresse \\ Code postal, ville],\n" + " ),\n" + " place: \"Paris\",\n" + " date: \"%s\",\n" + " subject: [%s],\n" + ")\n" + "\n" + "Corps de la lettre.\n", + mktypst_package, config->author, config->date, config->title); +} + +static void write_document(FILE* out, const struct Config* config) +{ + switch (config->tmpl) + { + case template_paper: + write_paper(out, config); + break; + + case template_letter: + write_letter(out, config); + break; + + default: + break; + } +} + +static void output_path(const char* name, char* buf, size_t size) +{ + size_t len = strlen(name); + + if (len >= 4 && strcmp(name + len - 4, ".typ") == 0) + (void) snprintf(buf, size, "%s", name); + else + (void) snprintf(buf, size, "%s.typ", name); +} + +static int generate(const struct Config* config) +{ + if (config->use_stdout) + { + write_document(stdout, config); + return 0; + } + + char path[1024]; + output_path(config->name, path, sizeof path); + + if (!config->force && access(path, F_OK) == 0) + { + error("file already exists (use -f to overwrite)", path); + return -1; + } + + FILE* file = fopen(path, "w"); + + if (file == NULL) + { + error("cannot create file", path); + return -1; + } + + write_document(file, config); + + if (fclose(file) != 0) + { + error("cannot write file", path); + return -1; + } + + (void) printf("created %s (%s)\n", path, templates[config->tmpl].name); + (void) printf(" typst watch %s\n", path); + + return 0; +} + +static int parse_args(int argc, char** argv, struct Config* config) +{ + const char* template_arg = NULL; + const char* name = NULL; + + for (int i = 1; i < argc; i++) + { + const char* arg = argv[i]; + + if (strcmp(arg, "-h") == 0 || strcmp(arg, "--help") == 0) + { + print_usage(stdout); + return -1; + } + + if (strcmp(arg, "-v") == 0 || strcmp(arg, "--version") == 0) + { + print_version(); + return -1; + } + + if (strcmp(arg, "--stdout") == 0) + { + config->use_stdout = 1; + continue; + } + + if (strcmp(arg, "-f") == 0 || strcmp(arg, "--force") == 0) + { + config->force = 1; + continue; + } + + int matched = 0; + + matched = opt_value(arg, "-t", "--template", argc, argv, &i, &template_arg); + if (matched != 0) + { + if (matched < 0) { error("missing argument for", arg); return 1; } + continue; + } + + matched = opt_value(arg, "-T", "--title", argc, argv, &i, &config->title); + if (matched != 0) + { + if (matched < 0) { error("missing argument for", arg); return 1; } + continue; + } + + matched = opt_value(arg, "-a", "--author", argc, argv, &i, &config->author); + if (matched != 0) + { + if (matched < 0) { error("missing argument for", arg); return 1; } + continue; + } + + matched = opt_value(arg, "-d", "--date", argc, argv, &i, &config->date); + if (matched != 0) + { + if (matched < 0) { error("missing argument for", arg); return 1; } + continue; + } + + if (arg[0] == '-' && arg[1] != '\0') + { + error("unknown option", arg); + return 1; + } + + if (name != NULL) + { + error("unexpected extra argument", arg); + return 1; + } + + name = arg; + } + + if (name == NULL) + { + error("missing document name", NULL); + return 1; + } + + config->name = name; + + if (template_arg != NULL && !find_template(template_arg, &config->tmpl)) + { + error("unknown template", template_arg); + return 1; + } + + if (config->title == NULL) + config->title = stem_of(name); + + if (config->author == NULL) + config->author = author_name(); + + if (config->date == NULL) + config->date = current_date(); + + return 0; +} + +int main(int argc, char** argv) +{ + (void) setlocale(LC_TIME, ""); + + struct Config config = + { + .name = NULL, + .tmpl = template_paper, + .title = NULL, + .author = NULL, + .date = NULL, + .use_stdout = 0, + .force = 0, + }; + + int status = parse_args(argc, argv, &config); + + if (status < 0) + return EXIT_SUCCESS; + + if (status > 0) + { + print_usage(stderr); + return EXIT_FAILURE; + } + + if (generate(&config) != 0) + return EXIT_FAILURE; + + return EXIT_SUCCESS; +} |
