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
build / build (push) Successful in 30s
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
/watch/build
|
/watch/build
|
||||||
/watch/src/c/i18n_tables.auto.c
|
/watch/src/c/i18n_tables.auto.c
|
||||||
/watch/resources/img
|
/watch/resources/img
|
||||||
|
/watch/resources/app_icon.png
|
||||||
|
|
||||||
# Durable backup of the same real birth data, outside dist/ so a dist/
|
# Durable backup of the same real birth data, outside dist/ so a dist/
|
||||||
# wipe doesn't lose it — see the Makefile's `scripts` target. Never
|
# wipe doesn't lose it — see the Makefile's `scripts` target. Never
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ cards rendered as an image:
|
|||||||
|
|
||||||
### `--format json`
|
### `--format json`
|
||||||
|
|
||||||
A single JSON object mirroring `DailyReading` exactly — `natal`,
|
A single JSON object mirroring `DailyReading` exactly — `date`, `natal`,
|
||||||
`transits`, `spread` — using the **slug** accessors
|
`transits`, `spread` — using the **slug** accessors
|
||||||
(`astro_body_slug()`, `astro_sign_slug()`, `astro_moon_phase_slug()`,
|
(`astro_body_slug()`, `astro_sign_slug()`, `astro_moon_phase_slug()`,
|
||||||
`astro_aspect_slug()`, `tarot_card_slug()`, `tarot_position_slug()`)
|
`astro_aspect_slug()`, `tarot_card_slug()`, `tarot_position_slug()`)
|
||||||
@@ -182,6 +182,7 @@ read.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"date": "2026-07-16",
|
||||||
"natal": {
|
"natal": {
|
||||||
"bodies": [
|
"bodies": [
|
||||||
{"body": "sun", "sign": "taurus", "degree_in_sign": 23.4926, "ecliptic_longitude": 53.4926, "house": 3},
|
{"body": "sun", "sign": "taurus", "degree_in_sign": 23.4926, "ecliptic_longitude": 53.4926, "house": 3},
|
||||||
@@ -210,6 +211,13 @@ read.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `date` is the UTC calendar date of `--date` (or the current system time
|
||||||
|
if `--date` was omitted) — the day this reading is *for*, not
|
||||||
|
necessarily the day it was generated on. `reading_generate()` stores
|
||||||
|
the full moment in `DailyReading.utc_moment`; only the date part is
|
||||||
|
serialized here since that's the only thing the rest of this format
|
||||||
|
(and the interpreter's own reports, which read this field back — see
|
||||||
|
"The interpreter" below) ever display.
|
||||||
- `bodies` is always `NUM_BODIES` (10) entries, Sun..Pluto in `Body` enum
|
- `bodies` is always `NUM_BODIES` (10) entries, Sun..Pluto in `Body` enum
|
||||||
order; `spread.positions` is always `TAROT_SPREAD_SIZE` (10) entries in
|
order; `spread.positions` is always `TAROT_SPREAD_SIZE` (10) entries in
|
||||||
`CelticCrossPosition` enum order (Present, Challenge, Crown,
|
`CelticCrossPosition` enum order (Present, Challenge, Crown,
|
||||||
@@ -279,11 +287,16 @@ dist/run-interpreter.sh html de # override format/language for this run
|
|||||||
### `--format text`
|
### `--format text`
|
||||||
|
|
||||||
A plain-text report, printed in a fixed order —
|
A plain-text report, printed in a fixed order —
|
||||||
1. the day's overall significance level (`Quiet`/`Notable`/
|
1. the day this reading is for (`Reading for: 2026-07-16`), from the
|
||||||
|
input JSON's top-level `date` field (see `--format json` above) —
|
||||||
|
not necessarily the day the report is actually being read on, if
|
||||||
|
`reading.json` was generated earlier and piped into
|
||||||
|
`interpreter-cli` later;
|
||||||
|
2. the day's overall significance level (`Quiet`/`Notable`/
|
||||||
`Significant`/`Major`, plus its rank out of the 4 defined levels,
|
`Significant`/`Major`, plus its rank out of the 4 defined levels,
|
||||||
e.g. `Notable (2/4)`), with a "worth a deeper Celtic Cross look"
|
e.g. `Notable (2/4)`), with a "worth a deeper Celtic Cross look"
|
||||||
line on `Major` days only;
|
line on `Major` days only;
|
||||||
2. up to 5 of today's aspects ranked by score, each naming the sign
|
3. up to 5 of today's aspects ranked by score, each naming the sign
|
||||||
and natal house the transiting planet currently occupies and the
|
and natal house the transiting planet currently occupies and the
|
||||||
sign and house of the natal planet it's aspecting, followed by a
|
sign and house of the natal planet it's aspecting, followed by a
|
||||||
short narrative sentence about what that transiting planet
|
short narrative sentence about what that transiting planet
|
||||||
@@ -291,10 +304,10 @@ A plain-text report, printed in a fixed order —
|
|||||||
governs* (see `CLAUDE.md`'s `narrative.c` bullet) — omitted only if
|
governs* (see `CLAUDE.md`'s `narrative.c` bullet) — omitted only if
|
||||||
the source material has nothing to say for that planet/aspect
|
the source material has nothing to say for that planet/aspect
|
||||||
combination;
|
combination;
|
||||||
3. the full 10-position Celtic Cross spread, in Waite's own drawing
|
4. the full 10-position Celtic Cross spread, in Waite's own drawing
|
||||||
order, each with its card (and orientation), the position's own
|
order, each with its card (and orientation), the position's own
|
||||||
description, and that card's actual meaning;
|
description, and that card's actual meaning;
|
||||||
4. finally, a guidance paragraph tying the day's top transit to the
|
5. finally, a guidance paragraph tying the day's top transit to the
|
||||||
spread's Attitude/Outcome cards, with its opening sentence tailored
|
spread's Attitude/Outcome cards, with its opening sentence tailored
|
||||||
to `day_level` (stronger wording on `Major`, softer on
|
to `day_level` (stronger wording on `Major`, softer on
|
||||||
`Quiet`/`Notable`, falling back to a transit-free reading of the
|
`Quiet`/`Notable`, falling back to a transit-free reading of the
|
||||||
@@ -305,6 +318,8 @@ A plain-text report, printed in a fixed order —
|
|||||||
seen the full spread it references.
|
seen the full spread it references.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
Reading for: 2026-07-16
|
||||||
|
|
||||||
Day significance: Major (4/4)
|
Day significance: Major (4/4)
|
||||||
(a major transit today - worth a deeper Celtic Cross look)
|
(a major transit today - worth a deeper Celtic Cross look)
|
||||||
|
|
||||||
@@ -354,6 +369,8 @@ The same report with `--lang de` (same input as the Major example
|
|||||||
above):
|
above):
|
||||||
|
|
||||||
```
|
```
|
||||||
|
Lesung für: 2026-07-16
|
||||||
|
|
||||||
Bedeutung des Tages: Einschneidend (4/4)
|
Bedeutung des Tages: Einschneidend (4/4)
|
||||||
(ein einschneidender Transit heute - ein genauerer Blick auf das Keltische Kreuz lohnt sich)
|
(ein einschneidender Transit heute - ein genauerer Blick auf das Keltische Kreuz lohnt sich)
|
||||||
|
|
||||||
@@ -415,6 +432,7 @@ has the slugs to work with directly.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"date": "2026-07-16",
|
||||||
"day_significance": {
|
"day_significance": {
|
||||||
"level": "major",
|
"level": "major",
|
||||||
"rank": 4,
|
"rank": 4,
|
||||||
@@ -448,6 +466,9 @@ has the slugs to work with directly.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `date` is copied straight through from the input JSON's own top-level
|
||||||
|
`date` field (see `--format json` above) - always language-independent
|
||||||
|
regardless of `--lang`, same as every other slug in this document.
|
||||||
- `significant_transits` has `interp.top_item_count` entries (0-5, same
|
- `significant_transits` has `interp.top_item_count` entries (0-5, same
|
||||||
ranking as `--format text`); `celtic_cross` always has exactly 10, in
|
ranking as `--format text`); `celtic_cross` always has exactly 10, in
|
||||||
`CelticCrossPosition` enum order (same order as `deck-engine`'s own
|
`CelticCrossPosition` enum order (same order as `deck-engine`'s own
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ card.world.upright=Gesicherter Erfolg, Belohnung, Reise, Weg, Auswanderung, Fluc
|
|||||||
card.world.reversed=Trägheit, Erstarrung, Stillstand, Beständigkeit.
|
card.world.reversed=Trägheit, Erstarrung, Stillstand, Beständigkeit.
|
||||||
|
|
||||||
# --- UI-Bezeichnungen des Interpreters (interpreter/src/main.c) ---
|
# --- UI-Bezeichnungen des Interpreters (interpreter/src/main.c) ---
|
||||||
|
interp.reading_date=Lesung für:
|
||||||
interp.day_significance=Bedeutung des Tages:
|
interp.day_significance=Bedeutung des Tages:
|
||||||
interp.day_level.quiet=Ruhig
|
interp.day_level.quiet=Ruhig
|
||||||
interp.day_level.notable=Bemerkenswert
|
interp.day_level.notable=Bemerkenswert
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ card.world.upright=Assured success, recompense, voyage, route, emigration, fligh
|
|||||||
card.world.reversed=Inertia, fixity, stagnation, permanence.
|
card.world.reversed=Inertia, fixity, stagnation, permanence.
|
||||||
|
|
||||||
# --- Interpreter UI labels (interpreter/src/main.c) ---
|
# --- Interpreter UI labels (interpreter/src/main.c) ---
|
||||||
|
interp.reading_date=Reading for:
|
||||||
interp.day_significance=Day significance:
|
interp.day_significance=Day significance:
|
||||||
interp.day_level.quiet=Quiet
|
interp.day_level.quiet=Quiet
|
||||||
interp.day_level.notable=Notable
|
interp.day_level.notable=Notable
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
void reading_generate(const char *tarot_seed, const BirthData *birth,
|
void reading_generate(const char *tarot_seed, const BirthData *birth,
|
||||||
time_t utc_moment, DailyReading *out) {
|
time_t utc_moment, DailyReading *out) {
|
||||||
|
out->utc_moment = utc_moment;
|
||||||
astro_compute_natal_chart(birth, &out->natal);
|
astro_compute_natal_chart(birth, &out->natal);
|
||||||
astro_compute_daily_transits(utc_moment, &out->natal, &out->transits);
|
astro_compute_daily_transits(utc_moment, &out->natal, &out->transits);
|
||||||
tarot_draw_celtic_cross(tarot_seed, &out->spread);
|
tarot_draw_celtic_cross(tarot_seed, &out->spread);
|
||||||
@@ -160,6 +161,9 @@ static void print_body_position_json(FILE *out, const char *indent, Body body,
|
|||||||
void reading_print_json(const DailyReading *r, FILE *out) {
|
void reading_print_json(const DailyReading *r, FILE *out) {
|
||||||
fprintf(out, "{\n");
|
fprintf(out, "{\n");
|
||||||
|
|
||||||
|
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, " \"natal\": {\n \"bodies\": [\n");
|
fprintf(out, " \"natal\": {\n \"bodies\": [\n");
|
||||||
for (int b = 0; b < NUM_BODIES; b++) {
|
for (int b = 0; b < NUM_BODIES; b++) {
|
||||||
print_body_position_json(out, " ", (Body)b, &r->natal.bodies[b]);
|
print_body_position_json(out, " ", (Body)b, &r->natal.bodies[b]);
|
||||||
|
|||||||
@@ -15,6 +15,12 @@
|
|||||||
#include "tarot.h"
|
#include "tarot.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
/* The moment passed to reading_generate() - the day this reading is
|
||||||
|
* for, not when it happened to be generated. Set by reading_generate()
|
||||||
|
* itself, so every caller (CLI, watch, reading_load_json() on the
|
||||||
|
* interpreter side) gets it for free without threading it through
|
||||||
|
* separately. */
|
||||||
|
time_t utc_moment;
|
||||||
NatalChart natal;
|
NatalChart natal;
|
||||||
DailyTransits transits;
|
DailyTransits transits;
|
||||||
CelticCrossSpread spread;
|
CelticCrossSpread spread;
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ static void test_reading_print_json_shape(void) {
|
|||||||
* a stray astro_*_name()/tarot_*_name() call leaking translated text
|
* a stray astro_*_name()/tarot_*_name() call leaking translated text
|
||||||
* into what must stay language-independent regardless of --lang). */
|
* into what must stay language-independent regardless of --lang). */
|
||||||
assert(buf[0] == '{');
|
assert(buf[0] == '{');
|
||||||
|
assert(strstr(buf, "\"date\": \"2025-07-02\"") != NULL); /* UTC date of the fixed 1751500000 seed */
|
||||||
assert(strstr(buf, "\"natal\"") != NULL);
|
assert(strstr(buf, "\"natal\"") != NULL);
|
||||||
assert(strstr(buf, "\"transits\"") != NULL);
|
assert(strstr(buf, "\"transits\"") != NULL);
|
||||||
assert(strstr(buf, "\"spread\"") != NULL);
|
assert(strstr(buf, "\"spread\"") != NULL);
|
||||||
|
|||||||
+124
-60
@@ -126,6 +126,15 @@ static const char *sign_display_name(ZodiacSign sign) {
|
|||||||
return ui(key, fallback[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) {
|
static const char *day_level_name(DaySignificance level) {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case DAY_QUIET: return ui("interp.day_level.quiet", "Quiet");
|
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;
|
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) {
|
static void print_top_transits_header(FILE *out, int count) {
|
||||||
if (count == 1) {
|
if (count == 1) {
|
||||||
fprintf(out, "%s\n", ui("interp.top_transits.one", "Top significant transit:"));
|
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) {
|
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:"),
|
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);
|
day_level_name(interp->day_level), interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
|
||||||
if (interpretation_deserves_framing(interp)) {
|
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, "\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) {
|
if (interp->top_item_count == 0) {
|
||||||
fprintf(out, "%s\n", ui("interp.no_notable_transits", "No notable transits today."));
|
fprintf(out, "%s\n", ui("interp.no_notable_transits", "No notable transits today."));
|
||||||
} else {
|
} else {
|
||||||
print_top_transits_header(out, interp->top_item_count);
|
print_top_transits_header(out, interp->top_item_count);
|
||||||
for (int i = 0; i < interp->top_item_count; i++) {
|
for (int i = 0; i < interp->top_item_count; i++) {
|
||||||
const SignificantItem *item = &interp->top_items[i];
|
print_transit_item_text(out, reading, &interp->top_items[i], i + 1);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fprintf(out, "\n");
|
fprintf(out, "\n");
|
||||||
|
|
||||||
print_celtic_cross_text(out, &reading->spread);
|
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 ===== */
|
/* ===== --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°, %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) {
|
static void interp_print_html(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
|
||||||
fprintf(out,
|
fprintf(out,
|
||||||
"<!doctype html>\n<html><head><meta charset=\"utf-8\">\n"
|
"<!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"));
|
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)",
|
fprintf(out, "<h2>%s</h2>\n<p>%s %s (%d/%d)",
|
||||||
ui("interp.heading_day_significance", "Day Significance"),
|
ui("interp.heading_day_significance", "Day Significance"),
|
||||||
ui("interp.day_significance", "Day significance:"), day_level_name(interp->day_level),
|
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, "</p>\n");
|
||||||
|
|
||||||
fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_transits", "Significant Transits"));
|
char *guidance = capture_guidance(interp, &reading->spread);
|
||||||
if (interp->top_item_count == 0) {
|
fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_guidance", "Guidance"));
|
||||||
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);
|
|
||||||
|
|
||||||
fprintf(out, "<tr><td>%d.</td><td>%s %s %s %s %s</td>"
|
fprintf(out, "<div class=\"spread attitude-outcome\">\n");
|
||||||
"<td>%s %.1f°, %s %.2f</td></tr>\n",
|
print_card_html(out, POSITION_ATTITUDE, &reading->spread.positions[POSITION_ATTITUDE]);
|
||||||
i + 1, ui("ui.transiting", "Transiting"), body_display_name(a->transiting_planet),
|
print_card_html(out, POSITION_OUTCOME, &reading->spread.positions[POSITION_OUTCOME]);
|
||||||
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>\n");
|
fprintf(out, "</div>\n");
|
||||||
|
|
||||||
char *guidance = capture_guidance(interp, &reading->spread);
|
fprintf(out, "<div class=\"guidance\">\n");
|
||||||
fprintf(out, "<h2>%s</h2>\n<div class=\"guidance\">\n", ui("interp.heading_guidance", "Guidance"));
|
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;
|
const char *start = guidance;
|
||||||
for (const char *p = guidance; ; p++) {
|
for (const char *p = guidance; ; p++) {
|
||||||
if (*p == '\n' || *p == '\0') {
|
if (*p == '\n' || *p == '\0') {
|
||||||
@@ -330,6 +373,23 @@ static void interp_print_html(FILE *out, const DailyReading *reading, const Dail
|
|||||||
free(guidance);
|
free(guidance);
|
||||||
fprintf(out, "</div>\n");
|
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");
|
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) {
|
static void interp_print_json(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
|
||||||
fprintf(out, "{\n");
|
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, " \"day_significance\": {\n");
|
||||||
fprintf(out, " \"level\": \"%s\",\n", k_day_level_slug[interp->day_level]);
|
fprintf(out, " \"level\": \"%s\",\n", k_day_level_slug[interp->day_level]);
|
||||||
fprintf(out, " \"rank\": %d,\n", interp->day_level + 1);
|
fprintf(out, " \"rank\": %d,\n", interp->day_level + 1);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
#define _DEFAULT_SOURCE /* for timegm, parsing "date" back into utc_moment */
|
||||||
|
|
||||||
#include "reading_io.h"
|
#include "reading_io.h"
|
||||||
#include "json.h"
|
#include "json.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
/* Mirrors astro.c's own k_body_slug/k_aspect_slug (its i18n lookup-key
|
/* Mirrors astro.c's own k_body_slug/k_aspect_slug (its i18n lookup-key
|
||||||
@@ -133,6 +136,25 @@ static void parse_spread(const JsonValue *positions, CelticCrossSpread *out) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Parses the top-level "date" field ("YYYY-MM-DD", see reading_print_json
|
||||||
|
* in engine/src/reading.c) back into out->utc_moment, as midnight UTC of
|
||||||
|
* that date - reading_print_json only ever wrote the calendar date, not a
|
||||||
|
* time of day, so that's the only value that round-trips through
|
||||||
|
* gmtime() to the same date. Lenient like parse_bodies()/parse_spread():
|
||||||
|
* a missing or malformed "date" just leaves out->utc_moment at 0 (from
|
||||||
|
* reading_load_json's own memset), not a load failure - nothing here
|
||||||
|
* depends on it for scoring, only for display. */
|
||||||
|
static void parse_date(const JsonValue *date, DailyReading *out) {
|
||||||
|
const char *date_str = date ? json_as_string(date) : NULL;
|
||||||
|
if (!date_str) return;
|
||||||
|
|
||||||
|
struct tm tm_date = {0};
|
||||||
|
if (sscanf(date_str, "%d-%d-%d", &tm_date.tm_year, &tm_date.tm_mon, &tm_date.tm_mday) != 3) return;
|
||||||
|
tm_date.tm_year -= 1900;
|
||||||
|
tm_date.tm_mon -= 1;
|
||||||
|
out->utc_moment = timegm(&tm_date);
|
||||||
|
}
|
||||||
|
|
||||||
bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
||||||
memset(out, 0, sizeof(*out));
|
memset(out, 0, sizeof(*out));
|
||||||
|
|
||||||
@@ -174,6 +196,8 @@ bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
|||||||
}
|
}
|
||||||
out->transits.aspect_count = filled;
|
out->transits.aspect_count = filled;
|
||||||
|
|
||||||
|
parse_date(json_object_get(root, "date"), out);
|
||||||
|
|
||||||
const JsonValue *natal = json_object_get(root, "natal");
|
const JsonValue *natal = json_object_get(root, "natal");
|
||||||
parse_bodies(natal ? json_object_get(natal, "bodies") : NULL, out->natal.bodies);
|
parse_bodies(natal ? json_object_get(natal, "bodies") : NULL, out->natal.bodies);
|
||||||
parse_bodies(json_object_get(transits, "bodies"), out->transits.bodies);
|
parse_bodies(json_object_get(transits, "bodies"), out->transits.bodies);
|
||||||
|
|||||||
@@ -14,10 +14,12 @@
|
|||||||
* around each significant event, and for guidance_print()'s tarot
|
* around each significant event, and for guidance_print()'s tarot
|
||||||
* framing: out->transits.aspects[]/aspect_count (used for scoring - see
|
* framing: out->transits.aspects[]/aspect_count (used for scoring - see
|
||||||
* significance.c), out->natal.bodies[]/out->transits.bodies[] (sign +
|
* significance.c), out->natal.bodies[]/out->transits.bodies[] (sign +
|
||||||
* house per body, used only for display), and out->spread.positions[]
|
* house per body, used only for display), out->spread.positions[]
|
||||||
* (card + reversed per Celtic Cross position, used only by
|
* (card + reversed per Celtic Cross position, used only by
|
||||||
* guidance.c). out->natal.ascendant_longitude/houses[] are left zeroed -
|
* guidance.c), and out->utc_moment (the top-level "date" field, parsed
|
||||||
* nothing reads them yet.
|
* back to midnight UTC of that date - used only for display, e.g. main.c
|
||||||
|
* printing which day a reading is for). out->natal.ascendant_longitude/
|
||||||
|
* houses[] are left zeroed - nothing reads them yet.
|
||||||
*
|
*
|
||||||
* Returns false on malformed JSON, or if "transits"."aspects" isn't
|
* Returns false on malformed JSON, or if "transits"."aspects" isn't
|
||||||
* present as an array (an empty array is fine - that's a real "no
|
* present as an array (an empty array is fine - that's a real "no
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
#include "../src/json.h"
|
#include "../src/json.h"
|
||||||
#include "../src/reading_io.h"
|
#include "../src/reading_io.h"
|
||||||
@@ -48,6 +49,7 @@ static void test_json_parse_rejects_malformed(void) {
|
|||||||
* absent entirely, to prove partial spread data doesn't fail the load. */
|
* absent entirely, to prove partial spread data doesn't fail the load. */
|
||||||
static const char *k_fixture =
|
static const char *k_fixture =
|
||||||
"{"
|
"{"
|
||||||
|
" \"date\": \"2026-07-16\","
|
||||||
" \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"taurus\", \"house\": 3}], \"houses\": []},"
|
" \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"taurus\", \"house\": 3}], \"houses\": []},"
|
||||||
" \"transits\": {"
|
" \"transits\": {"
|
||||||
" \"bodies\": [{\"body\": \"sun\", \"sign\": \"cancer\", \"house\": 5}],"
|
" \"bodies\": [{\"body\": \"sun\", \"sign\": \"cancer\", \"house\": 5}],"
|
||||||
@@ -80,6 +82,30 @@ static void test_reading_load_json_extracts_aspects(void) {
|
|||||||
printf("PASS test_reading_load_json_extracts_aspects\n");
|
printf("PASS test_reading_load_json_extracts_aspects\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_reading_load_json_extracts_date(void) {
|
||||||
|
DailyReading reading;
|
||||||
|
bool ok = reading_load_json(k_fixture, strlen(k_fixture), &reading);
|
||||||
|
|
||||||
|
assert(ok);
|
||||||
|
struct tm *utc = gmtime(&reading.utc_moment);
|
||||||
|
assert(utc->tm_year + 1900 == 2026);
|
||||||
|
assert(utc->tm_mon + 1 == 7);
|
||||||
|
assert(utc->tm_mday == 16);
|
||||||
|
|
||||||
|
printf("PASS test_reading_load_json_extracts_date\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_reading_load_json_missing_date_defaults_to_zero(void) {
|
||||||
|
const char *text = "{\"transits\": {\"aspects\": []}}";
|
||||||
|
DailyReading reading;
|
||||||
|
bool ok = reading_load_json(text, strlen(text), &reading);
|
||||||
|
|
||||||
|
assert(ok);
|
||||||
|
assert(reading.utc_moment == 0);
|
||||||
|
|
||||||
|
printf("PASS test_reading_load_json_missing_date_defaults_to_zero\n");
|
||||||
|
}
|
||||||
|
|
||||||
static void test_reading_load_json_extracts_body_sign_and_house(void) {
|
static void test_reading_load_json_extracts_body_sign_and_house(void) {
|
||||||
DailyReading reading;
|
DailyReading reading;
|
||||||
bool ok = reading_load_json(k_fixture, strlen(k_fixture), &reading);
|
bool ok = reading_load_json(k_fixture, strlen(k_fixture), &reading);
|
||||||
@@ -178,6 +204,8 @@ int main(void) {
|
|||||||
test_json_parse_basic_shapes();
|
test_json_parse_basic_shapes();
|
||||||
test_json_parse_rejects_malformed();
|
test_json_parse_rejects_malformed();
|
||||||
test_reading_load_json_extracts_aspects();
|
test_reading_load_json_extracts_aspects();
|
||||||
|
test_reading_load_json_extracts_date();
|
||||||
|
test_reading_load_json_missing_date_defaults_to_zero();
|
||||||
test_reading_load_json_extracts_body_sign_and_house();
|
test_reading_load_json_extracts_body_sign_and_house();
|
||||||
test_reading_load_json_extracts_spread_positions();
|
test_reading_load_json_extracts_spread_positions();
|
||||||
test_reading_load_json_missing_spread_is_not_fatal();
|
test_reading_load_json_missing_spread_is_not_fatal();
|
||||||
|
|||||||
@@ -42,6 +42,12 @@
|
|||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"media": [
|
"media": [
|
||||||
|
{
|
||||||
|
"type": "bitmap",
|
||||||
|
"name": "IMAGE_APP_ICON",
|
||||||
|
"file": "app_icon.png",
|
||||||
|
"menuIcon": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "bitmap",
|
"type": "bitmap",
|
||||||
"name": "IMG_CARD_FOOL",
|
"name": "IMG_CARD_FOOL",
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Downsizes res/app_icon_50x50_64_color.png into watch/resources/app_icon.png.
|
||||||
|
|
||||||
|
Pebble's own build tooling (process_sdk_resources.py) hard-fails any
|
||||||
|
"menuIcon": true resource larger than 25x25px - the fixed size Pebble OS
|
||||||
|
renders app icons at in the watch's own app launcher list (and, synced
|
||||||
|
from the watch, in the mobile app's installed-apps list too). The source
|
||||||
|
art in res/ is 50x50 (square, so it downsamples to exactly 25x25 with no
|
||||||
|
letterboxing) - the color variant is used, per the same "prefer color"
|
||||||
|
choice as the rest of this app's card art, over the companion
|
||||||
|
*_gray.png fallback also present in res/.
|
||||||
|
|
||||||
|
Generated output, like i18n_tables.auto.c and resources/img/*.png - not
|
||||||
|
committed (see .gitignore), regenerate by running this script directly
|
||||||
|
or via `pebble build` (wired into wscript alongside gen_card_images.py).
|
||||||
|
Requires Pillow (`pip install Pillow`), not part of this repo's own
|
||||||
|
desktop build/test toolchain.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
SRC_PATH = os.path.normpath(
|
||||||
|
os.path.join(SCRIPT_DIR, "..", "..", "res", "app_icon_50x50_64_color.png")
|
||||||
|
)
|
||||||
|
OUT_PATH = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "resources", "app_icon.png"))
|
||||||
|
|
||||||
|
MAX_ICON_SIZE = 25 # Pebble's own hard cap for "menuIcon": true resources.
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if os.path.exists(OUT_PATH) and os.path.getmtime(OUT_PATH) >= os.path.getmtime(SRC_PATH):
|
||||||
|
print(f"{OUT_PATH} already up to date", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(OUT_PATH), exist_ok=True)
|
||||||
|
with Image.open(SRC_PATH) as img:
|
||||||
|
ratio = MAX_ICON_SIZE / max(img.width, img.height)
|
||||||
|
size = (round(img.width * ratio), round(img.height * ratio))
|
||||||
|
resized = img.convert("RGBA").resize(size, Image.LANCZOS)
|
||||||
|
resized.save(OUT_PATH, "PNG", optimize=True)
|
||||||
|
|
||||||
|
print(f"wrote {OUT_PATH} ({size[0]}x{size[1]})", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+111
-23
@@ -31,6 +31,7 @@ static GFont s_font_body;
|
|||||||
* Static rather than malloc'd - fixed, known-in-advance sizes, and it
|
* Static rather than malloc'd - fixed, known-in-advance sizes, and it
|
||||||
* keeps report_window_reload() from having to reason about freeing the
|
* keeps report_window_reload() from having to reason about freeing the
|
||||||
* previous run's buffers before reusing them. */
|
* previous run's buffers before reusing them. */
|
||||||
|
static char s_reading_date[48];
|
||||||
static char s_day_title[64];
|
static char s_day_title[64];
|
||||||
static char s_transit_titles[MAX_SIGNIFICANT_ITEMS][96];
|
static char s_transit_titles[MAX_SIGNIFICANT_ITEMS][96];
|
||||||
static char s_narratives[MAX_SIGNIFICANT_ITEMS][NARRATIVE_TEXT_MAX];
|
static char s_narratives[MAX_SIGNIFICANT_ITEMS][NARRATIVE_TEXT_MAX];
|
||||||
@@ -53,6 +54,14 @@ typedef struct {
|
|||||||
|
|
||||||
static ImageSlot s_slots[TAROT_SPREAD_SIZE];
|
static ImageSlot s_slots[TAROT_SPREAD_SIZE];
|
||||||
|
|
||||||
|
/* Attitude (0) and Outcome (1) preview slots, drawn at half screen
|
||||||
|
* width each so the two sit side by side - the BitmapLayer's own bounds
|
||||||
|
* (narrower than IMAGE_WIDTH on 144px-wide platforms) clip the centered
|
||||||
|
* image automatically, same as GAlignCenter already does for the
|
||||||
|
* full-width slots above, no separate scaling logic needed. */
|
||||||
|
#define REPEAT_SLOT_COUNT 2
|
||||||
|
static ImageSlot s_repeat_slots[REPEAT_SLOT_COUNT];
|
||||||
|
|
||||||
#define MAX_TEXT_LAYERS 50
|
#define MAX_TEXT_LAYERS 50
|
||||||
static TextLayer *s_text_layers[MAX_TEXT_LAYERS];
|
static TextLayer *s_text_layers[MAX_TEXT_LAYERS];
|
||||||
static int s_text_layer_count;
|
static int s_text_layer_count;
|
||||||
@@ -75,8 +84,8 @@ static const char *reversed_marker(bool reversed) {
|
|||||||
return reversed ? i18n_get("ui.reversed", "(Reversed)") : "";
|
return reversed ? i18n_get("ui.reversed", "(Reversed)") : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
static int16_t add_text(int16_t width, int16_t y, const char *text, GFont font) {
|
static int16_t add_text_at(int16_t x, int16_t width, int16_t y, const char *text, GFont font) {
|
||||||
TextLayer *tl = text_layer_create(GRect(0, y, width, 4000));
|
TextLayer *tl = text_layer_create(GRect(x, y, width, 4000));
|
||||||
text_layer_set_font(tl, font);
|
text_layer_set_font(tl, font);
|
||||||
text_layer_set_text(tl, text);
|
text_layer_set_text(tl, text);
|
||||||
text_layer_set_overflow_mode(tl, GTextOverflowModeWordWrap);
|
text_layer_set_overflow_mode(tl, GTextOverflowModeWordWrap);
|
||||||
@@ -91,6 +100,10 @@ static int16_t add_text(int16_t width, int16_t y, const char *text, GFont font)
|
|||||||
return y + size.h + MARGIN;
|
return y + size.h + MARGIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int16_t add_text(int16_t width, int16_t y, const char *text, GFont font) {
|
||||||
|
return add_text_at(0, width, y, text, font);
|
||||||
|
}
|
||||||
|
|
||||||
static void teardown_content(void) {
|
static void teardown_content(void) {
|
||||||
for (int i = 0; i < s_text_layer_count; i++) {
|
for (int i = 0; i < s_text_layer_count; i++) {
|
||||||
text_layer_destroy(s_text_layers[i]);
|
text_layer_destroy(s_text_layers[i]);
|
||||||
@@ -107,6 +120,30 @@ static void teardown_content(void) {
|
|||||||
s_slots[i].bitmap_layer = NULL;
|
s_slots[i].bitmap_layer = NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < REPEAT_SLOT_COUNT; i++) {
|
||||||
|
if (s_repeat_slots[i].bitmap) {
|
||||||
|
gbitmap_destroy(s_repeat_slots[i].bitmap);
|
||||||
|
s_repeat_slots[i].bitmap = NULL;
|
||||||
|
}
|
||||||
|
if (s_repeat_slots[i].bitmap_layer) {
|
||||||
|
bitmap_layer_destroy(s_repeat_slots[i].bitmap_layer);
|
||||||
|
s_repeat_slots[i].bitmap_layer = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void update_slot_visibility(ImageSlot *slot, int16_t visible_top, int16_t visible_bottom) {
|
||||||
|
bool visible = slot->y < visible_bottom && slot->y + slot->height > visible_top;
|
||||||
|
|
||||||
|
if (visible && !slot->bitmap) {
|
||||||
|
slot->bitmap = gbitmap_create_with_resource(card_resource_id(slot->card));
|
||||||
|
bitmap_layer_set_bitmap(slot->bitmap_layer, slot->bitmap);
|
||||||
|
} else if (!visible && slot->bitmap) {
|
||||||
|
bitmap_layer_set_bitmap(slot->bitmap_layer, NULL);
|
||||||
|
gbitmap_destroy(slot->bitmap);
|
||||||
|
slot->bitmap = NULL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void update_visible_images(void) {
|
static void update_visible_images(void) {
|
||||||
@@ -115,17 +152,10 @@ static void update_visible_images(void) {
|
|||||||
int16_t visible_bottom = visible_top + s_screen_height;
|
int16_t visible_bottom = visible_top + s_screen_height;
|
||||||
|
|
||||||
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
ImageSlot *slot = &s_slots[i];
|
update_slot_visibility(&s_slots[i], visible_top, visible_bottom);
|
||||||
bool visible = slot->y < visible_bottom && slot->y + slot->height > visible_top;
|
}
|
||||||
|
for (int i = 0; i < REPEAT_SLOT_COUNT; i++) {
|
||||||
if (visible && !slot->bitmap) {
|
update_slot_visibility(&s_repeat_slots[i], visible_top, visible_bottom);
|
||||||
slot->bitmap = gbitmap_create_with_resource(card_resource_id(slot->card));
|
|
||||||
bitmap_layer_set_bitmap(slot->bitmap_layer, slot->bitmap);
|
|
||||||
} else if (!visible && slot->bitmap) {
|
|
||||||
bitmap_layer_set_bitmap(slot->bitmap_layer, NULL);
|
|
||||||
gbitmap_destroy(slot->bitmap);
|
|
||||||
slot->bitmap = NULL;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,25 +169,87 @@ static void build_content(int16_t width) {
|
|||||||
|
|
||||||
int16_t y = MARGIN;
|
int16_t y = MARGIN;
|
||||||
|
|
||||||
|
/* The day this reading is for (reading->utc_moment, set by
|
||||||
|
* reading_generate() - see reading_state_recompute()), not necessarily
|
||||||
|
* "today" if the report is viewed right at a UTC-midnight boundary
|
||||||
|
* before the next recompute. Plain gmtime(), same justification as
|
||||||
|
* reading_state.c's own use of it. */
|
||||||
|
struct tm *utc = gmtime(&reading->utc_moment);
|
||||||
|
snprintf(s_reading_date, sizeof s_reading_date, "%s %04d-%02d-%02d",
|
||||||
|
i18n_get("interp.reading_date", "Reading for:"), utc->tm_year + 1900,
|
||||||
|
utc->tm_mon + 1, utc->tm_mday);
|
||||||
|
y = add_text(width, y, s_reading_date, s_font_body);
|
||||||
|
|
||||||
y = add_text(width, y, i18n_get("interp.heading_day_significance", "Day Significance"), s_font_section);
|
y = add_text(width, y, i18n_get("interp.heading_day_significance", "Day Significance"), s_font_section);
|
||||||
snprintf(s_day_title, sizeof s_day_title, "%s (%d/%d)", day_level_name(interp->day_level),
|
snprintf(s_day_title, sizeof s_day_title, "%s (%d/%d)", day_level_name(interp->day_level),
|
||||||
interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
|
interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
|
||||||
y = add_text(width, y, s_day_title, s_font_body);
|
y = add_text(width, y, s_day_title, s_font_body);
|
||||||
|
|
||||||
y = add_text(width, y, i18n_get("interp.heading_transits", "Significant Transits"), s_font_section);
|
/* Build every transit's title + narrative up front (used both for the
|
||||||
if (interp->top_item_count == 0) {
|
* repeated top-item line below and the full list further down), so
|
||||||
y = add_text(width, y, i18n_get("interp.no_notable_transits", "No notable transits today."), s_font_body);
|
* neither buffer is computed twice for item 0. */
|
||||||
} else {
|
if (interp->top_item_count > 0) {
|
||||||
for (int i = 0; i < interp->top_item_count; i++) {
|
for (int i = 0; i < interp->top_item_count; i++) {
|
||||||
const Aspect *a = &interp->top_items[i].aspect;
|
const Aspect *a = &interp->top_items[i].aspect;
|
||||||
snprintf(s_transit_titles[i], sizeof s_transit_titles[i], "%s %s %s %s %s",
|
snprintf(s_transit_titles[i], sizeof s_transit_titles[i], "%s %s %s %s %s",
|
||||||
i18n_get("ui.transiting", "Transiting"), astro_body_name(a->transiting_planet),
|
i18n_get("ui.transiting", "Transiting"), astro_body_name(a->transiting_planet),
|
||||||
astro_aspect_name(a->type), i18n_get("ui.natal", "natal"),
|
astro_aspect_name(a->type), i18n_get("ui.natal", "natal"),
|
||||||
astro_body_name(a->natal_planet));
|
astro_body_name(a->natal_planet));
|
||||||
y = add_text(width, y, s_transit_titles[i], s_font_subhead);
|
|
||||||
|
|
||||||
narrative_write(s_narratives[i], sizeof s_narratives[i], "", a,
|
narrative_write(s_narratives[i], sizeof s_narratives[i], "", a,
|
||||||
reading->transits.bodies[a->transiting_planet].house);
|
reading->transits.bodies[a->transiting_planet].house);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
y = add_text(width, y, i18n_get("interp.heading_guidance", "Guidance"), s_font_section);
|
||||||
|
|
||||||
|
/* Attitude/Outcome preview, side by side, image only - no per-card
|
||||||
|
* caption text. A "Position: Card Name (Reversed)" label doesn't fit
|
||||||
|
* a half-width column without wrapping into 3+ short lines, and with
|
||||||
|
* both columns wrapping independently the reader ends up reading
|
||||||
|
* fragments across columns rather than down one column at a time
|
||||||
|
* (confirmed on-device: it reads as one garbled interleaved sentence,
|
||||||
|
* not two). Captions are unnecessary anyway - the guidance text
|
||||||
|
* printed immediately below already names and quotes both cards in
|
||||||
|
* full ("Deine Haltungskarte ist ..." / "Das wahrscheinliche Ergebnis
|
||||||
|
* ... ist ..."), and both reappear with their full position/name/
|
||||||
|
* meaning again in the Celtic Cross walkthrough further down. */
|
||||||
|
{
|
||||||
|
int16_t half_w = width / 2;
|
||||||
|
const TarotDraw *attitude = &reading->spread.positions[POSITION_ATTITUDE];
|
||||||
|
const TarotDraw *outcome = &reading->spread.positions[POSITION_OUTCOME];
|
||||||
|
int16_t img_height = card_image_height_for_width(IMAGE_WIDTH);
|
||||||
|
|
||||||
|
s_repeat_slots[0].card = attitude->card;
|
||||||
|
s_repeat_slots[0].y = y;
|
||||||
|
s_repeat_slots[0].height = img_height;
|
||||||
|
s_repeat_slots[0].bitmap_layer = bitmap_layer_create(GRect(0, y, half_w, img_height));
|
||||||
|
bitmap_layer_set_alignment(s_repeat_slots[0].bitmap_layer, GAlignCenter);
|
||||||
|
scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_layer(s_repeat_slots[0].bitmap_layer));
|
||||||
|
|
||||||
|
s_repeat_slots[1].card = outcome->card;
|
||||||
|
s_repeat_slots[1].y = y;
|
||||||
|
s_repeat_slots[1].height = img_height;
|
||||||
|
s_repeat_slots[1].bitmap_layer =
|
||||||
|
bitmap_layer_create(GRect(half_w, y, width - half_w, img_height));
|
||||||
|
bitmap_layer_set_alignment(s_repeat_slots[1].bitmap_layer, GAlignCenter);
|
||||||
|
scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_layer(s_repeat_slots[1].bitmap_layer));
|
||||||
|
|
||||||
|
y += img_height + MARGIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interp->top_item_count > 0) {
|
||||||
|
y = add_text(width, y, s_transit_titles[0], s_font_subhead);
|
||||||
|
y = add_text(width, y, s_narratives[0], s_font_body);
|
||||||
|
}
|
||||||
|
guidance_write(s_guidance, sizeof s_guidance, "", interp, &reading->spread);
|
||||||
|
y = add_text(width, y, s_guidance, s_font_body);
|
||||||
|
|
||||||
|
y = add_text(width, y, i18n_get("interp.heading_transits", "Significant Transits"), s_font_section);
|
||||||
|
if (interp->top_item_count == 0) {
|
||||||
|
y = add_text(width, y, i18n_get("interp.no_notable_transits", "No notable transits today."), s_font_body);
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < interp->top_item_count; i++) {
|
||||||
|
y = add_text(width, y, s_transit_titles[i], s_font_subhead);
|
||||||
y = add_text(width, y, s_narratives[i], s_font_body);
|
y = add_text(width, y, s_narratives[i], s_font_body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,10 +276,6 @@ static void build_content(int16_t width) {
|
|||||||
y = add_text(width, y, tarot_card_meaning(draw->card, draw->reversed), s_font_body);
|
y = add_text(width, y, tarot_card_meaning(draw->card, draw->reversed), s_font_body);
|
||||||
}
|
}
|
||||||
|
|
||||||
y = add_text(width, y, i18n_get("interp.heading_guidance", "Guidance"), s_font_section);
|
|
||||||
guidance_write(s_guidance, sizeof s_guidance, "", interp, &reading->spread);
|
|
||||||
y = add_text(width, y, s_guidance, s_font_body);
|
|
||||||
|
|
||||||
scroll_layer_set_content_size(s_scroll_layer, GSize(width, y));
|
scroll_layer_set_content_size(s_scroll_layer, GSize(width, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-1
File diff suppressed because one or more lines are too long
+10
-7
@@ -63,15 +63,18 @@ def configure(ctx):
|
|||||||
def build(ctx):
|
def build(ctx):
|
||||||
ctx.load('pebble_sdk')
|
ctx.load('pebble_sdk')
|
||||||
|
|
||||||
# Regenerate src/c/i18n_tables.auto.c from engine/i18n/*.lang, and
|
# Regenerate src/c/i18n_tables.auto.c from engine/i18n/*.lang,
|
||||||
# resources/img/*.png from res/img/*.jpeg, before compiling/bundling
|
# resources/img/*.png from res/img/*.jpeg, and resources/app_icon.png
|
||||||
# anything - see each script's own doc comment. Both run eagerly (not
|
# from res/app_icon_50x50_64_color.png, before compiling/bundling
|
||||||
# as waf Tasks) since the bundle step below depends on their output
|
# anything - see each script's own doc comment. All three run eagerly
|
||||||
# existing; gen_card_images.py skips any card whose PNG is already
|
# (not as waf Tasks) since the bundle step below depends on their
|
||||||
# up to date, so this stays cheap on repeat builds. Requires Pillow
|
# output existing; gen_card_images.py/gen_app_icon.py both skip
|
||||||
# (`pip install Pillow`) - see watch/README.md.
|
# regenerating anything already up to date, so this stays cheap on
|
||||||
|
# repeat builds. Requires Pillow (`pip install Pillow`) - see
|
||||||
|
# watch/README.md.
|
||||||
subprocess.check_call([sys.executable, 'scripts/gen_i18n_tables.py'])
|
subprocess.check_call([sys.executable, 'scripts/gen_i18n_tables.py'])
|
||||||
subprocess.check_call([sys.executable, 'scripts/gen_card_images.py'])
|
subprocess.check_call([sys.executable, 'scripts/gen_card_images.py'])
|
||||||
|
subprocess.check_call([sys.executable, 'scripts/gen_app_icon.py'])
|
||||||
|
|
||||||
engine_dir = ctx.path.parent.find_dir('engine/src')
|
engine_dir = ctx.path.parent.find_dir('engine/src')
|
||||||
engine_nodes = [engine_dir.find_node(name) for name in ENGINE_SRCS]
|
engine_nodes = [engine_dir.find_node(name) for name in ENGINE_SRCS]
|
||||||
|
|||||||
Reference in New Issue
Block a user