#define _DEFAULT_SOURCE /* for timegm */ #include #include #include #include #include "reading.h" #include "i18n.h" typedef enum { FORMAT_TEXT, FORMAT_HTML, FORMAT_JSON } OutputFormat; static void print_usage(const char *prog) { fprintf(stderr, "Usage: %s --seed --birth-date YYYY-MM-DD --birth-time HH:MM\n" " --birth-utc-offset --birth-lat --birth-lon \n" " [--date YYYY-MM-DDTHH:MM] [--format text|html|json]\n" " [--gender male|female|unspecified]\n" " [--lang ] [--i18n-dir ]\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" " --gender Who the reading is for - only affects the Celtic Cross\n" " position text's pronouns, never the chart/draw itself.\n" " Defaults to unspecified (pronoun-neutral phrasing).\n" " --format Output format, defaults to text. html links card art via\n" " img/, i.e. it expects to be saved next to the img/\n" " directory that `make images` copies into dist/. json uses\n" " stable, language-independent identifiers (not --lang's\n" " display text) - it's the interpreter/ module's input format.\n" " --lang Reading language code, defaults to en. Matches a file\n" " named .lang in --i18n-dir (see engine/i18n/).\n" " --i18n-dir Directory to look up .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: /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; const char *gender_str = "unspecified"; 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, "--gender") == 0) rc = require_arg(argc, argv, &i, arg, &gender_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 if (strcmp(format_str, "json") == 0) format = FORMAT_JSON; else { fprintf(stderr, "Invalid --format, expected text, html, or json\n"); return 1; } Gender gender; if (strcmp(gender_str, "unspecified") == 0) gender = GENDER_UNSPECIFIED; else if (strcmp(gender_str, "male") == 0) gender = GENDER_MALE; else if (strcmp(gender_str, "female") == 0) gender = GENDER_FEMALE; else { fprintf(stderr, "Invalid --gender, expected male, female, or unspecified\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, gender, utc_moment, &reading); if (format == FORMAT_HTML) { reading_print_html(&reading, stdout); } else if (format == FORMAT_JSON) { reading_print_json(&reading, stdout); } else { reading_print_text(&reading, stdout); } return 0; }