Add watch/phone icons and logo, reorder the interpretation report, and stamp every report with the day it's for
build / build (push) Successful in 30s

This commit is contained in:
ml
2026-07-16 18:54:32 +02:00
parent b94e76931a
commit 1ebbfebe31
16 changed files with 409 additions and 99 deletions
+124 -60
View File
@@ -126,6 +126,15 @@ static const char *sign_display_name(ZodiacSign sign) {
return ui(key, fallback[sign]);
}
/* "YYYY-MM-DD" for reading->utc_moment - the day this reading is for
* (see reading_load_json()/parse_date() in reading_io.c), not today's
* real date if the two ever differ (e.g. reading.json was generated
* earlier and interpreted later). */
static void format_date(time_t utc_moment, char *out, size_t out_size) {
struct tm *utc = gmtime(&utc_moment);
snprintf(out, out_size, "%04d-%02d-%02d", utc->tm_year + 1900, utc->tm_mon + 1, utc->tm_mday);
}
static const char *day_level_name(DaySignificance level) {
switch (level) {
case DAY_QUIET: return ui("interp.day_level.quiet", "Quiet");
@@ -179,6 +188,32 @@ static char *capture_guidance(const DailyInterpretation *interp, const CelticCro
return buf;
}
/* Prints one significant-transit item's full detail (title line with
* sign/house/orb/score, plus its narrative sentence) - shared between
* the "Significant Transits" list and the day's top item, which is
* repeated in full just above the guidance paragraph (see
* interp_print_text) so the reader has that same detail in view right
* next to the guidance text it informs. `index` is 1-based and prefixes
* the line with "N. "; pass 0 to omit it, for the repeated top item,
* which isn't part of the numbered list. */
static void print_transit_item_text(FILE *out, const DailyReading *reading,
const SignificantItem *item, int index) {
const Aspect *a = &item->aspect;
if (index > 0) fprintf(out, " %d. ", index);
fprintf(out, "%s %s", ui("ui.transiting", "Transiting"), body_display_name(a->transiting_planet));
print_body_in_sign_text(out, &reading->transits.bodies[a->transiting_planet]);
fprintf(out, " %s %s %s", aspect_display_name(a->type), ui("ui.natal", "natal"),
body_display_name(a->natal_planet));
print_body_in_sign_text(out, &reading->natal.bodies[a->natal_planet]);
fprintf(out, " (%s %.1f\xc2\xb0, %s %.2f)\n", ui("ui.orb", "orb"), a->orb,
ui("interp.score", "score"), item->score);
char narrative_buf[NARRATIVE_TEXT_MAX];
narrative_write(narrative_buf, sizeof narrative_buf, " ", a,
reading->transits.bodies[a->transiting_planet].house);
fputs(narrative_buf, out);
}
static void print_top_transits_header(FILE *out, int count) {
if (count == 1) {
fprintf(out, "%s\n", ui("interp.top_transits.one", "Top significant transit:"));
@@ -203,6 +238,10 @@ static void print_celtic_cross_text(FILE *out, const CelticCrossSpread *spread)
}
static void interp_print_text(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
char date_buf[32];
format_date(reading->utc_moment, date_buf, sizeof date_buf);
fprintf(out, "%s %s\n\n", ui("interp.reading_date", "Reading for:"), date_buf);
fprintf(out, "%s %s (%d/%d)\n", ui("interp.day_significance", "Day significance:"),
day_level_name(interp->day_level), interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
if (interpretation_deserves_framing(interp)) {
@@ -211,41 +250,68 @@ static void interp_print_text(FILE *out, const DailyReading *reading, const Dail
}
fprintf(out, "\n");
fprintf(out, "%s\n", ui("interp.heading_guidance", "Guidance"));
if (interp->top_item_count > 0) {
print_transit_item_text(out, reading, &interp->top_items[0], 0);
}
char guidance_buf[GUIDANCE_TEXT_MAX];
guidance_write(guidance_buf, sizeof guidance_buf, "", interp, &reading->spread);
fputs(guidance_buf, out);
fprintf(out, "\n");
if (interp->top_item_count == 0) {
fprintf(out, "%s\n", ui("interp.no_notable_transits", "No notable transits today."));
} else {
print_top_transits_header(out, interp->top_item_count);
for (int i = 0; i < interp->top_item_count; i++) {
const SignificantItem *item = &interp->top_items[i];
const Aspect *a = &item->aspect;
fprintf(out, " %d. %s %s", i + 1, ui("ui.transiting", "Transiting"),
body_display_name(a->transiting_planet));
print_body_in_sign_text(out, &reading->transits.bodies[a->transiting_planet]);
fprintf(out, " %s %s %s", aspect_display_name(a->type), ui("ui.natal", "natal"),
body_display_name(a->natal_planet));
print_body_in_sign_text(out, &reading->natal.bodies[a->natal_planet]);
fprintf(out, " (%s %.1f\xc2\xb0, %s %.2f)\n", ui("ui.orb", "orb"), a->orb,
ui("interp.score", "score"), item->score);
char narrative_buf[NARRATIVE_TEXT_MAX];
narrative_write(narrative_buf, sizeof narrative_buf, " ", a,
reading->transits.bodies[a->transiting_planet].house);
fputs(narrative_buf, out);
print_transit_item_text(out, reading, &interp->top_items[i], i + 1);
}
}
fprintf(out, "\n");
print_celtic_cross_text(out, &reading->spread);
fprintf(out, "\n");
char guidance_buf[GUIDANCE_TEXT_MAX];
guidance_write(guidance_buf, sizeof guidance_buf, "", interp, &reading->spread);
fputs(guidance_buf, out);
}
/* ===== --format html ===== */
/* HTML counterpart of print_transit_item_text() - same shared-between-
* the-repeated-top-item-and-the-full-list purpose, one table row (title
* + orb/score) plus a narrative row if non-empty. `index` is 1-based;
* pass 0 to omit the "N." cell, for the repeated top item. */
static void print_transit_item_html(FILE *out, const DailyReading *reading,
const SignificantItem *item, int index) {
const Aspect *a = &item->aspect;
char index_buf[16] = "";
if (index > 0) snprintf(index_buf, sizeof index_buf, "%d.", index);
char *text = capture_narrative(a, reading->transits.bodies[a->transiting_planet].house);
fprintf(out, "<tr><td>%s</td><td>%s %s %s %s %s</td>"
"<td>%s %.1f&deg;, %s %.2f</td></tr>\n",
index_buf, ui("ui.transiting", "Transiting"), body_display_name(a->transiting_planet),
aspect_display_name(a->type), ui("ui.natal", "natal"), body_display_name(a->natal_planet),
ui("ui.orb", "orb"), a->orb, ui("interp.score", "score"), item->score);
if (text[0] != '\0') fprintf(out, "<tr><td></td><td colspan=\"2\">%s</td></tr>\n", text);
free(text);
}
/* One Celtic Cross card as a ".card" div - shared between the
* Attitude/Outcome preview (right after guidance) and the full spread
* further down, so both render identically. */
static void print_card_html(FILE *out, CelticCrossPosition position, const TarotDraw *draw) {
fprintf(out,
"<div class=\"card\">\n"
" <div class=\"position\">%s</div>\n"
" <img class=\"%s\" src=\"img/%s\" alt=\"%s\">\n"
" <div class=\"name\">%s%s</div>\n"
" <div class=\"desc\">%s</div>\n"
" <div class=\"meaning\">%s</div>\n"
"</div>\n",
tarot_position_name(position), draw->reversed ? "reversed" : "",
tarot_card_image_file(draw->card), tarot_card_name(draw->card), tarot_card_name(draw->card),
reversed_marker(draw->reversed), tarot_position_description(position),
tarot_card_meaning(draw->card, draw->reversed));
}
static void interp_print_html(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
fprintf(out,
"<!doctype html>\n<html><head><meta charset=\"utf-8\">\n"
@@ -267,6 +333,11 @@ static void interp_print_html(FILE *out, const DailyReading *reading, const Dail
fprintf(out, "<h1>%s</h1>\n", ui("interp.heading", "Daily Interpretation"));
char date_buf[32];
format_date(reading->utc_moment, date_buf, sizeof date_buf);
fprintf(out, "<p class=\"reading-date\">%s %s</p>\n",
ui("interp.reading_date", "Reading for:"), date_buf);
fprintf(out, "<h2>%s</h2>\n<p>%s %s (%d/%d)",
ui("interp.heading_day_significance", "Day Significance"),
ui("interp.day_significance", "Day significance:"), day_level_name(interp->day_level),
@@ -277,48 +348,20 @@ static void interp_print_html(FILE *out, const DailyReading *reading, const Dail
}
fprintf(out, "</p>\n");
fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_transits", "Significant Transits"));
if (interp->top_item_count == 0) {
fprintf(out, "<p>%s</p>\n", ui("interp.no_notable_transits", "No notable transits today."));
} else {
fprintf(out, "<table>\n");
for (int i = 0; i < interp->top_item_count; i++) {
const SignificantItem *item = &interp->top_items[i];
const Aspect *a = &item->aspect;
char *text = capture_narrative(a, reading->transits.bodies[a->transiting_planet].house);
char *guidance = capture_guidance(interp, &reading->spread);
fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_guidance", "Guidance"));
fprintf(out, "<tr><td>%d.</td><td>%s %s %s %s %s</td>"
"<td>%s %.1f&deg;, %s %.2f</td></tr>\n",
i + 1, ui("ui.transiting", "Transiting"), body_display_name(a->transiting_planet),
aspect_display_name(a->type), ui("ui.natal", "natal"),
body_display_name(a->natal_planet), ui("ui.orb", "orb"), a->orb,
ui("interp.score", "score"), item->score);
if (text[0] != '\0') fprintf(out, "<tr><td></td><td colspan=\"2\">%s</td></tr>\n", text);
free(text);
}
fprintf(out, "</table>\n");
}
fprintf(out, "<h2>%s</h2>\n<div class=\"spread\">\n", ui("ui.celtic_cross", "Celtic Cross"));
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
const TarotDraw *draw = &reading->spread.positions[i];
fprintf(out,
"<div class=\"card\">\n"
" <div class=\"position\">%s</div>\n"
" <img class=\"%s\" src=\"img/%s\" alt=\"%s\">\n"
" <div class=\"name\">%s%s</div>\n"
" <div class=\"desc\">%s</div>\n"
" <div class=\"meaning\">%s</div>\n"
"</div>\n",
tarot_position_name((CelticCrossPosition)i), draw->reversed ? "reversed" : "",
tarot_card_image_file(draw->card), tarot_card_name(draw->card), tarot_card_name(draw->card),
reversed_marker(draw->reversed), tarot_position_description((CelticCrossPosition)i),
tarot_card_meaning(draw->card, draw->reversed));
}
fprintf(out, "<div class=\"spread attitude-outcome\">\n");
print_card_html(out, POSITION_ATTITUDE, &reading->spread.positions[POSITION_ATTITUDE]);
print_card_html(out, POSITION_OUTCOME, &reading->spread.positions[POSITION_OUTCOME]);
fprintf(out, "</div>\n");
char *guidance = capture_guidance(interp, &reading->spread);
fprintf(out, "<h2>%s</h2>\n<div class=\"guidance\">\n", ui("interp.heading_guidance", "Guidance"));
fprintf(out, "<div class=\"guidance\">\n");
if (interp->top_item_count > 0) {
fprintf(out, "<table>\n");
print_transit_item_html(out, reading, &interp->top_items[0], 0);
fprintf(out, "</table>\n");
}
const char *start = guidance;
for (const char *p = guidance; ; p++) {
if (*p == '\n' || *p == '\0') {
@@ -330,6 +373,23 @@ static void interp_print_html(FILE *out, const DailyReading *reading, const Dail
free(guidance);
fprintf(out, "</div>\n");
fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_transits", "Significant Transits"));
if (interp->top_item_count == 0) {
fprintf(out, "<p>%s</p>\n", ui("interp.no_notable_transits", "No notable transits today."));
} else {
fprintf(out, "<table>\n");
for (int i = 0; i < interp->top_item_count; i++) {
print_transit_item_html(out, reading, &interp->top_items[i], i + 1);
}
fprintf(out, "</table>\n");
}
fprintf(out, "<h2>%s</h2>\n<div class=\"spread\">\n", ui("ui.celtic_cross", "Celtic Cross"));
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
print_card_html(out, (CelticCrossPosition)i, &reading->spread.positions[i]);
}
fprintf(out, "</div>\n");
fprintf(out, "</body></html>\n");
}
@@ -356,6 +416,10 @@ static void json_string(FILE *out, const char *s) {
static void interp_print_json(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
fprintf(out, "{\n");
char date_buf[32];
format_date(reading->utc_moment, date_buf, sizeof date_buf);
fprintf(out, " \"date\": \"%s\",\n", date_buf);
fprintf(out, " \"day_significance\": {\n");
fprintf(out, " \"level\": \"%s\",\n", k_day_level_slug[interp->day_level]);
fprintf(out, " \"rank\": %d,\n", interp->day_level + 1);