Add core engine: tarot + astrology reading generator with CLI and i18n
Plain-C, dependency-free engine that produces a personalized daily Celtic Cross tarot spread and natal-chart/transit astrology reading, plus a standalone CLI (dist/deck-engine) and run.sh wrapper. Includes English/German output via a simple key=value translation format, and docs/diagrams describing the architecture and I/O.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
#define _DEFAULT_SOURCE /* for timegm */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "reading.h"
|
||||
#include "i18n.h"
|
||||
|
||||
typedef enum { FORMAT_TEXT, FORMAT_HTML } OutputFormat;
|
||||
|
||||
static void print_usage(const char *prog) {
|
||||
fprintf(stderr,
|
||||
"Usage: %s --seed <string> --birth-date YYYY-MM-DD --birth-time HH:MM\n"
|
||||
" --birth-utc-offset <hours> --birth-lat <deg> --birth-lon <deg>\n"
|
||||
" [--date YYYY-MM-DDTHH:MM] [--format text|html]\n"
|
||||
" [--lang <code>] [--i18n-dir <path>]\n\n"
|
||||
" --seed Tarot seed string; same seed -> same Celtic Cross spread.\n"
|
||||
" --birth-date/time Birth date/time in local clock time.\n"
|
||||
" --birth-utc-offset Hours to subtract from local birth time to get UTC (e.g. 2 for CEST).\n"
|
||||
" --birth-lat/lon Birth location in decimal degrees (north/east positive).\n"
|
||||
" --date UTC moment for today's transits/tarot draw. Defaults to now.\n"
|
||||
" --format Output format, defaults to text. html links card art via\n"
|
||||
" img/<file>, i.e. it expects to be saved next to the img/\n"
|
||||
" directory that `make images` copies into dist/.\n"
|
||||
" --lang Reading language code, defaults to en. Matches a file\n"
|
||||
" named <code>.lang in --i18n-dir (see engine/i18n/).\n"
|
||||
" --i18n-dir Directory to look up <lang>.lang in. Defaults to the\n"
|
||||
" i18n/ directory next to this binary (`make i18n`\n"
|
||||
" copies engine/i18n/ there as dist/i18n/).\n",
|
||||
prog);
|
||||
}
|
||||
|
||||
/* Default --i18n-dir: <directory this binary was invoked from>/i18n, so
|
||||
* `dist/deck-engine ...` finds dist/i18n/ regardless of the caller's cwd.
|
||||
* If argv[0] has no '/' (looked up via $PATH), falls back to a relative
|
||||
* "i18n" - callers in that situation should pass --i18n-dir explicitly. */
|
||||
static void default_i18n_path(const char *argv0, const char *lang, char *out, size_t out_size) {
|
||||
const char *slash = strrchr(argv0, '/');
|
||||
if (slash) {
|
||||
int dir_len = (int)(slash - argv0);
|
||||
snprintf(out, out_size, "%.*s/i18n/%s.lang", dir_len, argv0, lang);
|
||||
} else {
|
||||
snprintf(out, out_size, "i18n/%s.lang", lang);
|
||||
}
|
||||
}
|
||||
|
||||
static int require_arg(int argc, char **argv, int *i, const char *name, const char **out) {
|
||||
if (*i + 1 >= argc) {
|
||||
fprintf(stderr, "Missing value for %s\n", name);
|
||||
return -1;
|
||||
}
|
||||
*out = argv[++*i];
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *seed = NULL;
|
||||
const char *birth_date = NULL, *birth_time = NULL;
|
||||
const char *birth_utc_offset = NULL, *birth_lat = NULL, *birth_lon = NULL;
|
||||
const char *date_str = NULL, *format_str = "text";
|
||||
const char *lang = "en", *i18n_dir_arg = NULL;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
const char *arg = argv[i];
|
||||
int rc = 0;
|
||||
if (strcmp(arg, "--seed") == 0) rc = require_arg(argc, argv, &i, arg, &seed);
|
||||
else if (strcmp(arg, "--birth-date") == 0) rc = require_arg(argc, argv, &i, arg, &birth_date);
|
||||
else if (strcmp(arg, "--birth-time") == 0) rc = require_arg(argc, argv, &i, arg, &birth_time);
|
||||
else if (strcmp(arg, "--birth-utc-offset") == 0) rc = require_arg(argc, argv, &i, arg, &birth_utc_offset);
|
||||
else if (strcmp(arg, "--birth-lat") == 0) rc = require_arg(argc, argv, &i, arg, &birth_lat);
|
||||
else if (strcmp(arg, "--birth-lon") == 0) rc = require_arg(argc, argv, &i, arg, &birth_lon);
|
||||
else if (strcmp(arg, "--date") == 0) rc = require_arg(argc, argv, &i, arg, &date_str);
|
||||
else if (strcmp(arg, "--format") == 0) rc = require_arg(argc, argv, &i, arg, &format_str);
|
||||
else if (strcmp(arg, "--lang") == 0) rc = require_arg(argc, argv, &i, arg, &lang);
|
||||
else if (strcmp(arg, "--i18n-dir") == 0) rc = require_arg(argc, argv, &i, arg, &i18n_dir_arg);
|
||||
else if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") == 0) {
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
} else {
|
||||
fprintf(stderr, "Unknown argument: %s\n", arg);
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
if (rc != 0) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!seed || !birth_date || !birth_time || !birth_utc_offset || !birth_lat || !birth_lon) {
|
||||
fprintf(stderr, "Missing required argument(s).\n\n");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
BirthData birth = {0};
|
||||
if (sscanf(birth_date, "%d-%d-%d", &birth.year, &birth.month, &birth.day) != 3) {
|
||||
fprintf(stderr, "Invalid --birth-date, expected YYYY-MM-DD\n");
|
||||
return 1;
|
||||
}
|
||||
if (sscanf(birth_time, "%d:%d", &birth.hour, &birth.minute) != 2) {
|
||||
fprintf(stderr, "Invalid --birth-time, expected HH:MM\n");
|
||||
return 1;
|
||||
}
|
||||
birth.utc_offset_hours = atof(birth_utc_offset);
|
||||
birth.latitude = atof(birth_lat);
|
||||
birth.longitude = atof(birth_lon);
|
||||
|
||||
time_t utc_moment;
|
||||
if (date_str) {
|
||||
struct tm tm_date = {0};
|
||||
int second = 0;
|
||||
if (sscanf(date_str, "%d-%d-%dT%d:%d:%d", &tm_date.tm_year, &tm_date.tm_mon,
|
||||
&tm_date.tm_mday, &tm_date.tm_hour, &tm_date.tm_min, &second) < 5) {
|
||||
fprintf(stderr, "Invalid --date, expected YYYY-MM-DDTHH:MM[:SS]\n");
|
||||
return 1;
|
||||
}
|
||||
tm_date.tm_year -= 1900;
|
||||
tm_date.tm_mon -= 1;
|
||||
tm_date.tm_sec = second;
|
||||
utc_moment = timegm(&tm_date);
|
||||
} else {
|
||||
utc_moment = time(NULL);
|
||||
}
|
||||
|
||||
OutputFormat format;
|
||||
if (strcmp(format_str, "text") == 0) format = FORMAT_TEXT;
|
||||
else if (strcmp(format_str, "html") == 0) format = FORMAT_HTML;
|
||||
else {
|
||||
fprintf(stderr, "Invalid --format, expected text or html\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
char i18n_path[512];
|
||||
if (i18n_dir_arg) {
|
||||
snprintf(i18n_path, sizeof i18n_path, "%s/%s.lang", i18n_dir_arg, lang);
|
||||
} else {
|
||||
default_i18n_path(argv[0], lang, i18n_path, sizeof i18n_path);
|
||||
}
|
||||
if (!i18n_load(i18n_path) && strcmp(lang, "en") != 0) {
|
||||
fprintf(stderr, "warning: could not load translations for '%s' from %s - "
|
||||
"falling back to built-in English\n", lang, i18n_path);
|
||||
}
|
||||
|
||||
DailyReading reading;
|
||||
reading_generate(seed, &birth, utc_moment, &reading);
|
||||
|
||||
if (format == FORMAT_HTML) {
|
||||
reading_print_html(&reading, stdout);
|
||||
} else {
|
||||
reading_print_text(&reading, stdout);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user