/* * 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 * Version: 0.1.0 */ #include #include #include #include #include #include #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] \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 \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; }