hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-18 17:37:08 +0200
committerhachem <im@hachem.wtf>2026-09-18 17:37:08 +0200
commit5ecd3773b5a6f40b2c83687180d6f674959f46a3 (patch)
tree23599639ba61aa5aa0af4c5440b0f3aab9284ef3
parent2720a126aa5a8412f8d355f8937cf6c37975be78 (diff)
feat: add little binary tool
-rw-r--r--mktypst.c432
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;
+}