+16
-1
@@ -15,12 +15,16 @@ static void print_usage(const char *prog) {
|
||||
"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|json]\n"
|
||||
" [--gender male|female|unspecified]\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"
|
||||
" --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/<file>, i.e. it expects to be saved next to the img/\n"
|
||||
" directory that `make images` copies into dist/. json uses\n"
|
||||
@@ -63,6 +67,7 @@ int main(int argc, char **argv) {
|
||||
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];
|
||||
@@ -75,6 +80,7 @@ int main(int argc, char **argv) {
|
||||
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) {
|
||||
@@ -136,6 +142,15 @@ int main(int argc, char **argv) {
|
||||
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);
|
||||
@@ -148,7 +163,7 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
|
||||
DailyReading reading;
|
||||
reading_generate(seed, &birth, utc_moment, &reading);
|
||||
reading_generate(seed, &birth, gender, utc_moment, &reading);
|
||||
|
||||
if (format == FORMAT_HTML) {
|
||||
reading_print_html(&reading, stdout);
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
#include <math.h>
|
||||
|
||||
void reading_generate(const char *tarot_seed, const BirthData *birth,
|
||||
void reading_generate(const char *tarot_seed, const BirthData *birth, Gender gender,
|
||||
time_t utc_moment, DailyReading *out) {
|
||||
out->utc_moment = utc_moment;
|
||||
out->gender = gender;
|
||||
astro_compute_natal_chart(birth, &out->natal);
|
||||
astro_compute_daily_transits(utc_moment, &out->natal, &out->transits);
|
||||
tarot_draw_celtic_cross(tarot_seed, &out->spread);
|
||||
@@ -66,10 +67,10 @@ void reading_print_text(const DailyReading *r, FILE *out) {
|
||||
fprintf(out, "===== %s =====\n", ui("ui.celtic_cross", "Celtic Cross"));
|
||||
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||
const TarotDraw *draw = &r->spread.positions[i];
|
||||
fprintf(out, "%-16s %s%s%s\n", tarot_position_name((CelticCrossPosition)i),
|
||||
fprintf(out, "%-16s %s%s%s\n", tarot_position_name((CelticCrossPosition)i, r->gender),
|
||||
tarot_card_name(draw->card), draw->reversed ? " " : "",
|
||||
draw->reversed ? ui("ui.reversed", "(Reversed)") : "");
|
||||
fprintf(out, " %s\n", tarot_position_description((CelticCrossPosition)i));
|
||||
fprintf(out, " %s\n", tarot_position_description((CelticCrossPosition)i, r->gender));
|
||||
fprintf(out, " %s\n", tarot_card_meaning(draw->card, draw->reversed));
|
||||
}
|
||||
}
|
||||
@@ -138,12 +139,12 @@ void reading_print_html(const DailyReading *r, FILE *out) {
|
||||
" <div class=\"desc\">%s</div>\n"
|
||||
" <div class=\"meaning\">%s</div>\n"
|
||||
"</div>\n",
|
||||
tarot_position_name((CelticCrossPosition)i),
|
||||
tarot_position_name((CelticCrossPosition)i, r->gender),
|
||||
draw->reversed ? "reversed" : "",
|
||||
tarot_card_image_file(draw->card), tarot_card_name(draw->card),
|
||||
tarot_card_name(draw->card), draw->reversed ? " " : "",
|
||||
draw->reversed ? ui("ui.reversed", "(Reversed)") : "",
|
||||
tarot_position_description((CelticCrossPosition)i),
|
||||
tarot_position_description((CelticCrossPosition)i, r->gender),
|
||||
tarot_card_meaning(draw->card, draw->reversed));
|
||||
}
|
||||
fprintf(out, "</div>\n</body></html>\n");
|
||||
@@ -163,6 +164,7 @@ void reading_print_json(const DailyReading *r, FILE *out) {
|
||||
|
||||
struct tm *utc = gmtime(&r->utc_moment);
|
||||
fprintf(out, " \"date\": \"%04d-%02d-%02d\",\n", utc->tm_year + 1900, utc->tm_mon + 1, utc->tm_mday);
|
||||
fprintf(out, " \"gender\": \"%s\",\n", tarot_gender_slug(r->gender));
|
||||
|
||||
fprintf(out, " \"natal\": {\n \"bodies\": [\n");
|
||||
for (int b = 0; b < NUM_BODIES; b++) {
|
||||
|
||||
@@ -21,6 +21,10 @@ typedef struct {
|
||||
* interpreter side) gets it for free without threading it through
|
||||
* separately. */
|
||||
time_t utc_moment;
|
||||
/* Who the reading is for - only affects the pronouns in the Celtic
|
||||
* Cross position text (see tarot.h's Gender), set by reading_generate()
|
||||
* the same way utc_moment is, so every caller gets it for free. */
|
||||
Gender gender;
|
||||
NatalChart natal;
|
||||
DailyTransits transits;
|
||||
CelticCrossSpread spread;
|
||||
@@ -28,7 +32,7 @@ typedef struct {
|
||||
|
||||
/* The real API: this is the only function the watchapp needs to call.
|
||||
* It walks the returned struct directly to lay out its own screens. */
|
||||
void reading_generate(const char *tarot_seed, const BirthData *birth,
|
||||
void reading_generate(const char *tarot_seed, const BirthData *birth, Gender gender,
|
||||
time_t utc_moment, DailyReading *out);
|
||||
|
||||
/* Desktop-only output formats for inspecting a reading before any watch
|
||||
|
||||
+14
-2
@@ -52,6 +52,17 @@ typedef struct {
|
||||
bool reversed;
|
||||
} TarotDraw;
|
||||
|
||||
/* Who the reading is for - affects only the pronouns in the Celtic
|
||||
* Cross position text below (Waite's original wording addresses the
|
||||
* querent as "him"), never anything about the tarot draw or astrology
|
||||
* math itself. GENDER_UNSPECIFIED is the default and renders
|
||||
* pronoun-neutral phrasing (singular "they" in English). */
|
||||
typedef enum {
|
||||
GENDER_UNSPECIFIED = 0,
|
||||
GENDER_MALE,
|
||||
GENDER_FEMALE
|
||||
} Gender;
|
||||
|
||||
typedef struct {
|
||||
/* Indexed by CelticCrossPosition. */
|
||||
TarotDraw positions[TAROT_SPREAD_SIZE];
|
||||
@@ -67,8 +78,8 @@ const char *tarot_card_name(TarotCard card);
|
||||
const char *tarot_card_image_file(TarotCard card); /* matches res/img/ */
|
||||
const char *tarot_card_meaning(TarotCard card, bool reversed);
|
||||
|
||||
const char *tarot_position_name(CelticCrossPosition position);
|
||||
const char *tarot_position_description(CelticCrossPosition position);
|
||||
const char *tarot_position_name(CelticCrossPosition position, Gender gender);
|
||||
const char *tarot_position_description(CelticCrossPosition position, Gender gender);
|
||||
|
||||
/* Stable, language-independent identifiers (e.g. "high_priestess",
|
||||
* "wheel_of_fortune", "recent_past") - the same strings i18n.c's lookup
|
||||
@@ -76,5 +87,6 @@ const char *tarot_position_description(CelticCrossPosition position);
|
||||
* (reading_print_json) that must stay identical regardless of --lang. */
|
||||
const char *tarot_card_slug(TarotCard card);
|
||||
const char *tarot_position_slug(CelticCrossPosition position);
|
||||
const char *tarot_gender_slug(Gender gender); /* "unspecified"/"male"/"female" */
|
||||
|
||||
#endif
|
||||
|
||||
+112
-34
@@ -130,52 +130,97 @@ static const TarotCardInfo k_cards[TAROT_DECK_SIZE] = {
|
||||
|
||||
/* Position names and one-line descriptions follow Waite's own account
|
||||
* of "An Ancient Celtic Method of Divination" (Part III §7), in his
|
||||
* original drawing order. */
|
||||
* original drawing order. Waite addresses the querent as "him"
|
||||
* throughout; that wording is kept verbatim as the GENDER_MALE variant
|
||||
* below, and GENDER_FEMALE/GENDER_UNSPECIFIED are original paraphrases
|
||||
* written for this project (not Waite's own text) so the reading can
|
||||
* address the person it's actually for. Each array is indexed by
|
||||
* Gender (tarot.h): [GENDER_UNSPECIFIED], [GENDER_MALE], [GENDER_FEMALE]. */
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *description;
|
||||
const char *name[3];
|
||||
const char *description[3];
|
||||
} PositionInfo;
|
||||
|
||||
static const PositionInfo k_positions[TAROT_SPREAD_SIZE] = {
|
||||
[POSITION_PRESENT] = {
|
||||
"The Present",
|
||||
"This covers him: the general influence affecting the matter."
|
||||
{ "The Present", "The Present", "The Present" },
|
||||
{
|
||||
"This covers them: the general influence affecting the matter.",
|
||||
"This covers him: the general influence affecting the matter.",
|
||||
"This covers her: the general influence affecting the matter.",
|
||||
}
|
||||
},
|
||||
[POSITION_CHALLENGE] = {
|
||||
"The Challenge",
|
||||
"This crosses him: the nature of the obstacle in the matter."
|
||||
{ "The Challenge", "The Challenge", "The Challenge" },
|
||||
{
|
||||
"This crosses them: the nature of the obstacle in the matter.",
|
||||
"This crosses him: the nature of the obstacle in the matter.",
|
||||
"This crosses her: the nature of the obstacle in the matter.",
|
||||
}
|
||||
},
|
||||
[POSITION_CROWN] = {
|
||||
"The Crown",
|
||||
"This crowns him: the aim or ideal, the best that can be achieved."
|
||||
{ "The Crown", "The Crown", "The Crown" },
|
||||
{
|
||||
"This crowns them: the aim or ideal, the best that can be achieved.",
|
||||
"This crowns him: the aim or ideal, the best that can be achieved.",
|
||||
"This crowns her: the aim or ideal, the best that can be achieved.",
|
||||
}
|
||||
},
|
||||
[POSITION_FOUNDATION] = {
|
||||
"The Foundation",
|
||||
"This is beneath him: the basis of the matter, already actual."
|
||||
{ "The Foundation", "The Foundation", "The Foundation" },
|
||||
{
|
||||
"This is beneath them: the basis of the matter, already actual.",
|
||||
"This is beneath him: the basis of the matter, already actual.",
|
||||
"This is beneath her: the basis of the matter, already actual.",
|
||||
}
|
||||
},
|
||||
[POSITION_RECENT_PAST] = {
|
||||
"The Recent Past",
|
||||
"This is behind him: the influence that is just passing away."
|
||||
{ "The Recent Past", "The Recent Past", "The Recent Past" },
|
||||
{
|
||||
"This is behind them: the influence that is just passing away.",
|
||||
"This is behind him: the influence that is just passing away.",
|
||||
"This is behind her: the influence that is just passing away.",
|
||||
}
|
||||
},
|
||||
[POSITION_NEAR_FUTURE] = {
|
||||
"The Near Future",
|
||||
"This is before him: the influence now coming into action."
|
||||
{ "The Near Future", "The Near Future", "The Near Future" },
|
||||
{
|
||||
"This is before them: the influence now coming into action.",
|
||||
"This is before him: the influence now coming into action.",
|
||||
"This is before her: the influence now coming into action.",
|
||||
}
|
||||
},
|
||||
[POSITION_ATTITUDE] = {
|
||||
"Himself",
|
||||
"His position or attitude in the circumstances."
|
||||
{ "Themself", "Himself", "Herself" },
|
||||
{
|
||||
"Their position or attitude in the circumstances.",
|
||||
"His position or attitude in the circumstances.",
|
||||
"Her position or attitude in the circumstances.",
|
||||
}
|
||||
},
|
||||
[POSITION_ENVIRONMENT] = {
|
||||
"His House",
|
||||
"His environment and the tendencies at work therein."
|
||||
{ "Their House", "His House", "Her House" },
|
||||
{
|
||||
"Their environment and the tendencies at work therein.",
|
||||
"His environment and the tendencies at work therein.",
|
||||
"Her environment and the tendencies at work therein.",
|
||||
}
|
||||
},
|
||||
[POSITION_HOPES_AND_FEARS] = {
|
||||
"Hopes and Fears",
|
||||
"His hopes or fears in the matter."
|
||||
{ "Hopes and Fears", "Hopes and Fears", "Hopes and Fears" },
|
||||
{
|
||||
"Their hopes or fears in the matter.",
|
||||
"His hopes or fears in the matter.",
|
||||
"Her hopes or fears in the matter.",
|
||||
}
|
||||
},
|
||||
[POSITION_OUTCOME] = {
|
||||
"The Outcome",
|
||||
"What will come: the final result of the matter."
|
||||
{ "The Outcome", "The Outcome", "The Outcome" },
|
||||
{
|
||||
"What will come: the final result of the matter.",
|
||||
"What will come: the final result of the matter.",
|
||||
"What will come: the final result of the matter.",
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -229,13 +274,44 @@ static const char *card_field(const char *slug, const char *field, const char *f
|
||||
return i18n_get(key, fallback);
|
||||
}
|
||||
|
||||
static const char *position_field(const char *slug, const char *field, const char *fallback) {
|
||||
char key[64];
|
||||
strcpy(key, "position.");
|
||||
strcat(key, slug);
|
||||
strcat(key, ".");
|
||||
strcat(key, field);
|
||||
return i18n_get(key, fallback);
|
||||
/* Suffix appended to the i18n key for the male/female variants - the
|
||||
* unspecified/neutral variant uses the plain, unsuffixed key so it
|
||||
* matches the field name callers already know (e.g. "position.
|
||||
* attitude.name"), same convention as card_field()'s upright/reversed
|
||||
* field names above. */
|
||||
static const char *const k_gender_suffix[3] = {
|
||||
[GENDER_UNSPECIFIED] = "",
|
||||
[GENDER_MALE] = ".male",
|
||||
[GENDER_FEMALE] = ".female",
|
||||
};
|
||||
|
||||
static const char *const k_gender_slug[3] = {
|
||||
[GENDER_UNSPECIFIED] = "unspecified",
|
||||
[GENDER_MALE] = "male",
|
||||
[GENDER_FEMALE] = "female",
|
||||
};
|
||||
|
||||
static const char *position_field(const char *slug, const char *field, Gender gender,
|
||||
const char *fallback) {
|
||||
char neutral_key[80];
|
||||
strcpy(neutral_key, "position.");
|
||||
strcat(neutral_key, slug);
|
||||
strcat(neutral_key, ".");
|
||||
strcat(neutral_key, field);
|
||||
if (gender == GENDER_UNSPECIFIED) return i18n_get(neutral_key, fallback);
|
||||
|
||||
/* Not every position's text actually varies by gender (e.g. "The
|
||||
* Outcome" has no pronoun) - the .lang files only define a ".male"/
|
||||
* ".female" key where the wording differs. So look up the neutral key
|
||||
* first and use *that* (translated, if a catalog is loaded) as the
|
||||
* fallback for the gendered key, rather than jumping straight past a
|
||||
* loaded translation to the hardcoded English `fallback` whenever the
|
||||
* gendered key happens to be absent. */
|
||||
const char *neutral_text = i18n_get(neutral_key, fallback);
|
||||
char key[88];
|
||||
strcpy(key, neutral_key);
|
||||
strcat(key, k_gender_suffix[gender]);
|
||||
return i18n_get(key, neutral_text);
|
||||
}
|
||||
|
||||
const char *tarot_card_name(TarotCard card) {
|
||||
@@ -252,13 +328,15 @@ const char *tarot_card_meaning(TarotCard card, bool reversed) {
|
||||
reversed ? k_cards[card].reversed : k_cards[card].upright);
|
||||
}
|
||||
|
||||
const char *tarot_position_name(CelticCrossPosition position) {
|
||||
return position_field(k_position_slug[position], "name", k_positions[position].name);
|
||||
const char *tarot_position_name(CelticCrossPosition position, Gender gender) {
|
||||
return position_field(k_position_slug[position], "name", gender, k_positions[position].name[gender]);
|
||||
}
|
||||
|
||||
const char *tarot_position_description(CelticCrossPosition position) {
|
||||
return position_field(k_position_slug[position], "desc", k_positions[position].description);
|
||||
const char *tarot_position_description(CelticCrossPosition position, Gender gender) {
|
||||
return position_field(k_position_slug[position], "desc", gender,
|
||||
k_positions[position].description[gender]);
|
||||
}
|
||||
|
||||
const char *tarot_card_slug(TarotCard card) { return k_card_slug[card]; }
|
||||
const char *tarot_position_slug(CelticCrossPosition position) { return k_position_slug[position]; }
|
||||
const char *tarot_gender_slug(Gender gender) { return k_gender_slug[gender]; }
|
||||
|
||||
Reference in New Issue
Block a user